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 android.annotation.IntDef
20 
21 /**
22  * Helper for evaluating a collection of [Condition] objects with a given
23  * [Evaluator.ConditionOperand]
24  */
25 object Evaluator {
26     /** Operands for combining multiple conditions together */
27     @Retention(AnnotationRetention.SOURCE)
28     @IntDef(value = [OP_AND, OP_OR])
29     annotation class ConditionOperand
30 
31     /**
32      * 3-valued logical AND operand, with handling for unknown values (represented as null)
33      *
34      * ```
35      * +-----+----+---+---+
36      * | AND | T  | F | U |
37      * +-----+----+---+---+
38      * | T   | T  | F | U |
39      * | F   | F  | F | F |
40      * | U   | U  | F | U |
41      * +-----+----+---+---+
42      * ```
43      */
44     const val OP_AND = 0
45 
46     /**
47      * 3-valued logical OR operand, with handling for unknown values (represented as null)
48      *
49      * ```
50      * +-----+----+---+---+
51      * | OR  | T  | F | U |
52      * +-----+----+---+---+
53      * | T   | T  | T | T |
54      * | F   | T  | F | U |
55      * | U   | T  | U | U |
56      * +-----+----+---+---+
57      * ```
58      */
59     const val OP_OR = 1
60 
61     /**
62      * Evaluates a set of conditions with a given operand
63      *
64      * If overriding conditions are present, they take precedence over normal conditions if set.
65      *
66      * @param conditions The collection of conditions to evaluate. If empty, null is returned.
67      * @param operand The operand to use when evaluating.
68      * @return Either true or false if the value is known, or null if value is unknown
69      */
70     fun evaluate(conditions: Collection<Condition>, @ConditionOperand operand: Int): Boolean? {
71         if (conditions.isEmpty()) return null
72         // If there are overriding conditions with values set, they take precedence.
73         val values: Collection<Boolean?> =
74             conditions
75                 .filter { it.isConditionSet && it.isOverridingCondition }
76                 .ifEmpty { conditions }
77                 .map { condition ->
78                     if (condition.isConditionSet) {
79                         condition.isConditionMet
80                     } else {
81                         null
82                     }
83                 }
84         return evaluate(values = values, operand = operand)
85     }
86 
87     /**
88      * Evaluates a set of booleans with a given operand
89      *
90      * @param operand The operand to use when evaluating.
91      * @return Either true or false if the value is known, or null if value is unknown
92      */
93     internal fun evaluate(values: Collection<Boolean?>, @ConditionOperand operand: Int): Boolean? {
94         if (values.isEmpty()) return null
95         return when (operand) {
96             OP_AND -> threeValuedAndOrOr(values = values, returnValueIfAnyMatches = false)
97             OP_OR -> threeValuedAndOrOr(values = values, returnValueIfAnyMatches = true)
98             else -> null
99         }
100     }
101 
102     /**
103      * Helper for evaluating 3-valued logical AND/OR.
104      *
105      * @param returnValueIfAnyMatches AND returns false if any value is false. OR returns true if
106      *   any value is true.
107      */
108     private fun threeValuedAndOrOr(
109         values: Collection<Boolean?>,
110         returnValueIfAnyMatches: Boolean
111     ): Boolean? {
112         var hasUnknown = false
113         for (value in values) {
114             if (value == null) {
115                 hasUnknown = true
116                 continue
117             }
118             if (value == returnValueIfAnyMatches) {
119                 return returnValueIfAnyMatches
120             }
121         }
122         return if (hasUnknown) null else !returnValueIfAnyMatches
123     }
124 }
125