Merge "Add ability to AND and OR conditions together" into tm-qpr-dev am: 9038b06da0

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/20489827

Change-Id: I89606eb4288a72da6485cba7a203c54af8c8d1ac
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/packages/SystemUI/src/com/android/systemui/util/condition/CombinedCondition.kt b/packages/SystemUI/src/com/android/systemui/util/condition/CombinedCondition.kt
new file mode 100644
index 0000000..da81d54
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/condition/CombinedCondition.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util.condition
+
+/**
+ * A higher order [Condition] which combines multiple conditions with a specified
+ * [Evaluator.ConditionOperand].
+ */
+internal class CombinedCondition
+constructor(
+    private val conditions: Collection<Condition>,
+    @Evaluator.ConditionOperand private val operand: Int
+) : Condition(null, false), Condition.Callback {
+
+    override fun start() {
+        onConditionChanged(this)
+        conditions.forEach { it.addCallback(this) }
+    }
+
+    override fun onConditionChanged(condition: Condition) {
+        Evaluator.evaluate(conditions, operand)?.also { value -> updateCondition(value) }
+            ?: clearCondition()
+    }
+
+    override fun stop() {
+        conditions.forEach { it.removeCallback(this) }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/condition/Condition.java b/packages/SystemUI/src/com/android/systemui/util/condition/Condition.java
index 2c317dd..b39adef 100644
--- a/packages/SystemUI/src/com/android/systemui/util/condition/Condition.java
+++ b/packages/SystemUI/src/com/android/systemui/util/condition/Condition.java
@@ -24,7 +24,10 @@
 
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
 import java.util.Iterator;
+import java.util.List;
 
 /**
  * Base class for a condition that needs to be fulfilled in order for {@link Monitor} to inform
@@ -181,6 +184,42 @@
     }
 
     /**
+     * Creates a new condition which will only be true when both this condition and all the provided
+     * conditions are true.
+     */
+    public Condition and(Collection<Condition> others) {
+        final List<Condition> conditions = new ArrayList<>(others);
+        conditions.add(this);
+        return new CombinedCondition(conditions, Evaluator.OP_AND);
+    }
+
+    /**
+     * Creates a new condition which will only be true when both this condition and the provided
+     * condition is true.
+     */
+    public Condition and(Condition other) {
+        return new CombinedCondition(Arrays.asList(this, other), Evaluator.OP_AND);
+    }
+
+    /**
+     * Creates a new condition which will only be true when either this condition or any of the
+     * provided conditions are true.
+     */
+    public Condition or(Collection<Condition> others) {
+        final List<Condition> conditions = new ArrayList<>(others);
+        conditions.add(this);
+        return new CombinedCondition(conditions, Evaluator.OP_OR);
+    }
+
+    /**
+     * Creates a new condition which will only be true when either this condition or the provided
+     * condition is true.
+     */
+    public Condition or(Condition other) {
+        return new CombinedCondition(Arrays.asList(this, other), Evaluator.OP_OR);
+    }
+
+    /**
      * Callback that receives updates about whether the condition has been fulfilled.
      */
     public interface Callback {
diff --git a/packages/SystemUI/src/com/android/systemui/util/condition/Evaluator.kt b/packages/SystemUI/src/com/android/systemui/util/condition/Evaluator.kt
new file mode 100644
index 0000000..cf44e84
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/condition/Evaluator.kt
@@ -0,0 +1,92 @@
+package com.android.systemui.util.condition
+
+import android.annotation.IntDef
+
+/**
+ * Helper for evaluating a collection of [Condition] objects with a given
+ * [Evaluator.ConditionOperand]
+ */
+internal object Evaluator {
+    /** Operands for combining multiple conditions together */
+    @Retention(AnnotationRetention.SOURCE)
+    @IntDef(value = [OP_AND, OP_OR])
+    annotation class ConditionOperand
+
+    /**
+     * 3-valued logical AND operand, with handling for unknown values (represented as null)
+     *
+     * ```
+     * +-----+----+---+---+
+     * | AND | T  | F | U |
+     * +-----+----+---+---+
+     * | T   | T  | F | U |
+     * | F   | F  | F | F |
+     * | U   | U  | F | U |
+     * +-----+----+---+---+
+     * ```
+     */
+    const val OP_AND = 0
+
+    /**
+     * 3-valued logical OR operand, with handling for unknown values (represented as null)
+     *
+     * ```
+     * +-----+----+---+---+
+     * | OR  | T  | F | U |
+     * +-----+----+---+---+
+     * | T   | T  | T | T |
+     * | F   | T  | F | U |
+     * | U   | T  | U | U |
+     * +-----+----+---+---+
+     * ```
+     */
+    const val OP_OR = 1
+
+    /**
+     * Evaluates a set of conditions with a given operand
+     *
+     * If overriding conditions are present, they take precedence over normal conditions if set.
+     *
+     * @param conditions The collection of conditions to evaluate. If empty, null is returned.
+     * @param operand The operand to use when evaluating.
+     * @return Either true or false if the value is known, or null if value is unknown
+     */
+    fun evaluate(conditions: Collection<Condition>, @ConditionOperand operand: Int): Boolean? {
+        if (conditions.isEmpty()) return null
+        // If there are overriding conditions with values set, they take precedence.
+        val targetConditions =
+            conditions
+                .filter { it.isConditionSet && it.isOverridingCondition }
+                .ifEmpty { conditions }
+        return when (operand) {
+            OP_AND ->
+                threeValuedAndOrOr(conditions = targetConditions, returnValueIfAnyMatches = false)
+            OP_OR ->
+                threeValuedAndOrOr(conditions = targetConditions, returnValueIfAnyMatches = true)
+            else -> null
+        }
+    }
+
+    /**
+     * Helper for evaluating 3-valued logical AND/OR.
+     *
+     * @param returnValueIfAnyMatches AND returns false if any value is false. OR returns true if
+     * any value is true.
+     */
+    private fun threeValuedAndOrOr(
+        conditions: Collection<Condition>,
+        returnValueIfAnyMatches: Boolean
+    ): Boolean? {
+        var hasUnknown = false
+        for (condition in conditions) {
+            if (!condition.isConditionSet) {
+                hasUnknown = true
+                continue
+            }
+            if (condition.isConditionMet == returnValueIfAnyMatches) {
+                return returnValueIfAnyMatches
+            }
+        }
+        return if (hasUnknown) null else !returnValueIfAnyMatches
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/condition/Monitor.java b/packages/SystemUI/src/com/android/systemui/util/condition/Monitor.java
index cb430ba..24bc907 100644
--- a/packages/SystemUI/src/com/android/systemui/util/condition/Monitor.java
+++ b/packages/SystemUI/src/com/android/systemui/util/condition/Monitor.java
@@ -24,12 +24,10 @@
 import org.jetbrains.annotations.NotNull;
 
 import java.util.ArrayList;
-import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Set;
 import java.util.concurrent.Executor;
-import java.util.stream.Collectors;
 
 import javax.inject.Inject;
 
@@ -57,21 +55,10 @@
         }
 
         public void update() {
-            // Only consider set conditions.
-            final Collection<Condition> setConditions = mSubscription.mConditions.stream()
-                    .filter(Condition::isConditionSet).collect(Collectors.toSet());
-
-            // Overriding conditions do not override each other
-            final Collection<Condition> overridingConditions = setConditions.stream()
-                    .filter(Condition::isOverridingCondition).collect(Collectors.toSet());
-
-            final Collection<Condition> targetCollection = overridingConditions.isEmpty()
-                    ? setConditions : overridingConditions;
-
-            final boolean newAllConditionsMet = targetCollection.isEmpty() ? true : targetCollection
-                    .stream()
-                    .map(Condition::isConditionMet)
-                    .allMatch(conditionMet -> conditionMet);
+            final Boolean result = Evaluator.INSTANCE.evaluate(mSubscription.mConditions,
+                    Evaluator.OP_AND);
+            // Consider unknown (null) as true
+            final boolean newAllConditionsMet = result == null || result;
 
             if (mAllConditionsMet != null && newAllConditionsMet == mAllConditionsMet) {
                 return;
@@ -109,6 +96,7 @@
 
     /**
      * Registers a callback and the set of conditions to trigger it.
+     *
      * @param subscription A {@link Subscription} detailing the desired conditions and callback.
      * @return A {@link Subscription.Token} that can be used to remove the subscription.
      */
@@ -139,6 +127,7 @@
 
     /**
      * Removes a subscription from participating in future callbacks.
+     *
      * @param token The {@link Subscription.Token} returned when the {@link Subscription} was
      *              originally added.
      */
@@ -179,7 +168,9 @@
         private final Set<Condition> mConditions;
         private final Callback mCallback;
 
-        /** */
+        /**
+         *
+         */
         public Subscription(Set<Condition> conditions, Callback callback) {
             this.mConditions = Collections.unmodifiableSet(conditions);
             this.mCallback = callback;
@@ -209,7 +200,6 @@
 
             /**
              * Default constructor specifying the {@link Callback} for the {@link Subscription}.
-             * @param callback
              */
             public Builder(Callback callback) {
                 mCallback = callback;
@@ -218,7 +208,7 @@
 
             /**
              * Adds a {@link Condition} to be associated with the {@link Subscription}.
-             * @param condition
+             *
              * @return The updated {@link Builder}.
              */
             public Builder addCondition(Condition condition) {
@@ -228,7 +218,7 @@
 
             /**
              * Adds a set of {@link Condition} to be associated with the {@link Subscription}.
-             * @param condition
+             *
              * @return The updated {@link Builder}.
              */
             public Builder addConditions(Set<Condition> condition) {
@@ -238,6 +228,7 @@
 
             /**
              * Builds the {@link Subscription}.
+             *
              * @return The resulting {@link Subscription}.
              */
             public Subscription build() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/condition/ConditionTest.java b/packages/SystemUI/tests/src/com/android/systemui/util/condition/ConditionTest.java
index 0b53133..2878864 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/util/condition/ConditionTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/condition/ConditionTest.java
@@ -141,4 +141,158 @@
         mCondition.clearCondition();
         assertThat(mCondition.isConditionSet()).isFalse();
     }
+
+    @Test
+    public void combineConditionsWithOr_allFalse_reportsNotMet() {
+        mCondition.fakeUpdateCondition(false);
+
+        final Condition combinedCondition = mCondition.or(
+                new FakeCondition(/* initialValue= */ false));
+
+        final Condition.Callback callback = mock(Condition.Callback.class);
+        combinedCondition.addCallback(callback);
+
+        assertThat(combinedCondition.isConditionSet()).isTrue();
+        assertThat(combinedCondition.isConditionMet()).isFalse();
+        verify(callback, times(1)).onConditionChanged(combinedCondition);
+    }
+
+    @Test
+    public void combineConditionsWithOr_allTrue_reportsMet() {
+        mCondition.fakeUpdateCondition(true);
+
+        final Condition combinedCondition = mCondition.or(
+                new FakeCondition(/* initialValue= */ true));
+
+        final Condition.Callback callback = mock(Condition.Callback.class);
+        combinedCondition.addCallback(callback);
+
+        assertThat(combinedCondition.isConditionSet()).isTrue();
+        assertThat(combinedCondition.isConditionMet()).isTrue();
+        verify(callback, times(1)).onConditionChanged(combinedCondition);
+    }
+
+    @Test
+    public void combineConditionsWithOr_singleTrue_reportsMet() {
+        mCondition.fakeUpdateCondition(false);
+
+        final Condition combinedCondition = mCondition.or(
+                new FakeCondition(/* initialValue= */ true));
+
+        final Condition.Callback callback = mock(Condition.Callback.class);
+        combinedCondition.addCallback(callback);
+
+        assertThat(combinedCondition.isConditionSet()).isTrue();
+        assertThat(combinedCondition.isConditionMet()).isTrue();
+        verify(callback, times(1)).onConditionChanged(combinedCondition);
+    }
+
+    @Test
+    public void combineConditionsWithOr_unknownAndTrue_reportsMet() {
+        mCondition.fakeUpdateCondition(true);
+
+        // Combine with an unset condition.
+        final Condition combinedCondition = mCondition.or(
+                new FakeCondition(/* initialValue= */ null));
+
+        final Condition.Callback callback = mock(Condition.Callback.class);
+        combinedCondition.addCallback(callback);
+
+        assertThat(combinedCondition.isConditionSet()).isTrue();
+        assertThat(combinedCondition.isConditionMet()).isTrue();
+        verify(callback, times(1)).onConditionChanged(combinedCondition);
+    }
+
+    @Test
+    public void combineConditionsWithOr_unknownAndFalse_reportsNotMet() {
+        mCondition.fakeUpdateCondition(false);
+
+        // Combine with an unset condition.
+        final Condition combinedCondition = mCondition.or(
+                new FakeCondition(/* initialValue= */ null));
+
+        final Condition.Callback callback = mock(Condition.Callback.class);
+        combinedCondition.addCallback(callback);
+
+        assertThat(combinedCondition.isConditionSet()).isFalse();
+        assertThat(combinedCondition.isConditionMet()).isFalse();
+        verify(callback, never()).onConditionChanged(combinedCondition);
+    }
+
+    @Test
+    public void combineConditionsWithAnd_allFalse_reportsNotMet() {
+        mCondition.fakeUpdateCondition(false);
+
+        final Condition combinedCondition = mCondition.and(
+                new FakeCondition(/* initialValue= */ false));
+
+        final Condition.Callback callback = mock(Condition.Callback.class);
+        combinedCondition.addCallback(callback);
+
+        assertThat(combinedCondition.isConditionSet()).isTrue();
+        assertThat(combinedCondition.isConditionMet()).isFalse();
+        verify(callback, times(1)).onConditionChanged(combinedCondition);
+    }
+
+    @Test
+    public void combineConditionsWithAnd_allTrue_reportsMet() {
+        mCondition.fakeUpdateCondition(true);
+
+        final Condition combinedCondition = mCondition.and(
+                new FakeCondition(/* initialValue= */ true));
+
+        final Condition.Callback callback = mock(Condition.Callback.class);
+        combinedCondition.addCallback(callback);
+
+        assertThat(combinedCondition.isConditionSet()).isTrue();
+        assertThat(combinedCondition.isConditionMet()).isTrue();
+        verify(callback, times(1)).onConditionChanged(combinedCondition);
+    }
+
+    @Test
+    public void combineConditionsWithAnd_singleTrue_reportsNotMet() {
+        mCondition.fakeUpdateCondition(true);
+
+        final Condition combinedCondition = mCondition.and(
+                new FakeCondition(/* initialValue= */ false));
+
+        final Condition.Callback callback = mock(Condition.Callback.class);
+        combinedCondition.addCallback(callback);
+
+        assertThat(combinedCondition.isConditionSet()).isTrue();
+        assertThat(combinedCondition.isConditionMet()).isFalse();
+        verify(callback, times(1)).onConditionChanged(combinedCondition);
+    }
+
+    @Test
+    public void combineConditionsWithAnd_unknownAndTrue_reportsNotMet() {
+        mCondition.fakeUpdateCondition(true);
+
+        // Combine with an unset condition.
+        final Condition combinedCondition = mCondition.and(
+                new FakeCondition(/* initialValue= */ null));
+
+        final Condition.Callback callback = mock(Condition.Callback.class);
+        combinedCondition.addCallback(callback);
+
+        assertThat(combinedCondition.isConditionSet()).isFalse();
+        assertThat(combinedCondition.isConditionMet()).isFalse();
+        verify(callback, never()).onConditionChanged(combinedCondition);
+    }
+
+    @Test
+    public void combineConditionsWithAnd_unknownAndFalse_reportsMet() {
+        mCondition.fakeUpdateCondition(false);
+
+        // Combine with an unset condition.
+        final Condition combinedCondition = mCondition.and(
+                new FakeCondition(/* initialValue= */ null));
+
+        final Condition.Callback callback = mock(Condition.Callback.class);
+        combinedCondition.addCallback(callback);
+
+        assertThat(combinedCondition.isConditionSet()).isTrue();
+        assertThat(combinedCondition.isConditionMet()).isFalse();
+        verify(callback, times(1)).onConditionChanged(combinedCondition);
+    }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/condition/FakeCondition.java b/packages/SystemUI/tests/utils/src/com/android/systemui/util/condition/FakeCondition.java
index 1353ad2..07ed110 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/util/condition/FakeCondition.java
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/condition/FakeCondition.java
@@ -25,15 +25,21 @@
         super();
     }
 
-    FakeCondition(Boolean initialValue, Boolean overriding) {
+    FakeCondition(Boolean initialValue) {
+        super(initialValue, false);
+    }
+
+    FakeCondition(Boolean initialValue, boolean overriding) {
         super(initialValue, overriding);
     }
 
     @Override
-    public void start() {}
+    public void start() {
+    }
 
     @Override
-    public void stop() {}
+    public void stop() {
+    }
 
     public void fakeUpdateCondition(boolean isConditionMet) {
         updateCondition(isConditionMet);