From b0e25804072680d2ff4cc102a42dee38a16f3828 Mon Sep 17 00:00:00 2001 From: Lucas Silva Date: Wed, 19 Apr 2023 11:03:48 -0400 Subject: [PATCH] Add support for lazy conditions Conditions now define a start strategy, which determines how they are run. START_EAGERLY is how conditions function today, it starts the condition immediately and doesn't stop it. START_LAZILY will wait to start the condition until the previous one has emitted a value which did not short-circuit. For AND operand, this means the previous condition must have emitted true. For OR operand, it would be false. START_WHEN_NEEDED is similar to lazy, but will also cancel the condition when not needed. This can be used for expensive conditions, such as the low light condition, since it keeps the lowlight sensor on. Bug: 278875229 Test: atest CombinedConditionTest Change-Id: Ie81f74357f1d37c771fa91d097fef85ab11fa604 --- .../shared/condition/CombinedCondition.kt | 172 ++++++- .../systemui/shared/condition/Condition.java | 58 ++- .../shared/condition/ConditionExtensions.kt | 32 +- .../systemui/shared/condition/Evaluator.kt | 36 +- .../AssistantAttentionCondition.java | 10 + .../dreams/conditions/DreamCondition.java | 16 +- .../condition/SystemProcessCondition.java | 13 +- .../AssistantAttentionConditionTest.java | 6 +- .../dreams/conditions/DreamConditionTest.java | 18 +- .../condition/SystemProcessConditionTest.java | 9 +- .../shared/condition/CombinedConditionTest.kt | 450 ++++++++++++++++++ .../condition/ConditionExtensionsTest.kt | 24 +- .../condition/ConditionMonitorTest.java | 20 +- .../shared/condition/ConditionTest.java | 174 +------ .../shared/condition/FakeCondition.java | 19 +- 15 files changed, 821 insertions(+), 236 deletions(-) create mode 100644 packages/SystemUI/tests/src/com/android/systemui/shared/condition/CombinedConditionTest.kt diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/condition/CombinedCondition.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/condition/CombinedCondition.kt index 2d83458ec2f74..a2b6e2cf9ae33 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/condition/CombinedCondition.kt +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/condition/CombinedCondition.kt @@ -16,27 +16,179 @@ package com.android.systemui.shared.condition +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.launch + /** * A higher order [Condition] which combines multiple conditions with a specified - * [Evaluator.ConditionOperand]. + * [Evaluator.ConditionOperand]. Conditions are executed lazily as-needed. + * + * @param scope The [CoroutineScope] to execute in. + * @param conditions The list of conditions to evaluate. Since conditions are executed lazily, the + * ordering is important here. + * @param operand The [Evaluator.ConditionOperand] to apply to the conditions. */ -internal class CombinedCondition +@OptIn(ExperimentalCoroutinesApi::class) +class CombinedCondition constructor( + private val scope: CoroutineScope, private val conditions: Collection, @Evaluator.ConditionOperand private val operand: Int -) : Condition(null, false), Condition.Callback { +) : Condition(scope, null, false) { + + private var job: Job? = null + private val _startStrategy by lazy { calculateStartStrategy() } override fun start() { - onConditionChanged(this) - conditions.forEach { it.addCallback(this) } - } + job = + scope.launch { + val groupedConditions = conditions.groupBy { it.isOverridingCondition } - override fun onConditionChanged(condition: Condition) { - Evaluator.evaluate(conditions, operand)?.also { value -> updateCondition(value) } - ?: clearCondition() + lazilyEvaluate( + conditions = groupedConditions.getOrDefault(true, emptyList()), + filterUnknown = true + ) + .distinctUntilChanged() + .flatMapLatest { overriddenValue -> + // If there are overriding conditions with values set, they take precedence. + if (overriddenValue == null) { + lazilyEvaluate( + conditions = groupedConditions.getOrDefault(false, emptyList()), + filterUnknown = false + ) + } else { + flowOf(overriddenValue) + } + } + .collect { conditionMet -> + if (conditionMet == null) { + clearCondition() + } else { + updateCondition(conditionMet) + } + } + } } override fun stop() { - conditions.forEach { it.removeCallback(this) } + job?.cancel() + job = null + } + + /** + * Evaluates a list of conditions lazily with support for short-circuiting. Conditions are + * executed serially in the order provided. At any point if the result can be determined, we + * short-circuit and return the result without executing all conditions. + */ + private fun lazilyEvaluate( + conditions: Collection, + filterUnknown: Boolean, + ): Flow = callbackFlow { + val jobs = MutableList(conditions.size) { null } + val values = MutableList(conditions.size) { null } + val flows = conditions.map { it.toFlow() } + + fun cancelAllExcept(indexToSkip: Int) { + for (index in 0 until jobs.size) { + if (index == indexToSkip) { + continue + } + if ( + indexToSkip == -1 || + conditions.elementAt(index).startStrategy == START_WHEN_NEEDED + ) { + jobs[index]?.cancel() + jobs[index] = null + values[index] = null + } + } + } + + fun collectFlow(index: Int) { + // Base case which is triggered once we have collected all the flows. In this case, + // we never short-circuited and therefore should return the fully evaluated + // conditions. + if (flows.isEmpty() || index == -1) { + val filteredValues = + if (filterUnknown) { + values.filterNotNull() + } else { + values + } + trySend(Evaluator.evaluate(filteredValues, operand)) + return + } + jobs[index] = + scope.launch { + flows.elementAt(index).collect { value -> + values[index] = value + if (shouldEarlyReturn(value)) { + trySend(value) + // The overall result is contingent on this condition, so we don't need + // to monitor any other conditions. + cancelAllExcept(index) + } else { + collectFlow(jobs.indexOfFirst { it == null }) + } + } + } + } + + // Collect any eager conditions immediately. + var started = false + for ((index, condition) in conditions.withIndex()) { + if (condition.startStrategy == START_EAGERLY) { + collectFlow(index) + started = true + } + } + + // If no eager conditions started, start the first condition to kick off evaluation. + if (!started) { + collectFlow(0) + } + awaitClose { cancelAllExcept(-1) } + } + + private fun shouldEarlyReturn(conditionMet: Boolean?): Boolean { + return when (operand) { + Evaluator.OP_AND -> conditionMet == false + Evaluator.OP_OR -> conditionMet == true + else -> false + } + } + + /** + * Calculate the start strategy for this condition. This depends on the strategies of the child + * conditions. If there are any eager conditions, we must also start this condition eagerly. In + * the absence of eager conditions, we check for lazy conditions. In the absence of either, we + * make the condition only start when needed. + */ + private fun calculateStartStrategy(): Int { + var startStrategy = START_WHEN_NEEDED + for (condition in conditions) { + when (condition.startStrategy) { + START_EAGERLY -> return START_EAGERLY + START_LAZILY -> { + startStrategy = START_LAZILY + } + START_WHEN_NEEDED -> { + // this is the default, so do nothing + } + } + } + return startStrategy + } + + override fun getStartStrategy(): Int { + return _startStrategy } } diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/condition/Condition.java b/packages/SystemUI/shared/src/com/android/systemui/shared/condition/Condition.java index cc48090e12176..6bf1ce57b01e0 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/condition/Condition.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/condition/Condition.java @@ -18,11 +18,14 @@ package com.android.systemui.shared.condition; import android.util.Log; +import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.lifecycle.Lifecycle; import androidx.lifecycle.LifecycleEventObserver; import androidx.lifecycle.LifecycleOwner; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Arrays; @@ -30,6 +33,8 @@ import java.util.Collection; import java.util.Iterator; import java.util.List; +import kotlinx.coroutines.CoroutineScope; + /** * Base class for a condition that needs to be fulfilled in order for {@link Monitor} to inform * its callbacks. @@ -39,24 +44,27 @@ public abstract class Condition { private final ArrayList> mCallbacks = new ArrayList<>(); private final boolean mOverriding; + private final CoroutineScope mScope; private Boolean mIsConditionMet; private boolean mStarted = false; /** * By default, conditions have an initial value of false and are not overriding. */ - public Condition() { - this(false, false); + public Condition(CoroutineScope scope) { + this(scope, false, false); } /** * Constructor for specifying initial state and overriding condition attribute. + * * @param initialConditionMet Initial state of the condition. - * @param overriding Whether this condition overrides others. + * @param overriding Whether this condition overrides others. */ - protected Condition(Boolean initialConditionMet, boolean overriding) { + protected Condition(CoroutineScope scope, Boolean initialConditionMet, boolean overriding) { mIsConditionMet = initialConditionMet; mOverriding = overriding; + mScope = scope; } /** @@ -69,6 +77,29 @@ public abstract class Condition { */ protected abstract void stop(); + /** + * Condition should be started as soon as there is an active subscription. + */ + public static final int START_EAGERLY = 0; + /** + * Condition should be started lazily only if needed. But once started, it will not be cancelled + * unless there are no more active subscriptions. + */ + public static final int START_LAZILY = 1; + /** + * Condition should be started lazily only if needed, and can be stopped when not needed. This + * should be used for conditions which are expensive to keep running. + */ + public static final int START_WHEN_NEEDED = 2; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({START_EAGERLY, START_LAZILY, START_WHEN_NEEDED}) + @interface StartStrategy { + } + + @StartStrategy + protected abstract int getStartStrategy(); + /** * Returns whether the current condition overrides */ @@ -183,6 +214,7 @@ public abstract class Condition { /** * Returns whether the condition is set. This method should be consulted to understand the * value of {@link #isConditionMet()}. + * * @return {@code true} if value is present, {@code false} otherwise. */ public boolean isConditionSet() { @@ -210,17 +242,18 @@ public abstract class Condition { * conditions are true. */ public Condition and(@NonNull Collection others) { - final List conditions = new ArrayList<>(others); + final List conditions = new ArrayList<>(); conditions.add(this); - return new CombinedCondition(conditions, Evaluator.OP_AND); + conditions.addAll(others); + return new CombinedCondition(mScope, 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(@NonNull Condition other) { - return new CombinedCondition(Arrays.asList(this, other), Evaluator.OP_AND); + public Condition and(@NonNull Condition... others) { + return and(Arrays.asList(others)); } /** @@ -228,17 +261,18 @@ public abstract class Condition { * provided conditions are true. */ public Condition or(@NonNull Collection others) { - final List conditions = new ArrayList<>(others); + final List conditions = new ArrayList<>(); conditions.add(this); - return new CombinedCondition(conditions, Evaluator.OP_OR); + conditions.addAll(others); + return new CombinedCondition(mScope, 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(@NonNull Condition other) { - return new CombinedCondition(Arrays.asList(this, other), Evaluator.OP_OR); + public Condition or(@NonNull Condition... others) { + return or(Arrays.asList(others)); } /** diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/condition/ConditionExtensions.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/condition/ConditionExtensions.kt index 8f8bff86f64da..84edc35770073 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/condition/ConditionExtensions.kt +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/condition/ConditionExtensions.kt @@ -1,14 +1,22 @@ package com.android.systemui.shared.condition +import com.android.systemui.shared.condition.Condition.StartStrategy import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch /** Converts a boolean flow to a [Condition] object which can be used with a [Monitor] */ @JvmOverloads -fun Flow.toCondition(scope: CoroutineScope, initialValue: Boolean? = null): Condition { - return object : Condition(initialValue, false) { +fun Flow.toCondition( + scope: CoroutineScope, + @StartStrategy strategy: Int, + initialValue: Boolean? = null +): Condition { + return object : Condition(scope, initialValue, false) { var job: Job? = null override fun start() { @@ -19,5 +27,25 @@ fun Flow.toCondition(scope: CoroutineScope, initialValue: Boolean? = nu job?.cancel() job = null } + + override fun getStartStrategy() = strategy } } + +/** Converts a [Condition] to a boolean flow */ +fun Condition.toFlow(): Flow { + return callbackFlow { + val callback = + Condition.Callback { condition -> + if (condition.isConditionSet) { + trySend(condition.isConditionMet) + } else { + trySend(null) + } + } + addCallback(callback) + callback.onConditionChanged(this@toFlow) + awaitClose { removeCallback(callback) } + } + .distinctUntilChanged() +} diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/condition/Evaluator.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/condition/Evaluator.kt index 454294f36d2a1..584d9785c3003 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/condition/Evaluator.kt +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/condition/Evaluator.kt @@ -22,7 +22,7 @@ import android.annotation.IntDef * Helper for evaluating a collection of [Condition] objects with a given * [Evaluator.ConditionOperand] */ -internal object Evaluator { +object Evaluator { /** Operands for combining multiple conditions together */ @Retention(AnnotationRetention.SOURCE) @IntDef(value = [OP_AND, OP_OR]) @@ -70,15 +70,31 @@ internal object Evaluator { fun evaluate(conditions: Collection, @ConditionOperand operand: Int): Boolean? { if (conditions.isEmpty()) return null // If there are overriding conditions with values set, they take precedence. - val targetConditions = + val values: Collection = conditions .filter { it.isConditionSet && it.isOverridingCondition } .ifEmpty { conditions } + .map { condition -> + if (condition.isConditionSet) { + condition.isConditionMet + } else { + null + } + } + return evaluate(values = values, operand = operand) + } + + /** + * Evaluates a set of booleans with a given operand + * + * @param operand The operand to use when evaluating. + * @return Either true or false if the value is known, or null if value is unknown + */ + internal fun evaluate(values: Collection, @ConditionOperand operand: Int): Boolean? { + if (values.isEmpty()) return null return when (operand) { - OP_AND -> - threeValuedAndOrOr(conditions = targetConditions, returnValueIfAnyMatches = false) - OP_OR -> - threeValuedAndOrOr(conditions = targetConditions, returnValueIfAnyMatches = true) + OP_AND -> threeValuedAndOrOr(values = values, returnValueIfAnyMatches = false) + OP_OR -> threeValuedAndOrOr(values = values, returnValueIfAnyMatches = true) else -> null } } @@ -90,16 +106,16 @@ internal object Evaluator { * any value is true. */ private fun threeValuedAndOrOr( - conditions: Collection, + values: Collection, returnValueIfAnyMatches: Boolean ): Boolean? { var hasUnknown = false - for (condition in conditions) { - if (!condition.isConditionSet) { + for (value in values) { + if (value == null) { hasUnknown = true continue } - if (condition.isConditionMet == returnValueIfAnyMatches) { + if (value == returnValueIfAnyMatches) { return returnValueIfAnyMatches } } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/conditions/AssistantAttentionCondition.java b/packages/SystemUI/src/com/android/systemui/dreams/conditions/AssistantAttentionCondition.java index 250cfeca5aa4c..c889ac214cdac 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/conditions/AssistantAttentionCondition.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/conditions/AssistantAttentionCondition.java @@ -18,11 +18,14 @@ package com.android.systemui.dreams.conditions; import com.android.internal.app.AssistUtils; import com.android.internal.app.IVisualQueryDetectionAttentionListener; +import com.android.systemui.dagger.qualifiers.Application; import com.android.systemui.dreams.DreamOverlayStateController; import com.android.systemui.shared.condition.Condition; import javax.inject.Inject; +import kotlinx.coroutines.CoroutineScope; + /** * {@link AssistantAttentionCondition} provides a signal when assistant has the user's attention. */ @@ -58,8 +61,10 @@ public class AssistantAttentionCondition extends Condition { @Inject public AssistantAttentionCondition( + @Application CoroutineScope scope, DreamOverlayStateController dreamOverlayStateController, AssistUtils assistUtils) { + super(scope); mDreamOverlayStateController = dreamOverlayStateController; mAssistUtils = assistUtils; } @@ -75,6 +80,11 @@ public class AssistantAttentionCondition extends Condition { mDreamOverlayStateController.removeCallback(mCallback); } + @Override + protected int getStartStrategy() { + return START_EAGERLY; + } + private void enableVisualQueryDetection() { if (mEnabled) { return; diff --git a/packages/SystemUI/src/com/android/systemui/dreams/conditions/DreamCondition.java b/packages/SystemUI/src/com/android/systemui/dreams/conditions/DreamCondition.java index 3ef19b760826a..99688bee68e02 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/conditions/DreamCondition.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/conditions/DreamCondition.java @@ -19,19 +19,20 @@ import android.app.DreamManager; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.KeyguardUpdateMonitorCallback; +import com.android.systemui.dagger.qualifiers.Application; import com.android.systemui.shared.condition.Condition; import javax.inject.Inject; +import kotlinx.coroutines.CoroutineScope; + /** * {@link DreamCondition} provides a signal when a dream begins and ends. */ public class DreamCondition extends Condition { private final DreamManager mDreamManager; - private final KeyguardUpdateMonitor mUpdateMonitor; - private final KeyguardUpdateMonitorCallback mUpdateCallback = new KeyguardUpdateMonitorCallback() { @Override @@ -41,7 +42,11 @@ public class DreamCondition extends Condition { }; @Inject - public DreamCondition(DreamManager dreamManager, KeyguardUpdateMonitor monitor) { + public DreamCondition( + @Application CoroutineScope scope, + DreamManager dreamManager, + KeyguardUpdateMonitor monitor) { + super(scope); mDreamManager = dreamManager; mUpdateMonitor = monitor; } @@ -56,4 +61,9 @@ public class DreamCondition extends Condition { protected void stop() { mUpdateMonitor.removeCallback(mUpdateCallback); } + + @Override + protected int getStartStrategy() { + return START_EAGERLY; + } } diff --git a/packages/SystemUI/src/com/android/systemui/process/condition/SystemProcessCondition.java b/packages/SystemUI/src/com/android/systemui/process/condition/SystemProcessCondition.java index 80fbf91150652..b6a5ad6f21553 100644 --- a/packages/SystemUI/src/com/android/systemui/process/condition/SystemProcessCondition.java +++ b/packages/SystemUI/src/com/android/systemui/process/condition/SystemProcessCondition.java @@ -16,11 +16,14 @@ package com.android.systemui.process.condition; +import com.android.systemui.dagger.qualifiers.Application; import com.android.systemui.process.ProcessWrapper; import com.android.systemui.shared.condition.Condition; import javax.inject.Inject; +import kotlinx.coroutines.CoroutineScope; + /** * {@link SystemProcessCondition} checks to make sure the current process is being ran by the * System User. @@ -29,8 +32,9 @@ public class SystemProcessCondition extends Condition { private final ProcessWrapper mProcessWrapper; @Inject - public SystemProcessCondition(ProcessWrapper processWrapper) { - super(); + public SystemProcessCondition(@Application CoroutineScope scope, + ProcessWrapper processWrapper) { + super(scope); mProcessWrapper = processWrapper; } @@ -42,4 +46,9 @@ public class SystemProcessCondition extends Condition { @Override protected void stop() { } + + @Override + protected int getStartStrategy() { + return START_EAGERLY; + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/conditions/AssistantAttentionConditionTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/conditions/AssistantAttentionConditionTest.java index ef1061faeed9d..07cb5d88a515d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/conditions/AssistantAttentionConditionTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/conditions/AssistantAttentionConditionTest.java @@ -42,6 +42,8 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import kotlinx.coroutines.CoroutineScope; + @SmallTest @RunWith(AndroidTestingRunner.class) public class AssistantAttentionConditionTest extends SysuiTestCase { @@ -51,6 +53,8 @@ public class AssistantAttentionConditionTest extends SysuiTestCase { AssistUtils mAssistUtils; @Mock DreamOverlayStateController mDreamOverlayStateController; + @Mock + CoroutineScope mScope; private AssistantAttentionCondition mAssistantAttentionCondition; @@ -59,7 +63,7 @@ public class AssistantAttentionConditionTest extends SysuiTestCase { MockitoAnnotations.initMocks(this); mAssistantAttentionCondition = - new AssistantAttentionCondition(mDreamOverlayStateController, mAssistUtils); + new AssistantAttentionCondition(mScope, mDreamOverlayStateController, mAssistUtils); // Adding a callback also starts the condition. mAssistantAttentionCondition.addCallback(mCallback); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/conditions/DreamConditionTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/conditions/DreamConditionTest.java index e1c54976d7343..68c79652ac001 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/conditions/DreamConditionTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/conditions/DreamConditionTest.java @@ -25,7 +25,6 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.DreamManager; -import android.content.Context; import android.testing.AndroidTestingRunner; import androidx.test.filters.SmallTest; @@ -42,12 +41,11 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import kotlinx.coroutines.CoroutineScope; + @SmallTest @RunWith(AndroidTestingRunner.class) public class DreamConditionTest extends SysuiTestCase { - @Mock - Context mContext; - @Mock Condition.Callback mCallback; @@ -57,6 +55,9 @@ public class DreamConditionTest extends SysuiTestCase { @Mock KeyguardUpdateMonitor mKeyguardUpdateMonitor; + @Mock + CoroutineScope mScope; + @Before public void setup() { MockitoAnnotations.initMocks(this); @@ -68,7 +69,8 @@ public class DreamConditionTest extends SysuiTestCase { @Test public void testInitialDreamingState() { when(mDreamManager.isDreaming()).thenReturn(true); - final DreamCondition condition = new DreamCondition(mDreamManager, mKeyguardUpdateMonitor); + final DreamCondition condition = new DreamCondition(mScope, mDreamManager, + mKeyguardUpdateMonitor); condition.addCallback(mCallback); verify(mCallback).onConditionChanged(eq(condition)); @@ -81,7 +83,8 @@ public class DreamConditionTest extends SysuiTestCase { @Test public void testInitialNonDreamingState() { when(mDreamManager.isDreaming()).thenReturn(false); - final DreamCondition condition = new DreamCondition(mDreamManager, mKeyguardUpdateMonitor); + final DreamCondition condition = new DreamCondition(mScope, mDreamManager, + mKeyguardUpdateMonitor); condition.addCallback(mCallback); verify(mCallback, never()).onConditionChanged(eq(condition)); @@ -96,7 +99,8 @@ public class DreamConditionTest extends SysuiTestCase { final ArgumentCaptor callbackCaptor = ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback.class); when(mDreamManager.isDreaming()).thenReturn(true); - final DreamCondition condition = new DreamCondition(mDreamManager, mKeyguardUpdateMonitor); + final DreamCondition condition = new DreamCondition(mScope, mDreamManager, + mKeyguardUpdateMonitor); condition.addCallback(mCallback); verify(mKeyguardUpdateMonitor).registerCallback(callbackCaptor.capture()); diff --git a/packages/SystemUI/tests/src/com/android/systemui/process/condition/SystemProcessConditionTest.java b/packages/SystemUI/tests/src/com/android/systemui/process/condition/SystemProcessConditionTest.java index fb7197706ddca..ff60fcda53e07 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/process/condition/SystemProcessConditionTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/process/condition/SystemProcessConditionTest.java @@ -37,6 +37,8 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import kotlinx.coroutines.CoroutineScope; + @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper @SmallTest @@ -47,6 +49,9 @@ public class SystemProcessConditionTest extends SysuiTestCase { @Mock Monitor.Callback mCallback; + @Mock + CoroutineScope mScope; + private final FakeExecutor mExecutor = new FakeExecutor(new FakeSystemClock()); @Before @@ -61,7 +66,7 @@ public class SystemProcessConditionTest extends SysuiTestCase { @Test public void testConditionFailsWithNonSystemProcess() { - final Condition condition = new SystemProcessCondition(mProcessWrapper); + final Condition condition = new SystemProcessCondition(mScope, mProcessWrapper); when(mProcessWrapper.isSystemUser()).thenReturn(false); final Monitor monitor = new Monitor(mExecutor); @@ -82,7 +87,7 @@ public class SystemProcessConditionTest extends SysuiTestCase { @Test public void testConditionSucceedsWithSystemProcess() { - final Condition condition = new SystemProcessCondition(mProcessWrapper); + final Condition condition = new SystemProcessCondition(mScope, mProcessWrapper); when(mProcessWrapper.isSystemUser()).thenReturn(true); final Monitor monitor = new Monitor(mExecutor); diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/condition/CombinedConditionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/condition/CombinedConditionTest.kt new file mode 100644 index 0000000000000..1a0e932d7e8a5 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/condition/CombinedConditionTest.kt @@ -0,0 +1,450 @@ +/* + * Copyright (C) 2023 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.shared.condition + +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.shared.condition.Condition.START_EAGERLY +import com.android.systemui.shared.condition.Condition.START_LAZILY +import com.android.systemui.shared.condition.Condition.START_WHEN_NEEDED +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel +import kotlinx.coroutines.runBlocking +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class CombinedConditionTest : SysuiTestCase() { + + class FakeCondition + constructor( + scope: CoroutineScope, + initialValue: Boolean?, + overriding: Boolean = false, + @StartStrategy private val startStrategy: Int = START_WHEN_NEEDED, + ) : Condition(scope, initialValue, overriding) { + private var _started = false + val started: Boolean + get() = _started + + override fun start() { + _started = true + } + + override fun stop() { + _started = false + } + + override fun getStartStrategy(): Int { + return startStrategy + } + + fun setValue(value: Boolean?) { + value?.also { updateCondition(value) } ?: clearCondition() + } + } + + @Test + fun testOrOperatorWithMixedConditions() = runSelfCancelingTest { + val startWhenNeededCondition = + FakeCondition(scope = this, initialValue = false, startStrategy = START_WHEN_NEEDED) + val eagerCondition = + FakeCondition(scope = this, initialValue = false, startStrategy = START_EAGERLY) + val lazyCondition = + FakeCondition(scope = this, initialValue = false, startStrategy = START_LAZILY) + + val combinedCondition = + CombinedCondition( + scope = this, + conditions = + listOf( + eagerCondition, + lazyCondition, + startWhenNeededCondition, + ), + operand = Evaluator.OP_OR + ) + + val callback = Condition.Callback {} + combinedCondition.addCallback(callback) + + assertThat(combinedCondition.isConditionMet).isFalse() + assertThat(eagerCondition.started).isTrue() + assertThat(lazyCondition.started).isTrue() + assertThat(startWhenNeededCondition.started).isTrue() + + eagerCondition.setValue(true) + assertThat(combinedCondition.isConditionMet).isTrue() + assertThat(eagerCondition.started).isTrue() + assertThat(lazyCondition.started).isTrue() + assertThat(startWhenNeededCondition.started).isFalse() + + startWhenNeededCondition.setValue(true) + assertThat(combinedCondition.isConditionMet).isTrue() + assertThat(eagerCondition.started).isTrue() + assertThat(lazyCondition.started).isTrue() + assertThat(startWhenNeededCondition.started).isFalse() + + startWhenNeededCondition.setValue(false) + eagerCondition.setValue(false) + assertThat(combinedCondition.isConditionMet).isFalse() + assertThat(eagerCondition.started).isTrue() + assertThat(lazyCondition.started).isTrue() + assertThat(startWhenNeededCondition.started).isTrue() + } + + @Test + fun testAndOperatorWithMixedConditions() = runSelfCancelingTest { + val startWhenNeededCondition = + FakeCondition(scope = this, initialValue = false, startStrategy = START_WHEN_NEEDED) + val eagerCondition = + FakeCondition(scope = this, initialValue = false, startStrategy = START_EAGERLY) + val lazyCondition = + FakeCondition(scope = this, initialValue = false, startStrategy = START_LAZILY) + + val combinedCondition = + CombinedCondition( + scope = this, + conditions = + listOf( + startWhenNeededCondition, + lazyCondition, + eagerCondition, + ), + operand = Evaluator.OP_AND + ) + + val callback = Condition.Callback {} + combinedCondition.addCallback(callback) + + assertThat(combinedCondition.isConditionMet).isFalse() + assertThat(eagerCondition.started).isTrue() + assertThat(lazyCondition.started).isFalse() + assertThat(startWhenNeededCondition.started).isFalse() + + eagerCondition.setValue(true) + assertThat(combinedCondition.isConditionMet).isFalse() + assertThat(eagerCondition.started).isTrue() + assertThat(startWhenNeededCondition.started).isTrue() + assertThat(lazyCondition.started).isFalse() + + startWhenNeededCondition.setValue(true) + assertThat(combinedCondition.isConditionMet).isFalse() + assertThat(eagerCondition.started).isTrue() + assertThat(startWhenNeededCondition.started).isFalse() + assertThat(lazyCondition.started).isTrue() + + startWhenNeededCondition.setValue(false) + assertThat(combinedCondition.isConditionMet).isFalse() + assertThat(eagerCondition.started).isTrue() + assertThat(startWhenNeededCondition.started).isFalse() + assertThat(lazyCondition.started).isTrue() + + startWhenNeededCondition.setValue(true) + lazyCondition.setValue(true) + assertThat(combinedCondition.isConditionMet).isTrue() + assertThat(eagerCondition.started).isTrue() + assertThat(startWhenNeededCondition.started).isTrue() + assertThat(lazyCondition.started).isTrue() + } + + @Test + fun testAndOperatorWithStartWhenNeededConditions() = runSelfCancelingTest { + val conditions = + 0.rangeTo(2) + .map { + FakeCondition( + scope = this, + initialValue = false, + startStrategy = START_WHEN_NEEDED + ) + } + .toList() + + val combinedCondition = + CombinedCondition(scope = this, conditions = conditions, operand = Evaluator.OP_AND) + + val callback = Condition.Callback {} + combinedCondition.addCallback(callback) + assertThat(combinedCondition.isConditionMet).isFalse() + // Only the first condition should be started + assertThat(areStarted(conditions)).containsExactly(true, false, false).inOrder() + + conditions[0].setValue(true) + assertThat(combinedCondition.isConditionMet).isFalse() + assertThat(areStarted(conditions)).containsExactly(false, true, false).inOrder() + + conditions[1].setValue(true) + assertThat(combinedCondition.isConditionMet).isFalse() + assertThat(areStarted(conditions)).containsExactly(false, false, true).inOrder() + + conditions[2].setValue(true) + assertThat(combinedCondition.isConditionMet).isTrue() + assertThat(areStarted(conditions)).containsExactly(true, true, true).inOrder() + + conditions[0].setValue(false) + assertThat(combinedCondition.isConditionMet).isFalse() + assertThat(areStarted(conditions)).containsExactly(true, false, false).inOrder() + } + + @Test + fun testOrOperatorWithStartWhenNeededConditions() = runSelfCancelingTest { + val conditions = + 0.rangeTo(2) + .map { + FakeCondition( + scope = this, + initialValue = false, + startStrategy = START_WHEN_NEEDED + ) + } + .toList() + + val combinedCondition = + CombinedCondition(scope = this, conditions = conditions, operand = Evaluator.OP_OR) + + val callback = Condition.Callback {} + combinedCondition.addCallback(callback) + // Default is to monitor all conditions when false + assertThat(combinedCondition.isConditionMet).isFalse() + assertThat(areStarted(conditions)).containsExactly(true, true, true).inOrder() + + // Condition 2 is true, so we should only monitor condition 2 + conditions[1].setValue(true) + assertThat(combinedCondition.isConditionMet).isTrue() + assertThat(areStarted(conditions)).containsExactly(false, true, false).inOrder() + + // Condition 2 becomes false, so we go back to monitoring all conditions + conditions[1].setValue(false) + assertThat(combinedCondition.isConditionMet).isFalse() + assertThat(areStarted(conditions)).containsExactly(true, true, true).inOrder() + + // Condition 3 becomes true, so we only monitor condition 3 + conditions[2].setValue(true) + assertThat(combinedCondition.isConditionMet).isTrue() + assertThat(areStarted(conditions)).containsExactly(false, false, true).inOrder() + + // Condition 2 becomes true, but we are still only monitoring condition 3 + conditions[1].setValue(true) + assertThat(combinedCondition.isConditionMet).isTrue() + assertThat(areStarted(conditions)).containsExactly(false, false, true).inOrder() + + // Condition 3 becomes false, so we should now only be monitoring condition 2 + conditions[2].setValue(false) + assertThat(combinedCondition.isConditionMet).isTrue() + assertThat(areStarted(conditions)).containsExactly(false, true, false).inOrder() + } + + @Test + fun testRemovingCallbackWillStopMonitoringAllConditions() = runSelfCancelingTest { + val conditions = + 0.rangeTo(2) + .map { + FakeCondition( + scope = this, + initialValue = false, + startStrategy = START_WHEN_NEEDED + ) + } + .toList() + + val combinedCondition = + CombinedCondition(scope = this, conditions = conditions, operand = Evaluator.OP_OR) + + val callback = Condition.Callback {} + combinedCondition.addCallback(callback) + assertThat(areStarted(conditions)).containsExactly(true, true, true) + + combinedCondition.removeCallback(callback) + assertThat(areStarted(conditions)).containsExactly(false, false, false) + } + + @Test + fun testOverridingConditionSkipsOtherConditions() = runSelfCancelingTest { + val startWhenNeededCondition = + FakeCondition(scope = this, initialValue = false, startStrategy = START_WHEN_NEEDED) + val eagerCondition = + FakeCondition(scope = this, initialValue = false, startStrategy = START_EAGERLY) + val lazyCondition = + FakeCondition(scope = this, initialValue = false, startStrategy = START_LAZILY) + val overridingCondition1 = + FakeCondition(scope = this, initialValue = null, overriding = true) + val overridingCondition2 = + FakeCondition(scope = this, initialValue = null, overriding = true) + + val combinedCondition = + CombinedCondition( + scope = this, + conditions = + listOf( + eagerCondition, + overridingCondition1, + lazyCondition, + startWhenNeededCondition, + overridingCondition2 + ), + operand = Evaluator.OP_OR + ) + + val callback = Condition.Callback {} + combinedCondition.addCallback(callback) + assertThat(combinedCondition.isConditionMet).isFalse() + assertThat(eagerCondition.started).isTrue() + assertThat(lazyCondition.started).isTrue() + assertThat(startWhenNeededCondition.started).isTrue() + assertThat(overridingCondition1.started).isTrue() + + // Overriding condition is true, so we should stop monitoring all other conditions + overridingCondition1.setValue(true) + assertThat(combinedCondition.isConditionMet).isTrue() + assertThat(eagerCondition.started).isFalse() + assertThat(lazyCondition.started).isFalse() + assertThat(startWhenNeededCondition.started).isFalse() + assertThat(overridingCondition1.started).isTrue() + assertThat(overridingCondition2.started).isFalse() + + // Overriding condition is false, so we should only monitor other overriding conditions + overridingCondition1.setValue(false) + eagerCondition.setValue(true) + assertThat(combinedCondition.isConditionMet).isFalse() + assertThat(eagerCondition.started).isFalse() + assertThat(lazyCondition.started).isFalse() + assertThat(startWhenNeededCondition.started).isFalse() + assertThat(overridingCondition1.started).isTrue() + assertThat(overridingCondition2.started).isTrue() + + // Second overriding condition is true, condition 1 is still true + overridingCondition2.setValue(true) + assertThat(combinedCondition.isConditionMet).isTrue() + assertThat(eagerCondition.started).isFalse() + assertThat(lazyCondition.started).isFalse() + assertThat(startWhenNeededCondition.started).isFalse() + assertThat(overridingCondition1.started).isFalse() + assertThat(overridingCondition2.started).isTrue() + + // Overriding condition is cleared, condition 1 is still true + overridingCondition1.setValue(null) + overridingCondition2.setValue(null) + assertThat(combinedCondition.isConditionMet).isTrue() + assertThat(eagerCondition.started).isTrue() + assertThat(lazyCondition.started).isFalse() + assertThat(startWhenNeededCondition.started).isFalse() + assertThat(overridingCondition1.started).isTrue() + assertThat(overridingCondition2.started).isTrue() + } + + @Test + fun testAndOperatorCorrectlyHandlesUnknownValues() = runSelfCancelingTest { + val conditions = + 0.rangeTo(2) + .map { + FakeCondition(scope = this, initialValue = false, startStrategy = START_EAGERLY) + } + .toList() + + val combinedCondition = + CombinedCondition(scope = this, conditions = conditions, operand = Evaluator.OP_AND) + + val callback = Condition.Callback {} + combinedCondition.addCallback(callback) + + conditions[0].setValue(null) + conditions[1].setValue(true) + conditions[2].setValue(false) + assertThat(combinedCondition.isConditionMet).isFalse() + assertThat(combinedCondition.isConditionSet).isTrue() + + // The condition should not be set since the value is unknown + conditions[2].setValue(true) + assertThat(combinedCondition.isConditionMet).isFalse() + assertThat(combinedCondition.isConditionSet).isFalse() + + conditions[0].setValue(true) + assertThat(combinedCondition.isConditionMet).isTrue() + assertThat(combinedCondition.isConditionSet).isTrue() + } + + @Test + fun testOrOperatorCorrectlyHandlesUnknownValues() = runSelfCancelingTest { + val conditions = + 0.rangeTo(2) + .map { + FakeCondition(scope = this, initialValue = false, startStrategy = START_EAGERLY) + } + .toList() + + val combinedCondition = + CombinedCondition(scope = this, conditions = conditions, operand = Evaluator.OP_OR) + + val callback = Condition.Callback {} + combinedCondition.addCallback(callback) + + conditions[0].setValue(null) + conditions[1].setValue(true) + conditions[2].setValue(false) + assertThat(combinedCondition.isConditionMet).isTrue() + assertThat(combinedCondition.isConditionSet).isTrue() + + conditions[1].setValue(false) + assertThat(combinedCondition.isConditionMet).isFalse() + // The condition should not be set since the value is unknown + assertThat(combinedCondition.isConditionSet).isFalse() + } + + @Test + fun testEmptyConditions() = runSelfCancelingTest { + for (operand in intArrayOf(Evaluator.OP_OR, Evaluator.OP_AND)) { + val combinedCondition = + CombinedCondition( + scope = this, + conditions = emptyList(), + operand = operand, + ) + + val callback = Condition.Callback {} + combinedCondition.addCallback(callback) + assertThat(combinedCondition.isConditionMet).isFalse() + assertThat(combinedCondition.isConditionSet).isFalse() + } + } + + private fun areStarted(conditions: List): List { + return conditions.map { it.started } + } + + /** + * Executes the given block of execution within the scope of a dedicated [CoroutineScope] which + * is then automatically canceled and cleaned-up. + */ + private fun runSelfCancelingTest( + block: suspend CoroutineScope.() -> Unit, + ) = + runBlocking(IMMEDIATE) { + val scope = CoroutineScope(coroutineContext + Job()) + block(scope) + scope.cancel() + } + + companion object { + private val IMMEDIATE = Dispatchers.Main.immediate + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/condition/ConditionExtensionsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/condition/ConditionExtensionsTest.kt index 2b4a7fb4803b5..0a8210d6b319a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shared/condition/ConditionExtensionsTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/condition/ConditionExtensionsTest.kt @@ -32,7 +32,7 @@ class ConditionExtensionsTest : SysuiTestCase() { fun flowInitiallyTrue() = testScope.runTest { val flow = flowOf(true) - val condition = flow.toCondition(this) + val condition = flow.toCondition(scope = this, Condition.START_EAGERLY) runCurrent() assertThat(condition.isConditionSet).isFalse() @@ -47,7 +47,7 @@ class ConditionExtensionsTest : SysuiTestCase() { fun flowInitiallyFalse() = testScope.runTest { val flow = flowOf(false) - val condition = flow.toCondition(this) + val condition = flow.toCondition(scope = this, Condition.START_EAGERLY) runCurrent() assertThat(condition.isConditionSet).isFalse() @@ -62,7 +62,7 @@ class ConditionExtensionsTest : SysuiTestCase() { fun emptyFlowWithNoInitialValue() = testScope.runTest { val flow = emptyFlow() - val condition = flow.toCondition(this) + val condition = flow.toCondition(scope = this, Condition.START_EAGERLY) condition.start() runCurrent() @@ -74,7 +74,12 @@ class ConditionExtensionsTest : SysuiTestCase() { fun emptyFlowWithInitialValueOfTrue() = testScope.runTest { val flow = emptyFlow() - val condition = flow.toCondition(scope = this, initialValue = true) + val condition = + flow.toCondition( + scope = this, + strategy = Condition.START_EAGERLY, + initialValue = true + ) condition.start() runCurrent() @@ -86,7 +91,12 @@ class ConditionExtensionsTest : SysuiTestCase() { fun emptyFlowWithInitialValueOfFalse() = testScope.runTest { val flow = emptyFlow() - val condition = flow.toCondition(scope = this, initialValue = false) + val condition = + flow.toCondition( + scope = this, + strategy = Condition.START_EAGERLY, + initialValue = false + ) condition.start() runCurrent() @@ -98,7 +108,7 @@ class ConditionExtensionsTest : SysuiTestCase() { fun conditionUpdatesWhenFlowEmitsNewValue() = testScope.runTest { val flow = MutableStateFlow(false) - val condition = flow.toCondition(this) + val condition = flow.toCondition(scope = this, strategy = Condition.START_EAGERLY) condition.start() runCurrent() @@ -120,7 +130,7 @@ class ConditionExtensionsTest : SysuiTestCase() { fun stoppingConditionUnsubscribesFromFlow() = testScope.runTest { val flow = MutableSharedFlow() - val condition = flow.toCondition(this) + val condition = flow.toCondition(scope = this, strategy = Condition.START_EAGERLY) runCurrent() assertThat(flow.subscriptionCount.value).isEqualTo(0) diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/condition/ConditionMonitorTest.java b/packages/SystemUI/tests/src/com/android/systemui/shared/condition/ConditionMonitorTest.java index aa1636d8a0309..de5824d1f463f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shared/condition/ConditionMonitorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/condition/ConditionMonitorTest.java @@ -39,12 +39,15 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import java.util.Arrays; import java.util.HashSet; +import kotlinx.coroutines.CoroutineScope; + @SmallTest @RunWith(AndroidTestingRunner.class) public class ConditionMonitorTest extends SysuiTestCase { @@ -54,15 +57,18 @@ public class ConditionMonitorTest extends SysuiTestCase { private HashSet mConditions; private FakeExecutor mExecutor = new FakeExecutor(new FakeSystemClock()); + @Mock + private CoroutineScope mScope; + private Monitor mConditionMonitor; @Before public void setup() { MockitoAnnotations.initMocks(this); - mCondition1 = spy(new FakeCondition()); - mCondition2 = spy(new FakeCondition()); - mCondition3 = spy(new FakeCondition()); + mCondition1 = spy(new FakeCondition(mScope)); + mCondition2 = spy(new FakeCondition(mScope)); + mCondition3 = spy(new FakeCondition(mScope)); mConditions = new HashSet<>(Arrays.asList(mCondition1, mCondition2, mCondition3)); mConditionMonitor = new Monitor(mExecutor); @@ -396,7 +402,7 @@ public class ConditionMonitorTest extends SysuiTestCase { @Test public void unsetCondition_shouldNotAffectValue() { - final FakeCondition settableCondition = new FakeCondition(null, false); + final FakeCondition settableCondition = new FakeCondition(mScope, null, false); mCondition1.fakeUpdateCondition(true); mCondition2.fakeUpdateCondition(true); mCondition3.fakeUpdateCondition(true); @@ -414,7 +420,7 @@ public class ConditionMonitorTest extends SysuiTestCase { @Test public void setUnsetCondition_shouldAffectValue() { - final FakeCondition settableCondition = new FakeCondition(null, false); + final FakeCondition settableCondition = new FakeCondition(mScope, null, false); mCondition1.fakeUpdateCondition(true); mCondition2.fakeUpdateCondition(true); mCondition3.fakeUpdateCondition(true); @@ -443,7 +449,7 @@ public class ConditionMonitorTest extends SysuiTestCase { @Test public void clearingOverridingCondition_shouldBeExcluded() { - final FakeCondition overridingCondition = new FakeCondition(true, true); + final FakeCondition overridingCondition = new FakeCondition(mScope, true, true); mCondition1.fakeUpdateCondition(false); mCondition2.fakeUpdateCondition(false); mCondition3.fakeUpdateCondition(false); @@ -466,7 +472,7 @@ public class ConditionMonitorTest extends SysuiTestCase { @Test public void settingUnsetOverridingCondition_shouldBeIncluded() { - final FakeCondition overridingCondition = new FakeCondition(null, true); + final FakeCondition overridingCondition = new FakeCondition(mScope, null, true); mCondition1.fakeUpdateCondition(false); mCondition2.fakeUpdateCondition(false); mCondition3.fakeUpdateCondition(false); diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/condition/ConditionTest.java b/packages/SystemUI/tests/src/com/android/systemui/shared/condition/ConditionTest.java index 8443221e8b7a6..6efade9c93610 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shared/condition/ConditionTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/condition/ConditionTest.java @@ -34,15 +34,23 @@ import com.android.systemui.SysuiTestCase; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import kotlinx.coroutines.CoroutineScope; @SmallTest @RunWith(AndroidTestingRunner.class) public class ConditionTest extends SysuiTestCase { + @Mock + CoroutineScope mScope; + private FakeCondition mCondition; @Before public void setup() { - mCondition = spy(new FakeCondition()); + MockitoAnnotations.initMocks(this); + mCondition = spy(new FakeCondition(mScope)); } @Test @@ -152,168 +160,4 @@ public class ConditionTest extends SysuiTestCase { 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/src/com/android/systemui/shared/condition/FakeCondition.java b/packages/SystemUI/tests/src/com/android/systemui/shared/condition/FakeCondition.java index 55a6d39d46441..a325cbf25ffe5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shared/condition/FakeCondition.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/condition/FakeCondition.java @@ -16,21 +16,19 @@ package com.android.systemui.shared.condition; +import kotlinx.coroutines.CoroutineScope; + /** * Fake implementation of {@link Condition}, and provides a way for tests to update * condition fulfillment. */ public class FakeCondition extends Condition { - FakeCondition() { - super(); + FakeCondition(CoroutineScope scope) { + super(scope); } - FakeCondition(Boolean initialValue) { - super(initialValue, false); - } - - FakeCondition(Boolean initialValue, boolean overriding) { - super(initialValue, overriding); + FakeCondition(CoroutineScope scope, Boolean initialValue, boolean overriding) { + super(scope, initialValue, overriding); } @Override @@ -41,6 +39,11 @@ public class FakeCondition extends Condition { public void stop() { } + @Override + protected int getStartStrategy() { + return START_EAGERLY; + } + public void fakeUpdateCondition(boolean isConditionMet) { updateCondition(isConditionMet); }