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
This commit is contained in:
Lucas Silva
2023-04-19 11:03:48 -04:00
parent 3a2bda18c4
commit b0e2580407
15 changed files with 821 additions and 236 deletions

View File

@@ -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<Condition>,
@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<Condition>,
filterUnknown: Boolean,
): Flow<Boolean?> = callbackFlow {
val jobs = MutableList<Job?>(conditions.size) { null }
val values = MutableList<Boolean?>(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
}
}

View File

@@ -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<WeakReference<Callback>> 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<Condition> others) {
final List<Condition> conditions = new ArrayList<>(others);
final List<Condition> 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<Condition> others) {
final List<Condition> conditions = new ArrayList<>(others);
final List<Condition> 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));
}
/**

View File

@@ -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<Boolean>.toCondition(scope: CoroutineScope, initialValue: Boolean? = null): Condition {
return object : Condition(initialValue, false) {
fun Flow<Boolean>.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<Boolean>.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<Boolean?> {
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()
}

View File

@@ -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<Condition>, @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<Boolean?> =
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<Boolean?>, @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<Condition>,
values: Collection<Boolean?>,
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
}
}

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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<KeyguardUpdateMonitorCallback> 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());

View File

@@ -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);

View File

@@ -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<FakeCondition>): List<Boolean> {
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
}
}

View File

@@ -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<Boolean>()
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<Boolean>()
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<Boolean>()
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<Boolean>()
val condition = flow.toCondition(this)
val condition = flow.toCondition(scope = this, strategy = Condition.START_EAGERLY)
runCurrent()
assertThat(flow.subscriptionCount.value).isEqualTo(0)

View File

@@ -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<Condition> 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);

View File

@@ -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);
}
}

View File

@@ -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);
}