1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.shared.condition
18 
19 import kotlinx.coroutines.CoroutineScope
20 import kotlinx.coroutines.ExperimentalCoroutinesApi
21 import kotlinx.coroutines.Job
22 import kotlinx.coroutines.channels.awaitClose
23 import kotlinx.coroutines.flow.Flow
24 import kotlinx.coroutines.flow.callbackFlow
25 import kotlinx.coroutines.flow.distinctUntilChanged
26 import kotlinx.coroutines.flow.flatMapLatest
27 import kotlinx.coroutines.flow.flowOf
28 import kotlinx.coroutines.launch
29 
30 /**
31  * A higher order [Condition] which combines multiple conditions with a specified
32  * [Evaluator.ConditionOperand]. Conditions are executed lazily as-needed.
33  *
34  * @param scope The [CoroutineScope] to execute in.
35  * @param conditions The list of conditions to evaluate. Since conditions are executed lazily, the
36  *   ordering is important here.
37  * @param operand The [Evaluator.ConditionOperand] to apply to the conditions.
38  */
39 @OptIn(ExperimentalCoroutinesApi::class)
40 class CombinedCondition
41 constructor(
42     private val scope: CoroutineScope,
43     private val conditions: Collection<Condition>,
44     @Evaluator.ConditionOperand private val operand: Int
45 ) : Condition(scope, null, false) {
46 
47     private var job: Job? = null
48     private val _startStrategy by lazy { calculateStartStrategy() }
49 
50     override fun start() {
51         job =
52             scope.launch {
53                 val groupedConditions = conditions.groupBy { it.isOverridingCondition }
54 
55                 lazilyEvaluate(
56                         conditions = groupedConditions.getOrDefault(true, emptyList()),
57                         filterUnknown = true
58                     )
59                     .distinctUntilChanged()
60                     .flatMapLatest { overriddenValue ->
61                         // If there are overriding conditions with values set, they take precedence.
62                         if (overriddenValue == null) {
63                             lazilyEvaluate(
64                                 conditions = groupedConditions.getOrDefault(false, emptyList()),
65                                 filterUnknown = false
66                             )
67                         } else {
68                             flowOf(overriddenValue)
69                         }
70                     }
71                     .collect { conditionMet ->
72                         if (conditionMet == null) {
73                             clearCondition()
74                         } else {
75                             updateCondition(conditionMet)
76                         }
77                     }
78             }
79     }
80 
81     override fun stop() {
82         job?.cancel()
83         job = null
84     }
85 
86     /**
87      * Evaluates a list of conditions lazily with support for short-circuiting. Conditions are
88      * executed serially in the order provided. At any point if the result can be determined, we
89      * short-circuit and return the result without executing all conditions.
90      */
91     private fun lazilyEvaluate(
92         conditions: Collection<Condition>,
93         filterUnknown: Boolean,
94     ): Flow<Boolean?> = callbackFlow {
95         val jobs = MutableList<Job?>(conditions.size) { null }
96         val values = MutableList<Boolean?>(conditions.size) { null }
97         val flows = conditions.map { it.toFlow() }
98 
99         fun cancelAllExcept(indexToSkip: Int) {
100             for (index in 0 until jobs.size) {
101                 if (index == indexToSkip) {
102                     continue
103                 }
104                 if (
105                     indexToSkip == -1 ||
106                         conditions.elementAt(index).startStrategy == START_WHEN_NEEDED
107                 ) {
108                     jobs[index]?.cancel()
109                     jobs[index] = null
110                     values[index] = null
111                 }
112             }
113         }
114 
115         fun collectFlow(index: Int) {
116             // Base case which is triggered once we have collected all the flows. In this case,
117             // we never short-circuited and therefore should return the fully evaluated
118             // conditions.
119             if (flows.isEmpty() || index == -1) {
120                 val filteredValues =
121                     if (filterUnknown) {
122                         values.filterNotNull()
123                     } else {
124                         values
125                     }
126                 trySend(Evaluator.evaluate(filteredValues, operand))
127                 return
128             }
129             jobs[index] =
130                 scope.launch {
131                     flows.elementAt(index).collect { value ->
132                         values[index] = value
133                         if (shouldEarlyReturn(value)) {
134                             trySend(value)
135                             // The overall result is contingent on this condition, so we don't need
136                             // to monitor any other conditions.
137                             cancelAllExcept(index)
138                         } else {
139                             collectFlow(jobs.indexOfFirst { it == null })
140                         }
141                     }
142                 }
143         }
144 
145         // Collect any eager conditions immediately.
146         var started = false
147         for ((index, condition) in conditions.withIndex()) {
148             if (condition.startStrategy == START_EAGERLY) {
149                 collectFlow(index)
150                 started = true
151             }
152         }
153 
154         // If no eager conditions started, start the first condition to kick off evaluation.
155         if (!started) {
156             collectFlow(0)
157         }
158         awaitClose { cancelAllExcept(-1) }
159     }
160 
161     private fun shouldEarlyReturn(conditionMet: Boolean?): Boolean {
162         return when (operand) {
163             Evaluator.OP_AND -> conditionMet == false
164             Evaluator.OP_OR -> conditionMet == true
165             else -> false
166         }
167     }
168 
169     /**
170      * Calculate the start strategy for this condition. This depends on the strategies of the child
171      * conditions. If there are any eager conditions, we must also start this condition eagerly. In
172      * the absence of eager conditions, we check for lazy conditions. In the absence of either, we
173      * make the condition only start when needed.
174      */
175     private fun calculateStartStrategy(): Int {
176         var startStrategy = START_WHEN_NEEDED
177         for (condition in conditions) {
178             when (condition.startStrategy) {
179                 START_EAGERLY -> return START_EAGERLY
180                 START_LAZILY -> {
181                     startStrategy = START_LAZILY
182                 }
183                 START_WHEN_NEEDED -> {
184                     // this is the default, so do nothing
185                 }
186             }
187         }
188         return startStrategy
189     }
190 
191     override fun getStartStrategy(): Int {
192         return _startStrategy
193     }
194 }
195