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