Loading packages/SystemUI/multivalentTests/src/com/android/systemui/shared/condition/ConditionExtensionsTest.kt +21 −9 Original line number Original line Diff line number Diff line Loading @@ -19,6 +19,10 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class) class ConditionExtensionsTest : SysuiTestCase() { class ConditionExtensionsTest : SysuiTestCase() { private lateinit var testScope: TestScope private lateinit var testScope: TestScope private val testCallback = Condition.Callback { // This is a no-op } @Before @Before fun setUp() { fun setUp() { Loading @@ -33,7 +37,7 @@ class ConditionExtensionsTest : SysuiTestCase() { assertThat(condition.isConditionSet).isFalse() assertThat(condition.isConditionSet).isFalse() condition.start() condition.testStart() assertThat(condition.isConditionSet).isTrue() assertThat(condition.isConditionSet).isTrue() assertThat(condition.isConditionMet).isTrue() assertThat(condition.isConditionMet).isTrue() } } Loading @@ -46,7 +50,7 @@ class ConditionExtensionsTest : SysuiTestCase() { assertThat(condition.isConditionSet).isFalse() assertThat(condition.isConditionSet).isFalse() condition.start() condition.testStart() assertThat(condition.isConditionSet).isTrue() assertThat(condition.isConditionSet).isTrue() assertThat(condition.isConditionMet).isFalse() assertThat(condition.isConditionMet).isFalse() } } Loading @@ -56,7 +60,7 @@ class ConditionExtensionsTest : SysuiTestCase() { testScope.runTest { testScope.runTest { val flow = emptyFlow<Boolean>() val flow = emptyFlow<Boolean>() val condition = flow.toCondition(scope = this, Condition.START_EAGERLY) val condition = flow.toCondition(scope = this, Condition.START_EAGERLY) condition.start() condition.testStop() assertThat(condition.isConditionSet).isFalse() assertThat(condition.isConditionSet).isFalse() assertThat(condition.isConditionMet).isFalse() assertThat(condition.isConditionMet).isFalse() Loading @@ -72,7 +76,7 @@ class ConditionExtensionsTest : SysuiTestCase() { strategy = Condition.START_EAGERLY, strategy = Condition.START_EAGERLY, initialValue = true, initialValue = true, ) ) condition.start() condition.testStart() assertThat(condition.isConditionSet).isTrue() assertThat(condition.isConditionSet).isTrue() assertThat(condition.isConditionMet).isTrue() assertThat(condition.isConditionMet).isTrue() Loading @@ -88,7 +92,7 @@ class ConditionExtensionsTest : SysuiTestCase() { strategy = Condition.START_EAGERLY, strategy = Condition.START_EAGERLY, initialValue = false, initialValue = false, ) ) condition.start() condition.testStart() assertThat(condition.isConditionSet).isTrue() assertThat(condition.isConditionSet).isTrue() assertThat(condition.isConditionMet).isFalse() assertThat(condition.isConditionMet).isFalse() Loading @@ -99,7 +103,7 @@ class ConditionExtensionsTest : SysuiTestCase() { testScope.runTest { testScope.runTest { val flow = MutableStateFlow(false) val flow = MutableStateFlow(false) val condition = flow.toCondition(scope = this, strategy = Condition.START_EAGERLY) val condition = flow.toCondition(scope = this, strategy = Condition.START_EAGERLY) condition.start() condition.testStart() assertThat(condition.isConditionSet).isTrue() assertThat(condition.isConditionSet).isTrue() assertThat(condition.isConditionMet).isFalse() assertThat(condition.isConditionMet).isFalse() Loading @@ -110,7 +114,7 @@ class ConditionExtensionsTest : SysuiTestCase() { flow.value = false flow.value = false assertThat(condition.isConditionMet).isFalse() assertThat(condition.isConditionMet).isFalse() condition.stop() condition.testStop() } } @Test @Test Loading @@ -120,10 +124,18 @@ class ConditionExtensionsTest : SysuiTestCase() { val condition = flow.toCondition(scope = this, strategy = Condition.START_EAGERLY) val condition = flow.toCondition(scope = this, strategy = Condition.START_EAGERLY) assertThat(flow.subscriptionCount.value).isEqualTo(0) assertThat(flow.subscriptionCount.value).isEqualTo(0) condition.start() condition.testStart() assertThat(flow.subscriptionCount.value).isEqualTo(1) assertThat(flow.subscriptionCount.value).isEqualTo(1) condition.stop() condition.testStop() assertThat(flow.subscriptionCount.value).isEqualTo(0) assertThat(flow.subscriptionCount.value).isEqualTo(0) } } fun Condition.testStart() { addCallback(testCallback) } fun Condition.testStop() { removeCallback(testCallback) } } } packages/SystemUI/multivalentTests/src/com/android/systemui/shared/condition/FakeCondition.java +1 −1 Original line number Original line Diff line number Diff line Loading @@ -40,7 +40,7 @@ public class FakeCondition extends Condition { } } @Override @Override protected int getStartStrategy() { public int getStartStrategy() { return START_EAGERLY; return START_EAGERLY; } } Loading packages/SystemUI/shared/src/com/android/systemui/shared/condition/CombinedCondition.kt +5 −6 Original line number Original line Diff line number Diff line Loading @@ -41,7 +41,7 @@ class CombinedCondition constructor( constructor( private val scope: CoroutineScope, private val scope: CoroutineScope, private val conditions: Collection<Condition>, private val conditions: Collection<Condition>, @Evaluator.ConditionOperand private val operand: Int @Evaluator.ConditionOperand private val operand: Int, ) : Condition(scope, null, false) { ) : Condition(scope, null, false) { private var job: Job? = null private var job: Job? = null Loading @@ -54,7 +54,7 @@ constructor( lazilyEvaluate( lazilyEvaluate( conditions = groupedConditions.getOrDefault(true, emptyList()), conditions = groupedConditions.getOrDefault(true, emptyList()), filterUnknown = true filterUnknown = true, ) ) .distinctUntilChanged() .distinctUntilChanged() .flatMapLatest { overriddenValue -> .flatMapLatest { overriddenValue -> Loading @@ -62,7 +62,7 @@ constructor( if (overriddenValue == null) { if (overriddenValue == null) { lazilyEvaluate( lazilyEvaluate( conditions = groupedConditions.getOrDefault(false, emptyList()), conditions = groupedConditions.getOrDefault(false, emptyList()), filterUnknown = false filterUnknown = false, ) ) } else { } else { flowOf(overriddenValue) flowOf(overriddenValue) Loading Loading @@ -188,7 +188,6 @@ constructor( return startStrategy return startStrategy } } override fun getStartStrategy(): Int { override val startStrategy: Int return _startStrategy get() = _startStrategy } } } packages/SystemUI/shared/src/com/android/systemui/shared/condition/Condition.javadeleted 100644 → 0 +0 −306 Original line number Original line Diff line number Diff line /* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.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 kotlinx.coroutines.CoroutineScope; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Iterator; import java.util.List; /** * Base class for a condition that needs to be fulfilled in order for {@link Monitor} to inform * its callbacks. */ public abstract class Condition { private final String mTag = getClass().getSimpleName(); 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(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. */ protected Condition(CoroutineScope scope, Boolean initialConditionMet, boolean overriding) { mIsConditionMet = initialConditionMet; mOverriding = overriding; mScope = scope; } /** * Starts monitoring the condition. */ protected abstract void start(); /** * Stops monitoring the 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 */ public boolean isOverridingCondition() { return mOverriding; } /** * Registers a callback to receive updates once started. This should be called before * {@link #start()}. Also triggers the callback immediately if already started. */ public void addCallback(@NonNull Callback callback) { if (shouldLog()) Log.d(mTag, "adding callback"); mCallbacks.add(new WeakReference<>(callback)); if (mStarted) { callback.onConditionChanged(this); return; } start(); mStarted = true; } /** * Removes the provided callback from further receiving updates. */ public void removeCallback(@NonNull Callback callback) { if (shouldLog()) Log.d(mTag, "removing callback"); final Iterator<WeakReference<Callback>> iterator = mCallbacks.iterator(); while (iterator.hasNext()) { final Callback cb = iterator.next().get(); if (cb == null || cb == callback) { iterator.remove(); } } if (!mCallbacks.isEmpty() || !mStarted) { return; } stop(); mStarted = false; } /** * Wrapper to {@link #addCallback(Callback)} when a lifecycle is in the resumed state * and {@link #removeCallback(Callback)} when not resumed automatically. */ public Callback observe(LifecycleOwner owner, Callback listener) { return observe(owner.getLifecycle(), listener); } /** * Wrapper to {@link #addCallback(Callback)} when a lifecycle is in the resumed state * and {@link #removeCallback(Condition.Callback)} when not resumed automatically. */ public Callback observe(Lifecycle lifecycle, Callback listener) { lifecycle.addObserver((LifecycleEventObserver) (lifecycleOwner, event) -> { if (event == Lifecycle.Event.ON_RESUME) { addCallback(listener); } else if (event == Lifecycle.Event.ON_PAUSE) { removeCallback(listener); } }); return listener; } /** * Updates the value for whether the condition has been fulfilled, and sends an update if the * value changes and any callback is registered. * * @param isConditionMet True if the condition has been fulfilled. False otherwise. */ protected void updateCondition(boolean isConditionMet) { if (mIsConditionMet != null && mIsConditionMet == isConditionMet) { return; } if (shouldLog()) Log.d(mTag, "updating condition to " + isConditionMet); mIsConditionMet = isConditionMet; sendUpdate(); } /** * Clears the set condition value. This is purposefully separate from * {@link #updateCondition(boolean)} to avoid confusion around {@code null} values. */ protected void clearCondition() { if (mIsConditionMet == null) { return; } if (shouldLog()) Log.d(mTag, "clearing condition"); mIsConditionMet = null; sendUpdate(); } private void sendUpdate() { final Iterator<WeakReference<Callback>> iterator = mCallbacks.iterator(); while (iterator.hasNext()) { final Callback cb = iterator.next().get(); if (cb == null) { iterator.remove(); } else { cb.onConditionChanged(this); } } } /** * 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() { return mIsConditionMet != null; } /** * Returns whether the condition has been met. Note that this method will return {@code false} * if the condition is not set as well. */ public boolean isConditionMet() { return Boolean.TRUE.equals(mIsConditionMet); } protected final boolean shouldLog() { return Log.isLoggable(mTag, Log.DEBUG); } protected final String getTag() { if (isOverridingCondition()) { return mTag + "[OVRD]"; } return mTag; } /** * Returns the state of the condition. * - "Invalid", condition hasn't been set / not monitored * - "True", condition has been met * - "False", condition has not been met */ protected final String getState() { if (!isConditionSet()) { return "Invalid"; } return isConditionMet() ? "True" : "False"; } /** * Creates a new condition which will only be true when both this condition and all the provided * conditions are true. */ public Condition and(@NonNull Collection<Condition> others) { final List<Condition> conditions = new ArrayList<>(); conditions.add(this); 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... others) { return and(Arrays.asList(others)); } /** * Creates a new condition which will only be true when either this condition or any of the * provided conditions are true. */ public Condition or(@NonNull Collection<Condition> others) { final List<Condition> conditions = new ArrayList<>(); conditions.add(this); 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... others) { return or(Arrays.asList(others)); } /** * Callback that receives updates about whether the condition has been fulfilled. */ public interface Callback { /** * Called when the fulfillment of the condition changes. * * @param condition The condition in question. */ void onConditionChanged(Condition condition); } } packages/SystemUI/shared/src/com/android/systemui/shared/condition/Condition.kt 0 → 100644 +270 −0 Original line number Original line Diff line number Diff line /* * Copyright (C) 2025 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.util.Log import androidx.annotation.IntDef import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleOwner import java.lang.ref.WeakReference import kotlinx.coroutines.CoroutineScope /** * Base class for a condition that needs to be fulfilled in order for [Monitor] to inform its * callbacks. */ abstract class Condition /** * Constructor for specifying initial state and overriding condition attribute. * * @param initialConditionMet Initial state of the condition. * @param overriding Whether this condition overrides others. */ @JvmOverloads protected constructor( private val _scope: CoroutineScope, private var _isConditionMet: Boolean? = false, /** Returns whether the current condition overrides */ val isOverridingCondition: Boolean = false, ) { private val mTag: String = javaClass.simpleName private val _callbacks = mutableListOf<WeakReference<Callback>>() private var _started = false /** Starts monitoring the condition. */ protected abstract fun start() /** Stops monitoring the condition. */ protected abstract fun stop() @Retention(AnnotationRetention.SOURCE) @IntDef(START_EAGERLY, START_LAZILY, START_WHEN_NEEDED) annotation class StartStrategy @get:StartStrategy abstract val startStrategy: Int /** * Registers a callback to receive updates once started. This should be called before [.start]. * Also triggers the callback immediately if already started. */ fun addCallback(callback: Callback) { if (shouldLog()) Log.d(mTag, "adding callback") _callbacks.add(WeakReference(callback)) if (_started) { callback.onConditionChanged(this) return } start() _started = true } /** Removes the provided callback from further receiving updates. */ fun removeCallback(callback: Callback) { if (shouldLog()) Log.d(mTag, "removing callback") val iterator = _callbacks.iterator() while (iterator.hasNext()) { val cb = iterator.next().get() if (cb == null || cb === callback) { iterator.remove() } } if (_callbacks.isNotEmpty() || !_started) { return } stop() _started = false } /** * Wrapper to [.addCallback] when a lifecycle is in the resumed state and [.removeCallback] when * not resumed automatically. */ fun observe(owner: LifecycleOwner, listener: Callback): Callback { return observe(owner.lifecycle, listener) } /** * Wrapper to [.addCallback] when a lifecycle is in the resumed state and [.removeCallback] when * not resumed automatically. */ fun observe(lifecycle: Lifecycle, listener: Callback): Callback { lifecycle.addObserver( LifecycleEventObserver { lifecycleOwner: LifecycleOwner?, event: Lifecycle.Event -> if (event == Lifecycle.Event.ON_RESUME) { addCallback(listener) } else if (event == Lifecycle.Event.ON_PAUSE) { removeCallback(listener) } } ) return listener } /** * Updates the value for whether the condition has been fulfilled, and sends an update if the * value changes and any callback is registered. * * @param isConditionMet True if the condition has been fulfilled. False otherwise. */ protected fun updateCondition(isConditionMet: Boolean) { if (_isConditionMet != null && _isConditionMet == isConditionMet) { return } if (shouldLog()) Log.d(mTag, "updating condition to $isConditionMet") _isConditionMet = isConditionMet sendUpdate() } /** * Clears the set condition value. This is purposefully separate from [.updateCondition] to * avoid confusion around `null` values. */ fun clearCondition() { if (_isConditionMet == null) { return } if (shouldLog()) Log.d(mTag, "clearing condition") _isConditionMet = null sendUpdate() } private fun sendUpdate() { val iterator = _callbacks.iterator() while (iterator.hasNext()) { val cb = iterator.next().get() if (cb == null) { iterator.remove() } else { cb.onConditionChanged(this) } } } val isConditionSet: Boolean /** * Returns whether the condition is set. This method should be consulted to understand the * value of [.isConditionMet]. * * @return `true` if value is present, `false` otherwise. */ get() = _isConditionMet != null val isConditionMet: Boolean /** * Returns whether the condition has been met. Note that this method will return `false` if * the condition is not set as well. */ get() = true == _isConditionMet protected fun shouldLog(): Boolean { return Log.isLoggable(mTag, Log.DEBUG) } val tag: String get() { if (isOverridingCondition) { return "$mTag[OVRD]" } return mTag } val state: String /** * Returns the state of the condition. * - "Invalid", condition hasn't been set / not monitored * - "True", condition has been met * - "False", condition has not been met */ get() { if (!isConditionSet) { return "Invalid" } return if (isConditionMet) "True" else "False" } /** * Creates a new condition which will only be true when both this condition and all the provided * conditions are true. */ fun and(others: Collection<Condition>): Condition { val conditions: List<Condition> = listOf(this, *others.toTypedArray()) return CombinedCondition(_scope, conditions, Evaluator.OP_AND) } /** * Creates a new condition which will only be true when both this condition and the provided * condition is true. */ fun and(vararg others: Condition): Condition { return and(listOf(*others)) } /** * Creates a new condition which will only be true when either this condition or any of the * provided conditions are true. */ fun or(others: Collection<Condition>): Condition { val conditions: MutableList<Condition> = ArrayList() conditions.add(this) conditions.addAll(others) return CombinedCondition(_scope, conditions, Evaluator.OP_OR) } /** * Creates a new condition which will only be true when either this condition or the provided * condition is true. */ fun or(vararg others: Condition): Condition { return or(listOf(*others)) } /** Callback that receives updates about whether the condition has been fulfilled. */ fun interface Callback { /** * Called when the fulfillment of the condition changes. * * @param condition The condition in question. */ fun onConditionChanged(condition: Condition) } companion object { /** Condition should be started as soon as there is an active subscription. */ const val START_EAGERLY: Int = 0 /** * Condition should be started lazily only if needed. But once started, it will not be * cancelled unless there are no more active subscriptions. */ const val START_LAZILY: Int = 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. */ const val START_WHEN_NEEDED: Int = 2 } } Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/shared/condition/ConditionExtensionsTest.kt +21 −9 Original line number Original line Diff line number Diff line Loading @@ -19,6 +19,10 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class) class ConditionExtensionsTest : SysuiTestCase() { class ConditionExtensionsTest : SysuiTestCase() { private lateinit var testScope: TestScope private lateinit var testScope: TestScope private val testCallback = Condition.Callback { // This is a no-op } @Before @Before fun setUp() { fun setUp() { Loading @@ -33,7 +37,7 @@ class ConditionExtensionsTest : SysuiTestCase() { assertThat(condition.isConditionSet).isFalse() assertThat(condition.isConditionSet).isFalse() condition.start() condition.testStart() assertThat(condition.isConditionSet).isTrue() assertThat(condition.isConditionSet).isTrue() assertThat(condition.isConditionMet).isTrue() assertThat(condition.isConditionMet).isTrue() } } Loading @@ -46,7 +50,7 @@ class ConditionExtensionsTest : SysuiTestCase() { assertThat(condition.isConditionSet).isFalse() assertThat(condition.isConditionSet).isFalse() condition.start() condition.testStart() assertThat(condition.isConditionSet).isTrue() assertThat(condition.isConditionSet).isTrue() assertThat(condition.isConditionMet).isFalse() assertThat(condition.isConditionMet).isFalse() } } Loading @@ -56,7 +60,7 @@ class ConditionExtensionsTest : SysuiTestCase() { testScope.runTest { testScope.runTest { val flow = emptyFlow<Boolean>() val flow = emptyFlow<Boolean>() val condition = flow.toCondition(scope = this, Condition.START_EAGERLY) val condition = flow.toCondition(scope = this, Condition.START_EAGERLY) condition.start() condition.testStop() assertThat(condition.isConditionSet).isFalse() assertThat(condition.isConditionSet).isFalse() assertThat(condition.isConditionMet).isFalse() assertThat(condition.isConditionMet).isFalse() Loading @@ -72,7 +76,7 @@ class ConditionExtensionsTest : SysuiTestCase() { strategy = Condition.START_EAGERLY, strategy = Condition.START_EAGERLY, initialValue = true, initialValue = true, ) ) condition.start() condition.testStart() assertThat(condition.isConditionSet).isTrue() assertThat(condition.isConditionSet).isTrue() assertThat(condition.isConditionMet).isTrue() assertThat(condition.isConditionMet).isTrue() Loading @@ -88,7 +92,7 @@ class ConditionExtensionsTest : SysuiTestCase() { strategy = Condition.START_EAGERLY, strategy = Condition.START_EAGERLY, initialValue = false, initialValue = false, ) ) condition.start() condition.testStart() assertThat(condition.isConditionSet).isTrue() assertThat(condition.isConditionSet).isTrue() assertThat(condition.isConditionMet).isFalse() assertThat(condition.isConditionMet).isFalse() Loading @@ -99,7 +103,7 @@ class ConditionExtensionsTest : SysuiTestCase() { testScope.runTest { testScope.runTest { val flow = MutableStateFlow(false) val flow = MutableStateFlow(false) val condition = flow.toCondition(scope = this, strategy = Condition.START_EAGERLY) val condition = flow.toCondition(scope = this, strategy = Condition.START_EAGERLY) condition.start() condition.testStart() assertThat(condition.isConditionSet).isTrue() assertThat(condition.isConditionSet).isTrue() assertThat(condition.isConditionMet).isFalse() assertThat(condition.isConditionMet).isFalse() Loading @@ -110,7 +114,7 @@ class ConditionExtensionsTest : SysuiTestCase() { flow.value = false flow.value = false assertThat(condition.isConditionMet).isFalse() assertThat(condition.isConditionMet).isFalse() condition.stop() condition.testStop() } } @Test @Test Loading @@ -120,10 +124,18 @@ class ConditionExtensionsTest : SysuiTestCase() { val condition = flow.toCondition(scope = this, strategy = Condition.START_EAGERLY) val condition = flow.toCondition(scope = this, strategy = Condition.START_EAGERLY) assertThat(flow.subscriptionCount.value).isEqualTo(0) assertThat(flow.subscriptionCount.value).isEqualTo(0) condition.start() condition.testStart() assertThat(flow.subscriptionCount.value).isEqualTo(1) assertThat(flow.subscriptionCount.value).isEqualTo(1) condition.stop() condition.testStop() assertThat(flow.subscriptionCount.value).isEqualTo(0) assertThat(flow.subscriptionCount.value).isEqualTo(0) } } fun Condition.testStart() { addCallback(testCallback) } fun Condition.testStop() { removeCallback(testCallback) } } }
packages/SystemUI/multivalentTests/src/com/android/systemui/shared/condition/FakeCondition.java +1 −1 Original line number Original line Diff line number Diff line Loading @@ -40,7 +40,7 @@ public class FakeCondition extends Condition { } } @Override @Override protected int getStartStrategy() { public int getStartStrategy() { return START_EAGERLY; return START_EAGERLY; } } Loading
packages/SystemUI/shared/src/com/android/systemui/shared/condition/CombinedCondition.kt +5 −6 Original line number Original line Diff line number Diff line Loading @@ -41,7 +41,7 @@ class CombinedCondition constructor( constructor( private val scope: CoroutineScope, private val scope: CoroutineScope, private val conditions: Collection<Condition>, private val conditions: Collection<Condition>, @Evaluator.ConditionOperand private val operand: Int @Evaluator.ConditionOperand private val operand: Int, ) : Condition(scope, null, false) { ) : Condition(scope, null, false) { private var job: Job? = null private var job: Job? = null Loading @@ -54,7 +54,7 @@ constructor( lazilyEvaluate( lazilyEvaluate( conditions = groupedConditions.getOrDefault(true, emptyList()), conditions = groupedConditions.getOrDefault(true, emptyList()), filterUnknown = true filterUnknown = true, ) ) .distinctUntilChanged() .distinctUntilChanged() .flatMapLatest { overriddenValue -> .flatMapLatest { overriddenValue -> Loading @@ -62,7 +62,7 @@ constructor( if (overriddenValue == null) { if (overriddenValue == null) { lazilyEvaluate( lazilyEvaluate( conditions = groupedConditions.getOrDefault(false, emptyList()), conditions = groupedConditions.getOrDefault(false, emptyList()), filterUnknown = false filterUnknown = false, ) ) } else { } else { flowOf(overriddenValue) flowOf(overriddenValue) Loading Loading @@ -188,7 +188,6 @@ constructor( return startStrategy return startStrategy } } override fun getStartStrategy(): Int { override val startStrategy: Int return _startStrategy get() = _startStrategy } } }
packages/SystemUI/shared/src/com/android/systemui/shared/condition/Condition.javadeleted 100644 → 0 +0 −306 Original line number Original line Diff line number Diff line /* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.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 kotlinx.coroutines.CoroutineScope; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Iterator; import java.util.List; /** * Base class for a condition that needs to be fulfilled in order for {@link Monitor} to inform * its callbacks. */ public abstract class Condition { private final String mTag = getClass().getSimpleName(); 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(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. */ protected Condition(CoroutineScope scope, Boolean initialConditionMet, boolean overriding) { mIsConditionMet = initialConditionMet; mOverriding = overriding; mScope = scope; } /** * Starts monitoring the condition. */ protected abstract void start(); /** * Stops monitoring the 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 */ public boolean isOverridingCondition() { return mOverriding; } /** * Registers a callback to receive updates once started. This should be called before * {@link #start()}. Also triggers the callback immediately if already started. */ public void addCallback(@NonNull Callback callback) { if (shouldLog()) Log.d(mTag, "adding callback"); mCallbacks.add(new WeakReference<>(callback)); if (mStarted) { callback.onConditionChanged(this); return; } start(); mStarted = true; } /** * Removes the provided callback from further receiving updates. */ public void removeCallback(@NonNull Callback callback) { if (shouldLog()) Log.d(mTag, "removing callback"); final Iterator<WeakReference<Callback>> iterator = mCallbacks.iterator(); while (iterator.hasNext()) { final Callback cb = iterator.next().get(); if (cb == null || cb == callback) { iterator.remove(); } } if (!mCallbacks.isEmpty() || !mStarted) { return; } stop(); mStarted = false; } /** * Wrapper to {@link #addCallback(Callback)} when a lifecycle is in the resumed state * and {@link #removeCallback(Callback)} when not resumed automatically. */ public Callback observe(LifecycleOwner owner, Callback listener) { return observe(owner.getLifecycle(), listener); } /** * Wrapper to {@link #addCallback(Callback)} when a lifecycle is in the resumed state * and {@link #removeCallback(Condition.Callback)} when not resumed automatically. */ public Callback observe(Lifecycle lifecycle, Callback listener) { lifecycle.addObserver((LifecycleEventObserver) (lifecycleOwner, event) -> { if (event == Lifecycle.Event.ON_RESUME) { addCallback(listener); } else if (event == Lifecycle.Event.ON_PAUSE) { removeCallback(listener); } }); return listener; } /** * Updates the value for whether the condition has been fulfilled, and sends an update if the * value changes and any callback is registered. * * @param isConditionMet True if the condition has been fulfilled. False otherwise. */ protected void updateCondition(boolean isConditionMet) { if (mIsConditionMet != null && mIsConditionMet == isConditionMet) { return; } if (shouldLog()) Log.d(mTag, "updating condition to " + isConditionMet); mIsConditionMet = isConditionMet; sendUpdate(); } /** * Clears the set condition value. This is purposefully separate from * {@link #updateCondition(boolean)} to avoid confusion around {@code null} values. */ protected void clearCondition() { if (mIsConditionMet == null) { return; } if (shouldLog()) Log.d(mTag, "clearing condition"); mIsConditionMet = null; sendUpdate(); } private void sendUpdate() { final Iterator<WeakReference<Callback>> iterator = mCallbacks.iterator(); while (iterator.hasNext()) { final Callback cb = iterator.next().get(); if (cb == null) { iterator.remove(); } else { cb.onConditionChanged(this); } } } /** * 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() { return mIsConditionMet != null; } /** * Returns whether the condition has been met. Note that this method will return {@code false} * if the condition is not set as well. */ public boolean isConditionMet() { return Boolean.TRUE.equals(mIsConditionMet); } protected final boolean shouldLog() { return Log.isLoggable(mTag, Log.DEBUG); } protected final String getTag() { if (isOverridingCondition()) { return mTag + "[OVRD]"; } return mTag; } /** * Returns the state of the condition. * - "Invalid", condition hasn't been set / not monitored * - "True", condition has been met * - "False", condition has not been met */ protected final String getState() { if (!isConditionSet()) { return "Invalid"; } return isConditionMet() ? "True" : "False"; } /** * Creates a new condition which will only be true when both this condition and all the provided * conditions are true. */ public Condition and(@NonNull Collection<Condition> others) { final List<Condition> conditions = new ArrayList<>(); conditions.add(this); 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... others) { return and(Arrays.asList(others)); } /** * Creates a new condition which will only be true when either this condition or any of the * provided conditions are true. */ public Condition or(@NonNull Collection<Condition> others) { final List<Condition> conditions = new ArrayList<>(); conditions.add(this); 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... others) { return or(Arrays.asList(others)); } /** * Callback that receives updates about whether the condition has been fulfilled. */ public interface Callback { /** * Called when the fulfillment of the condition changes. * * @param condition The condition in question. */ void onConditionChanged(Condition condition); } }
packages/SystemUI/shared/src/com/android/systemui/shared/condition/Condition.kt 0 → 100644 +270 −0 Original line number Original line Diff line number Diff line /* * Copyright (C) 2025 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.util.Log import androidx.annotation.IntDef import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleOwner import java.lang.ref.WeakReference import kotlinx.coroutines.CoroutineScope /** * Base class for a condition that needs to be fulfilled in order for [Monitor] to inform its * callbacks. */ abstract class Condition /** * Constructor for specifying initial state and overriding condition attribute. * * @param initialConditionMet Initial state of the condition. * @param overriding Whether this condition overrides others. */ @JvmOverloads protected constructor( private val _scope: CoroutineScope, private var _isConditionMet: Boolean? = false, /** Returns whether the current condition overrides */ val isOverridingCondition: Boolean = false, ) { private val mTag: String = javaClass.simpleName private val _callbacks = mutableListOf<WeakReference<Callback>>() private var _started = false /** Starts monitoring the condition. */ protected abstract fun start() /** Stops monitoring the condition. */ protected abstract fun stop() @Retention(AnnotationRetention.SOURCE) @IntDef(START_EAGERLY, START_LAZILY, START_WHEN_NEEDED) annotation class StartStrategy @get:StartStrategy abstract val startStrategy: Int /** * Registers a callback to receive updates once started. This should be called before [.start]. * Also triggers the callback immediately if already started. */ fun addCallback(callback: Callback) { if (shouldLog()) Log.d(mTag, "adding callback") _callbacks.add(WeakReference(callback)) if (_started) { callback.onConditionChanged(this) return } start() _started = true } /** Removes the provided callback from further receiving updates. */ fun removeCallback(callback: Callback) { if (shouldLog()) Log.d(mTag, "removing callback") val iterator = _callbacks.iterator() while (iterator.hasNext()) { val cb = iterator.next().get() if (cb == null || cb === callback) { iterator.remove() } } if (_callbacks.isNotEmpty() || !_started) { return } stop() _started = false } /** * Wrapper to [.addCallback] when a lifecycle is in the resumed state and [.removeCallback] when * not resumed automatically. */ fun observe(owner: LifecycleOwner, listener: Callback): Callback { return observe(owner.lifecycle, listener) } /** * Wrapper to [.addCallback] when a lifecycle is in the resumed state and [.removeCallback] when * not resumed automatically. */ fun observe(lifecycle: Lifecycle, listener: Callback): Callback { lifecycle.addObserver( LifecycleEventObserver { lifecycleOwner: LifecycleOwner?, event: Lifecycle.Event -> if (event == Lifecycle.Event.ON_RESUME) { addCallback(listener) } else if (event == Lifecycle.Event.ON_PAUSE) { removeCallback(listener) } } ) return listener } /** * Updates the value for whether the condition has been fulfilled, and sends an update if the * value changes and any callback is registered. * * @param isConditionMet True if the condition has been fulfilled. False otherwise. */ protected fun updateCondition(isConditionMet: Boolean) { if (_isConditionMet != null && _isConditionMet == isConditionMet) { return } if (shouldLog()) Log.d(mTag, "updating condition to $isConditionMet") _isConditionMet = isConditionMet sendUpdate() } /** * Clears the set condition value. This is purposefully separate from [.updateCondition] to * avoid confusion around `null` values. */ fun clearCondition() { if (_isConditionMet == null) { return } if (shouldLog()) Log.d(mTag, "clearing condition") _isConditionMet = null sendUpdate() } private fun sendUpdate() { val iterator = _callbacks.iterator() while (iterator.hasNext()) { val cb = iterator.next().get() if (cb == null) { iterator.remove() } else { cb.onConditionChanged(this) } } } val isConditionSet: Boolean /** * Returns whether the condition is set. This method should be consulted to understand the * value of [.isConditionMet]. * * @return `true` if value is present, `false` otherwise. */ get() = _isConditionMet != null val isConditionMet: Boolean /** * Returns whether the condition has been met. Note that this method will return `false` if * the condition is not set as well. */ get() = true == _isConditionMet protected fun shouldLog(): Boolean { return Log.isLoggable(mTag, Log.DEBUG) } val tag: String get() { if (isOverridingCondition) { return "$mTag[OVRD]" } return mTag } val state: String /** * Returns the state of the condition. * - "Invalid", condition hasn't been set / not monitored * - "True", condition has been met * - "False", condition has not been met */ get() { if (!isConditionSet) { return "Invalid" } return if (isConditionMet) "True" else "False" } /** * Creates a new condition which will only be true when both this condition and all the provided * conditions are true. */ fun and(others: Collection<Condition>): Condition { val conditions: List<Condition> = listOf(this, *others.toTypedArray()) return CombinedCondition(_scope, conditions, Evaluator.OP_AND) } /** * Creates a new condition which will only be true when both this condition and the provided * condition is true. */ fun and(vararg others: Condition): Condition { return and(listOf(*others)) } /** * Creates a new condition which will only be true when either this condition or any of the * provided conditions are true. */ fun or(others: Collection<Condition>): Condition { val conditions: MutableList<Condition> = ArrayList() conditions.add(this) conditions.addAll(others) return CombinedCondition(_scope, conditions, Evaluator.OP_OR) } /** * Creates a new condition which will only be true when either this condition or the provided * condition is true. */ fun or(vararg others: Condition): Condition { return or(listOf(*others)) } /** Callback that receives updates about whether the condition has been fulfilled. */ fun interface Callback { /** * Called when the fulfillment of the condition changes. * * @param condition The condition in question. */ fun onConditionChanged(condition: Condition) } companion object { /** Condition should be started as soon as there is an active subscription. */ const val START_EAGERLY: Int = 0 /** * Condition should be started lazily only if needed. But once started, it will not be * cancelled unless there are no more active subscriptions. */ const val START_LAZILY: Int = 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. */ const val START_WHEN_NEEDED: Int = 2 } }