diff options
| author | 2025-02-26 16:51:02 -0800 | |
|---|---|---|
| committer | 2025-03-10 22:34:56 -0700 | |
| commit | 6d85344a249cc1017cfda6227bded6aa11cb6933 (patch) | |
| tree | a3169823e2279ce25ebee8f72959b696625c455b | |
| parent | 819136b66c76334bf1950f0fd9c5ef35b462b2b8 (diff) | |
Make Condition#start a suspend function.
This change makes Condition#start a suspend function so that
suspendable calls can be made within the function body. It also
converts existing conditions to Kotlin to more easily extend start.
Test: atest ConditionTest
Bug: 394520234
Flag: EXEMPT refactor
Change-Id: I877ec08f454d18b5b55c4005039b46ba52011429
41 files changed, 2087 insertions, 2218 deletions
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/DeviceInactiveConditionTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/DeviceInactiveConditionTest.kt index 0c97750ba281..c98d6c531d54 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/DeviceInactiveConditionTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/DeviceInactiveConditionTest.kt @@ -20,6 +20,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.keyguard.keyguardUpdateMonitor import com.android.systemui.SysuiTestCase +import com.android.systemui.condition.testStart import com.android.systemui.keyguard.WakefulnessLifecycle import com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_ASLEEP import com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_AWAKE @@ -67,7 +68,7 @@ class DeviceInactiveConditionTest : SysuiTestCase() { fun asleep_conditionTrue() = kosmos.runTest { // Condition is false to start. - underTest.start() + testStart(underTest) assertThat(underTest.isConditionMet).isFalse() // Condition is true when device goes to sleep. @@ -79,7 +80,7 @@ class DeviceInactiveConditionTest : SysuiTestCase() { fun dozingAndAsleep_conditionFalse() = kosmos.runTest { // Condition is true when device is asleep. - underTest.start() + testStart(underTest) sleep() assertThat(underTest.isConditionMet).isTrue() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/conditions/AssistantAttentionConditionTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/conditions/AssistantAttentionConditionTest.java deleted file mode 100644 index ccadd143b582..000000000000 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/conditions/AssistantAttentionConditionTest.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * 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.dreams.conditions; - -import static com.google.common.truth.Truth.assertThat; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.filters.SmallTest; - -import com.android.systemui.SysuiTestCase; -import com.android.systemui.assist.AssistManager; -import com.android.systemui.assist.AssistManager.VisualQueryAttentionListener; -import com.android.systemui.shared.condition.Condition; - -import kotlinx.coroutines.CoroutineScope; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -@SmallTest -@RunWith(AndroidJUnit4.class) -@android.platform.test.annotations.EnabledOnRavenwood -public class AssistantAttentionConditionTest extends SysuiTestCase { - @Mock - Condition.Callback mCallback; - @Mock - AssistManager mAssistManager; - @Mock - CoroutineScope mScope; - - private AssistantAttentionCondition mAssistantAttentionCondition; - - @Before - public void setup() { - MockitoAnnotations.initMocks(this); - - mAssistantAttentionCondition = new AssistantAttentionCondition(mScope, mAssistManager); - // Adding a callback also starts the condition. - mAssistantAttentionCondition.addCallback(mCallback); - } - - @Test - public void testEnableVisualQueryDetection() { - verify(mAssistManager).addVisualQueryAttentionListener( - any(VisualQueryAttentionListener.class)); - } - - @Test - public void testDisableVisualQueryDetection() { - mAssistantAttentionCondition.stop(); - verify(mAssistManager).removeVisualQueryAttentionListener( - any(VisualQueryAttentionListener.class)); - } - - @Test - public void testAttentionChangedTriggersCondition() { - final ArgumentCaptor<VisualQueryAttentionListener> argumentCaptor = - ArgumentCaptor.forClass(VisualQueryAttentionListener.class); - verify(mAssistManager).addVisualQueryAttentionListener(argumentCaptor.capture()); - - argumentCaptor.getValue().onAttentionGained(); - assertThat(mAssistantAttentionCondition.isConditionMet()).isTrue(); - - argumentCaptor.getValue().onAttentionLost(); - assertThat(mAssistantAttentionCondition.isConditionMet()).isFalse(); - - verify(mCallback, times(2)).onConditionChanged(eq(mAssistantAttentionCondition)); - } -} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/conditions/AssistantAttentionConditionTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/conditions/AssistantAttentionConditionTest.kt new file mode 100644 index 000000000000..eefc61a035c3 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/conditions/AssistantAttentionConditionTest.kt @@ -0,0 +1,85 @@ +/* + * 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.dreams.conditions + +import android.platform.test.annotations.EnabledOnRavenwood +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.assist.AssistManager +import com.android.systemui.assist.AssistManager.VisualQueryAttentionListener +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.testScope +import com.android.systemui.shared.condition.Condition +import com.google.common.truth.Truth +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.eq + +@SmallTest +@RunWith(AndroidJUnit4::class) +@EnabledOnRavenwood +class AssistantAttentionConditionTest : SysuiTestCase() { + private val kosmos = Kosmos() + + @Mock private lateinit var callback: Condition.Callback + + @Mock private lateinit var assistManager: AssistManager + + private lateinit var underTest: AssistantAttentionCondition + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + + underTest = AssistantAttentionCondition(kosmos.testScope, assistManager) + // Adding a callback also starts the condition. + underTest.addCallback(callback) + } + + @Test + fun testEnableVisualQueryDetection() = + kosmos.runTest { Mockito.verify(assistManager).addVisualQueryAttentionListener(any()) } + + @Test + fun testDisableVisualQueryDetection() = + kosmos.runTest { + underTest.stop() + Mockito.verify(assistManager).removeVisualQueryAttentionListener(any()) + } + + @Test + fun testAttentionChangedTriggersCondition() = + kosmos.runTest { + val argumentCaptor = argumentCaptor<VisualQueryAttentionListener>() + Mockito.verify(assistManager).addVisualQueryAttentionListener(argumentCaptor.capture()) + + argumentCaptor.lastValue.onAttentionGained() + Truth.assertThat(underTest.isConditionMet).isTrue() + + argumentCaptor.lastValue.onAttentionLost() + Truth.assertThat(underTest.isConditionMet).isFalse() + + Mockito.verify(callback, Mockito.times(2)).onConditionChanged(eq(underTest)) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/conditions/DreamConditionTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/conditions/DreamConditionTest.java deleted file mode 100644 index 58c17e2a7286..000000000000 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/conditions/DreamConditionTest.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * 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.dreams.conditions; - -import static com.google.common.truth.Truth.assertThat; - -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.clearInvocations; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.app.DreamManager; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.filters.SmallTest; - -import com.android.keyguard.KeyguardUpdateMonitor; -import com.android.keyguard.KeyguardUpdateMonitorCallback; -import com.android.systemui.SysuiTestCase; -import com.android.systemui.shared.condition.Condition; - -import kotlinx.coroutines.CoroutineScope; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -@SmallTest -@RunWith(AndroidJUnit4.class) -@android.platform.test.annotations.EnabledOnRavenwood -public class DreamConditionTest extends SysuiTestCase { - @Mock - Condition.Callback mCallback; - - @Mock - DreamManager mDreamManager; - - @Mock - KeyguardUpdateMonitor mKeyguardUpdateMonitor; - - @Mock - CoroutineScope mScope; - - @Before - public void setup() { - MockitoAnnotations.initMocks(this); - } - - /** - * Ensure a dreaming state immediately triggers the condition. - */ - @Test - public void testInitialDreamingState() { - when(mDreamManager.isDreaming()).thenReturn(true); - final DreamCondition condition = new DreamCondition(mScope, mDreamManager, - mKeyguardUpdateMonitor); - condition.addCallback(mCallback); - - verify(mCallback).onConditionChanged(eq(condition)); - assertThat(condition.isConditionMet()).isTrue(); - } - - /** - * Ensure a non-dreaming state does not trigger the condition. - */ - @Test - public void testInitialNonDreamingState() { - when(mDreamManager.isDreaming()).thenReturn(false); - final DreamCondition condition = new DreamCondition(mScope, mDreamManager, - mKeyguardUpdateMonitor); - condition.addCallback(mCallback); - - verify(mCallback, never()).onConditionChanged(eq(condition)); - assertThat(condition.isConditionMet()).isFalse(); - } - - /** - * Ensure that changing dream state triggers condition. - */ - @Test - public void testChange() { - final ArgumentCaptor<KeyguardUpdateMonitorCallback> callbackCaptor = - ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback.class); - when(mDreamManager.isDreaming()).thenReturn(true); - final DreamCondition condition = new DreamCondition(mScope, mDreamManager, - mKeyguardUpdateMonitor); - condition.addCallback(mCallback); - verify(mKeyguardUpdateMonitor).registerCallback(callbackCaptor.capture()); - - clearInvocations(mCallback); - callbackCaptor.getValue().onDreamingStateChanged(false); - verify(mCallback).onConditionChanged(eq(condition)); - assertThat(condition.isConditionMet()).isFalse(); - - clearInvocations(mCallback); - callbackCaptor.getValue().onDreamingStateChanged(true); - verify(mCallback).onConditionChanged(eq(condition)); - assertThat(condition.isConditionMet()).isTrue(); - } -} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/conditions/DreamConditionTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/conditions/DreamConditionTest.kt new file mode 100644 index 000000000000..302a6e1f432f --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/conditions/DreamConditionTest.kt @@ -0,0 +1,106 @@ +/* + * 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.dreams.conditions + +import android.app.DreamManager +import android.platform.test.annotations.EnabledOnRavenwood +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.keyguard.KeyguardUpdateMonitor +import com.android.keyguard.KeyguardUpdateMonitorCallback +import com.android.systemui.SysuiTestCase +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.runCurrent +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.testScope +import com.android.systemui.shared.condition.Condition +import com.google.common.truth.Truth +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.eq +import org.mockito.kotlin.whenever + +@SmallTest +@RunWith(AndroidJUnit4::class) +@EnabledOnRavenwood +class DreamConditionTest : SysuiTestCase() { + private val kosmos = Kosmos() + + @Mock private lateinit var callback: Condition.Callback + + @Mock private lateinit var dreamManager: DreamManager + + @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + } + + /** Ensure a dreaming state immediately triggers the condition. */ + @Test + fun testInitialDreamingState() = + kosmos.runTest { + whenever(dreamManager.isDreaming).thenReturn(true) + val condition = DreamCondition(testScope, dreamManager, keyguardUpdateMonitor) + condition.addCallback(callback) + runCurrent() + + Mockito.verify(callback).onConditionChanged(eq(condition)) + Truth.assertThat(condition.isConditionMet).isTrue() + } + + /** Ensure a non-dreaming state does not trigger the condition. */ + @Test + fun testInitialNonDreamingState() = + kosmos.runTest { + whenever(dreamManager.isDreaming).thenReturn(false) + val condition = DreamCondition(testScope, dreamManager, keyguardUpdateMonitor) + condition.addCallback(callback) + + Mockito.verify(callback, Mockito.never()).onConditionChanged(eq(condition)) + Truth.assertThat(condition.isConditionMet).isFalse() + } + + /** Ensure that changing dream state triggers condition. */ + @Test + fun testChange() = + kosmos.runTest { + val callbackCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>() + whenever(dreamManager.isDreaming).thenReturn(true) + val condition = DreamCondition(testScope, dreamManager, keyguardUpdateMonitor) + condition.addCallback(callback) + runCurrent() + Mockito.verify(keyguardUpdateMonitor).registerCallback(callbackCaptor.capture()) + + Mockito.clearInvocations(callback) + callbackCaptor.lastValue.onDreamingStateChanged(false) + runCurrent() + Mockito.verify(callback).onConditionChanged(eq(condition)) + Truth.assertThat(condition.isConditionMet).isFalse() + + Mockito.clearInvocations(callback) + callbackCaptor.lastValue.onDreamingStateChanged(true) + runCurrent() + Mockito.verify(callback).onConditionChanged(eq(condition)) + Truth.assertThat(condition.isConditionMet).isTrue() + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/process/condition/SystemProcessConditionTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/process/condition/SystemProcessConditionTest.java deleted file mode 100644 index 5250d56edc0b..000000000000 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/process/condition/SystemProcessConditionTest.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * 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.process.condition; - -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.testing.TestableLooper; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.filters.SmallTest; - -import com.android.systemui.SysuiTestCase; -import com.android.systemui.process.ProcessWrapper; -import com.android.systemui.shared.condition.Condition; -import com.android.systemui.shared.condition.Monitor; -import com.android.systemui.util.concurrency.FakeExecutor; -import com.android.systemui.util.time.FakeSystemClock; - -import kotlinx.coroutines.CoroutineScope; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -@RunWith(AndroidJUnit4.class) -@TestableLooper.RunWithLooper -@SmallTest -public class SystemProcessConditionTest extends SysuiTestCase { - @Mock - ProcessWrapper mProcessWrapper; - - @Mock - Monitor.Callback mCallback; - - @Mock - CoroutineScope mScope; - - private final FakeExecutor mExecutor = new FakeExecutor(new FakeSystemClock()); - - @Before - public void setup() { - MockitoAnnotations.initMocks(this); - } - - /** - * Verifies condition reports false when tracker reports the process is being ran by the - * system user. - */ - @Test - public void testConditionFailsWithNonSystemProcess() { - - final Condition condition = new SystemProcessCondition(mScope, mProcessWrapper); - when(mProcessWrapper.isSystemUser()).thenReturn(false); - - final Monitor monitor = new Monitor(mExecutor); - - monitor.addSubscription(new Monitor.Subscription.Builder(mCallback) - .addCondition(condition) - .build()); - - mExecutor.runAllReady(); - - verify(mCallback).onConditionsChanged(false); - } - - /** - * Verifies condition reports true when tracker reports the process is being ran by the - * system user. - */ - @Test - public void testConditionSucceedsWithSystemProcess() { - - final Condition condition = new SystemProcessCondition(mScope, mProcessWrapper); - when(mProcessWrapper.isSystemUser()).thenReturn(true); - - final Monitor monitor = new Monitor(mExecutor); - - monitor.addSubscription(new Monitor.Subscription.Builder(mCallback) - .addCondition(condition) - .build()); - - mExecutor.runAllReady(); - - verify(mCallback).onConditionsChanged(true); - } -} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/process/condition/SystemProcessConditionTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/process/condition/SystemProcessConditionTest.kt new file mode 100644 index 000000000000..21524d97d7af --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/process/condition/SystemProcessConditionTest.kt @@ -0,0 +1,101 @@ +/* + * 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.process.condition + +import android.testing.TestableLooper.RunWithLooper +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.runCurrent +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.testScope +import com.android.systemui.process.ProcessWrapper +import com.android.systemui.shared.condition.Condition +import com.android.systemui.shared.condition.Monitor +import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.time.FakeSystemClock +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.whenever + +@RunWith(AndroidJUnit4::class) +@RunWithLooper +@SmallTest +class SystemProcessConditionTest : SysuiTestCase() { + private val kosmos = Kosmos() + + @Mock private lateinit var processWrapper: ProcessWrapper + + @Mock private lateinit var callback: Monitor.Callback + + private val executor = FakeExecutor(FakeSystemClock()) + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + } + + /** + * Verifies condition reports false when tracker reports the process is being ran by the system + * user. + */ + @Test + fun testConditionFailsWithNonSystemProcess() = + kosmos.runTest { + val condition: Condition = SystemProcessCondition(kosmos.testScope, processWrapper) + whenever(processWrapper.isSystemUser).thenReturn(false) + + val monitor = Monitor(executor) + + monitor.addSubscription( + Monitor.Subscription.Builder(callback).addCondition(condition).build() + ) + + executor.runAllReady() + runCurrent() + executor.runAllReady() + + Mockito.verify(callback).onConditionsChanged(false) + } + + /** + * Verifies condition reports true when tracker reports the process is being ran by the system + * user. + */ + @Test + fun testConditionSucceedsWithSystemProcess() = + kosmos.runTest { + val condition: Condition = SystemProcessCondition(testScope, processWrapper) + whenever(processWrapper.isSystemUser).thenReturn(true) + + val monitor = Monitor(executor) + + monitor.addSubscription( + Monitor.Subscription.Builder(callback).addCondition(condition).build() + ) + + executor.runAllReady() + runCurrent() + executor.runAllReady() + + Mockito.verify(callback).onConditionsChanged(true) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/condition/ConditionExtensionsTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/condition/ConditionExtensionsTest.kt index 17509dc6a80f..8e2d1b5a88f6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/condition/ConditionExtensionsTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/condition/ConditionExtensionsTest.kt @@ -3,64 +3,57 @@ package com.android.systemui.shared.condition import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.condition.testStart +import com.android.systemui.condition.testStop +import com.android.systemui.kosmos.runCurrent +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.runTest -import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) class ConditionExtensionsTest : SysuiTestCase() { - private lateinit var testScope: TestScope - private val testCallback = - Condition.Callback { - // This is a no-op - } - - @Before - fun setUp() { - testScope = TestScope(UnconfinedTestDispatcher()) - } + private val kosmos = testKosmos() @Test fun flowInitiallyTrue() = - testScope.runTest { + kosmos.runTest { val flow = flowOf(true) - val condition = flow.toCondition(scope = this, Condition.START_EAGERLY) + val condition = flow.toCondition(scope = testScope, Condition.START_EAGERLY) assertThat(condition.isConditionSet).isFalse() - condition.testStart() + testStart(condition) assertThat(condition.isConditionSet).isTrue() assertThat(condition.isConditionMet).isTrue() } @Test fun flowInitiallyFalse() = - testScope.runTest { + kosmos.runTest { val flow = flowOf(false) - val condition = flow.toCondition(scope = this, Condition.START_EAGERLY) + val condition = flow.toCondition(scope = testScope, Condition.START_EAGERLY) assertThat(condition.isConditionSet).isFalse() - condition.testStart() + testStart(condition) assertThat(condition.isConditionSet).isTrue() assertThat(condition.isConditionMet).isFalse() } @Test fun emptyFlowWithNoInitialValue() = - testScope.runTest { + kosmos.runTest { val flow = emptyFlow<Boolean>() - val condition = flow.toCondition(scope = this, Condition.START_EAGERLY) - condition.testStop() + val condition = flow.toCondition(scope = testScope, Condition.START_EAGERLY) + testStop(condition) assertThat(condition.isConditionSet).isFalse() assertThat(condition.isConditionMet).isFalse() @@ -68,15 +61,15 @@ class ConditionExtensionsTest : SysuiTestCase() { @Test fun emptyFlowWithInitialValueOfTrue() = - testScope.runTest { + kosmos.runTest { val flow = emptyFlow<Boolean>() val condition = flow.toCondition( - scope = this, + scope = testScope, strategy = Condition.START_EAGERLY, initialValue = true, ) - condition.testStart() + testStart(condition) assertThat(condition.isConditionSet).isTrue() assertThat(condition.isConditionMet).isTrue() @@ -84,15 +77,15 @@ class ConditionExtensionsTest : SysuiTestCase() { @Test fun emptyFlowWithInitialValueOfFalse() = - testScope.runTest { + kosmos.runTest { val flow = emptyFlow<Boolean>() val condition = flow.toCondition( - scope = this, + scope = testScope, strategy = Condition.START_EAGERLY, initialValue = false, ) - condition.testStart() + testStart(condition) assertThat(condition.isConditionSet).isTrue() assertThat(condition.isConditionMet).isFalse() @@ -100,42 +93,36 @@ class ConditionExtensionsTest : SysuiTestCase() { @Test fun conditionUpdatesWhenFlowEmitsNewValue() = - testScope.runTest { + kosmos.runTest { val flow = MutableStateFlow(false) - val condition = flow.toCondition(scope = this, strategy = Condition.START_EAGERLY) - condition.testStart() + val condition = flow.toCondition(scope = testScope, strategy = Condition.START_EAGERLY) + testStart(condition) assertThat(condition.isConditionSet).isTrue() assertThat(condition.isConditionMet).isFalse() flow.value = true + runCurrent() assertThat(condition.isConditionMet).isTrue() flow.value = false + runCurrent() assertThat(condition.isConditionMet).isFalse() - condition.testStop() + testStop(condition) } @Test fun stoppingConditionUnsubscribesFromFlow() = - testScope.runTest { + kosmos.runTest { val flow = MutableSharedFlow<Boolean>() - val condition = flow.toCondition(scope = this, strategy = Condition.START_EAGERLY) + val condition = flow.toCondition(scope = testScope, strategy = Condition.START_EAGERLY) assertThat(flow.subscriptionCount.value).isEqualTo(0) - condition.testStart() + testStart(condition) assertThat(flow.subscriptionCount.value).isEqualTo(1) - condition.testStop() + testStop(condition) assertThat(flow.subscriptionCount.value).isEqualTo(0) } - - fun Condition.testStart() { - addCallback(testCallback) - } - - fun Condition.testStop() { - removeCallback(testCallback) - } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/condition/ConditionMonitorTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/condition/ConditionMonitorTest.java deleted file mode 100644 index 267f22bb2569..000000000000 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/condition/ConditionMonitorTest.java +++ /dev/null @@ -1,621 +0,0 @@ -/* - * 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 static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.anyBoolean; -import static org.mockito.Mockito.clearInvocations; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.filters.SmallTest; - -import com.android.systemui.SysuiTestCase; -import com.android.systemui.plugins.log.TableLogBufferBase; -import com.android.systemui.util.concurrency.FakeExecutor; -import com.android.systemui.util.time.FakeSystemClock; - -import kotlinx.coroutines.CoroutineScope; - -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.Collections; -import java.util.HashSet; - -@SmallTest -@RunWith(AndroidJUnit4.class) -public class ConditionMonitorTest extends SysuiTestCase { - private FakeCondition mCondition1; - private FakeCondition mCondition2; - private FakeCondition mCondition3; - private HashSet<Condition> mConditions; - private FakeExecutor mExecutor = new FakeExecutor(new FakeSystemClock()); - - @Mock - private CoroutineScope mScope; - @Mock - private TableLogBufferBase mLogBuffer; - - private Monitor mConditionMonitor; - - @Before - public void setup() { - MockitoAnnotations.initMocks(this); - - 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); - } - - public Monitor.Subscription.Builder getDefaultBuilder( - Monitor.Callback callback) { - return new Monitor.Subscription.Builder(callback) - .addConditions(mConditions); - } - - private Condition createMockCondition() { - final Condition condition = Mockito.mock( - Condition.class); - when(condition.isConditionSet()).thenReturn(true); - return condition; - } - - @Test - public void testOverridingCondition() { - final Condition overridingCondition = createMockCondition(); - final Condition regularCondition = createMockCondition(); - final Monitor.Callback callback = Mockito.mock( - Monitor.Callback.class); - - final Monitor.Callback referenceCallback = Mockito.mock( - Monitor.Callback.class); - - final Monitor - monitor = new Monitor(mExecutor); - - monitor.addSubscription(getDefaultBuilder(callback) - .addCondition(overridingCondition) - .addCondition(regularCondition) - .build()); - - monitor.addSubscription(getDefaultBuilder(referenceCallback) - .addCondition(regularCondition) - .build()); - - mExecutor.runAllReady(); - - when(overridingCondition.isOverridingCondition()).thenReturn(true); - when(overridingCondition.isConditionMet()).thenReturn(true); - when(regularCondition.isConditionMet()).thenReturn(false); - - final ArgumentCaptor<Condition.Callback> mCallbackCaptor = - ArgumentCaptor.forClass(Condition.Callback.class); - - verify(overridingCondition).addCallback(mCallbackCaptor.capture()); - - mCallbackCaptor.getValue().onConditionChanged(overridingCondition); - mExecutor.runAllReady(); - - verify(callback).onConditionsChanged(eq(true)); - verify(referenceCallback).onConditionsChanged(eq(false)); - Mockito.clearInvocations(callback); - Mockito.clearInvocations(referenceCallback); - - when(regularCondition.isConditionMet()).thenReturn(true); - when(overridingCondition.isConditionMet()).thenReturn(false); - - mCallbackCaptor.getValue().onConditionChanged(overridingCondition); - mExecutor.runAllReady(); - - verify(callback).onConditionsChanged(eq(false)); - verify(referenceCallback, never()).onConditionsChanged(anyBoolean()); - } - - /** - * Ensures that when multiple overriding conditions are present, it is the aggregate of those - * conditions that are considered. - */ - @Test - public void testMultipleOverridingConditions() { - final Condition overridingCondition = createMockCondition(); - final Condition overridingCondition2 = createMockCondition(); - final Condition regularCondition = createMockCondition(); - final Monitor.Callback callback = Mockito.mock( - Monitor.Callback.class); - - final Monitor - monitor = new Monitor(mExecutor); - - monitor.addSubscription(getDefaultBuilder(callback) - .addCondition(overridingCondition) - .addCondition(overridingCondition2) - .build()); - - mExecutor.runAllReady(); - - when(overridingCondition.isOverridingCondition()).thenReturn(true); - when(overridingCondition.isConditionMet()).thenReturn(true); - when(overridingCondition2.isOverridingCondition()).thenReturn(true); - when(overridingCondition.isConditionMet()).thenReturn(false); - when(regularCondition.isConditionMet()).thenReturn(true); - - final ArgumentCaptor<Condition.Callback> mCallbackCaptor = - ArgumentCaptor.forClass(Condition.Callback.class); - - verify(overridingCondition).addCallback(mCallbackCaptor.capture()); - - mCallbackCaptor.getValue().onConditionChanged(overridingCondition); - mExecutor.runAllReady(); - - verify(callback).onConditionsChanged(eq(false)); - Mockito.clearInvocations(callback); - } - - // Ensure that updating a callback that is removed doesn't result in an exception due to the - // absence of the condition. - @Test - public void testUpdateRemovedCallback() { - final Monitor.Callback callback1 = - mock(Monitor.Callback.class); - final Monitor.Subscription.Token subscription1 = - mConditionMonitor.addSubscription(getDefaultBuilder(callback1).build()); - ArgumentCaptor<Condition.Callback> monitorCallback = - ArgumentCaptor.forClass(Condition.Callback.class); - mExecutor.runAllReady(); - verify(mCondition1).addCallback(monitorCallback.capture()); - // This will execute first before the handler for onConditionChanged. - mConditionMonitor.removeSubscription(subscription1); - monitorCallback.getValue().onConditionChanged(mCondition1); - mExecutor.runAllReady(); - } - - @Test - public void addCallback_addFirstCallback_addCallbackToAllConditions() { - final Monitor.Callback callback1 = - mock(Monitor.Callback.class); - mConditionMonitor.addSubscription(getDefaultBuilder(callback1).build()); - mExecutor.runAllReady(); - mConditions.forEach(condition -> verify(condition).addCallback(any())); - - final Monitor.Callback callback2 = - mock(Monitor.Callback.class); - mConditionMonitor.addSubscription(getDefaultBuilder(callback2).build()); - mExecutor.runAllReady(); - mConditions.forEach(condition -> verify(condition, times(1)).addCallback(any())); - } - - @Test - public void addCallback_addFirstCallback_reportWithDefaultValue() { - final Monitor.Callback callback = - mock(Monitor.Callback.class); - mConditionMonitor.addSubscription(getDefaultBuilder(callback).build()); - mExecutor.runAllReady(); - verify(callback).onConditionsChanged(false); - } - - @Test - public void addCallback_addSecondCallback_reportWithExistingValue() { - final Monitor.Callback callback1 = - mock(Monitor.Callback.class); - final Condition condition = mock( - Condition.class); - when(condition.isConditionMet()).thenReturn(true); - final Monitor - monitor = new Monitor(mExecutor); - monitor.addSubscription(new Monitor.Subscription.Builder(callback1) - .addCondition(condition) - .build()); - - final Monitor.Callback callback2 = - mock(Monitor.Callback.class); - monitor.addSubscription(new Monitor.Subscription.Builder(callback2) - .addCondition(condition) - .build()); - mExecutor.runAllReady(); - verify(callback2).onConditionsChanged(eq(true)); - } - - @Test - public void addCallback_noConditions_reportAllConditionsMet() { - final Monitor - monitor = new Monitor(mExecutor); - final Monitor.Callback callback = mock( - Monitor.Callback.class); - - monitor.addSubscription(new Monitor.Subscription.Builder(callback).build()); - mExecutor.runAllReady(); - verify(callback).onConditionsChanged(true); - } - - @Test - public void addCallback_preCondition_noConditions_reportAllConditionsMet() { - final Monitor - monitor = new Monitor(mExecutor, new HashSet<>(Arrays.asList(mCondition1))); - final Monitor.Callback callback = mock( - Monitor.Callback.class); - - monitor.addSubscription(new Monitor.Subscription.Builder(callback).build()); - mExecutor.runAllReady(); - verify(callback, never()).onConditionsChanged(true); - mCondition1.fakeUpdateCondition(true); - mExecutor.runAllReady(); - verify(callback).onConditionsChanged(true); - } - - @Test - public void removeCallback_noFailureOnDoubleRemove() { - final Condition condition = mock( - Condition.class); - final Monitor - monitor = new Monitor(mExecutor); - final Monitor.Callback callback = - mock(Monitor.Callback.class); - final Monitor.Subscription.Token token = monitor.addSubscription( - new Monitor.Subscription.Builder(callback).addCondition(condition).build() - ); - monitor.removeSubscription(token); - mExecutor.runAllReady(); - // Ensure second removal doesn't cause an exception. - monitor.removeSubscription(token); - mExecutor.runAllReady(); - } - - @Test - public void removeCallback_shouldNoLongerReceiveUpdate() { - final Condition condition = mock( - Condition.class); - final Monitor - monitor = new Monitor(mExecutor); - final Monitor.Callback callback = - mock(Monitor.Callback.class); - final Monitor.Subscription.Token token = monitor.addSubscription( - new Monitor.Subscription.Builder(callback).addCondition(condition).build() - ); - monitor.removeSubscription(token); - mExecutor.runAllReady(); - clearInvocations(callback); - - final ArgumentCaptor<Condition.Callback> conditionCallbackCaptor = - ArgumentCaptor.forClass(Condition.Callback.class); - verify(condition).addCallback(conditionCallbackCaptor.capture()); - - final Condition.Callback conditionCallback = conditionCallbackCaptor.getValue(); - verify(condition).removeCallback(conditionCallback); - } - - @Test - public void removeCallback_removeLastCallback_removeCallbackFromAllConditions() { - final Monitor.Callback callback1 = - mock(Monitor.Callback.class); - final Monitor.Callback callback2 = - mock(Monitor.Callback.class); - final Monitor.Subscription.Token subscription1 = - mConditionMonitor.addSubscription(getDefaultBuilder(callback1).build()); - final Monitor.Subscription.Token subscription2 = - mConditionMonitor.addSubscription(getDefaultBuilder(callback2).build()); - - mConditionMonitor.removeSubscription(subscription1); - mExecutor.runAllReady(); - mConditions.forEach(condition -> verify(condition, never()).removeCallback(any())); - - mConditionMonitor.removeSubscription(subscription2); - mExecutor.runAllReady(); - mConditions.forEach(condition -> verify(condition).removeCallback(any())); - } - - @Test - public void updateCallbacks_allConditionsMet_reportTrue() { - final Monitor.Callback callback = - mock(Monitor.Callback.class); - mConditionMonitor.addSubscription(getDefaultBuilder(callback).build()); - clearInvocations(callback); - - mCondition1.fakeUpdateCondition(true); - mCondition2.fakeUpdateCondition(true); - mCondition3.fakeUpdateCondition(true); - mExecutor.runAllReady(); - - verify(callback).onConditionsChanged(true); - } - - @Test - public void updateCallbacks_oneConditionStoppedMeeting_reportFalse() { - final Monitor.Callback callback = - mock(Monitor.Callback.class); - mConditionMonitor.addSubscription(getDefaultBuilder(callback).build()); - - mCondition1.fakeUpdateCondition(true); - mCondition2.fakeUpdateCondition(true); - mCondition3.fakeUpdateCondition(true); - clearInvocations(callback); - - mCondition1.fakeUpdateCondition(false); - mExecutor.runAllReady(); - verify(callback).onConditionsChanged(false); - } - - @Test - public void updateCallbacks_shouldOnlyUpdateWhenValueChanges() { - final Monitor.Callback callback = - mock(Monitor.Callback.class); - mConditionMonitor.addSubscription(getDefaultBuilder(callback).build()); - mExecutor.runAllReady(); - verify(callback).onConditionsChanged(false); - clearInvocations(callback); - - mCondition1.fakeUpdateCondition(true); - mExecutor.runAllReady(); - verify(callback, never()).onConditionsChanged(anyBoolean()); - - mCondition2.fakeUpdateCondition(true); - mExecutor.runAllReady(); - verify(callback, never()).onConditionsChanged(anyBoolean()); - - mCondition3.fakeUpdateCondition(true); - mExecutor.runAllReady(); - verify(callback).onConditionsChanged(true); - } - - @Test - public void clearCondition_shouldUpdateValue() { - mCondition1.fakeUpdateCondition(false); - mCondition2.fakeUpdateCondition(true); - mCondition3.fakeUpdateCondition(true); - - final Monitor.Callback callback = - mock(Monitor.Callback.class); - mConditionMonitor.addSubscription(getDefaultBuilder(callback).build()); - mExecutor.runAllReady(); - verify(callback).onConditionsChanged(false); - - mCondition1.clearCondition(); - mExecutor.runAllReady(); - verify(callback).onConditionsChanged(true); - } - - @Test - public void unsetCondition_shouldNotAffectValue() { - final FakeCondition settableCondition = new FakeCondition(mScope, null, false); - mCondition1.fakeUpdateCondition(true); - mCondition2.fakeUpdateCondition(true); - mCondition3.fakeUpdateCondition(true); - - final Monitor.Callback callback = - mock(Monitor.Callback.class); - - mConditionMonitor.addSubscription(getDefaultBuilder(callback) - .addCondition(settableCondition) - .build()); - - mExecutor.runAllReady(); - verify(callback).onConditionsChanged(true); - } - - @Test - public void setUnsetCondition_shouldAffectValue() { - final FakeCondition settableCondition = new FakeCondition(mScope, null, false); - mCondition1.fakeUpdateCondition(true); - mCondition2.fakeUpdateCondition(true); - mCondition3.fakeUpdateCondition(true); - - final Monitor.Callback callback = - mock(Monitor.Callback.class); - - mConditionMonitor.addSubscription(getDefaultBuilder(callback) - .addCondition(settableCondition) - .build()); - - mExecutor.runAllReady(); - verify(callback).onConditionsChanged(true); - clearInvocations(callback); - - settableCondition.fakeUpdateCondition(false); - mExecutor.runAllReady(); - verify(callback).onConditionsChanged(false); - clearInvocations(callback); - - - settableCondition.clearCondition(); - mExecutor.runAllReady(); - verify(callback).onConditionsChanged(true); - } - - @Test - public void clearingOverridingCondition_shouldBeExcluded() { - final FakeCondition overridingCondition = new FakeCondition(mScope, true, true); - mCondition1.fakeUpdateCondition(false); - mCondition2.fakeUpdateCondition(false); - mCondition3.fakeUpdateCondition(false); - - final Monitor.Callback callback = - mock(Monitor.Callback.class); - - mConditionMonitor.addSubscription(getDefaultBuilder(callback) - .addCondition(overridingCondition) - .build()); - - mExecutor.runAllReady(); - verify(callback).onConditionsChanged(true); - clearInvocations(callback); - - overridingCondition.clearCondition(); - mExecutor.runAllReady(); - verify(callback).onConditionsChanged(false); - } - - @Test - public void settingUnsetOverridingCondition_shouldBeIncluded() { - final FakeCondition overridingCondition = new FakeCondition(mScope, null, true); - mCondition1.fakeUpdateCondition(false); - mCondition2.fakeUpdateCondition(false); - mCondition3.fakeUpdateCondition(false); - - final Monitor.Callback callback = - mock(Monitor.Callback.class); - - mConditionMonitor.addSubscription(getDefaultBuilder(callback) - .addCondition(overridingCondition) - .build()); - - mExecutor.runAllReady(); - verify(callback).onConditionsChanged(false); - clearInvocations(callback); - - overridingCondition.fakeUpdateCondition(true); - mExecutor.runAllReady(); - verify(callback).onConditionsChanged(true); - } - - /** - * Ensures that the result of a condition being true leads to its nested condition being - * activated. - */ - @Test - public void testNestedCondition() { - mCondition1.fakeUpdateCondition(false); - final Monitor.Callback callback = - mock(Monitor.Callback.class); - - mCondition2.fakeUpdateCondition(false); - - // Create a nested condition - mConditionMonitor.addSubscription(new Monitor.Subscription.Builder( - new Monitor.Subscription.Builder(callback) - .addCondition(mCondition2) - .build()) - .addCondition(mCondition1) - .build()); - - mExecutor.runAllReady(); - - // Ensure the nested condition callback is not called at all. - verify(callback, never()).onActiveChanged(anyBoolean()); - verify(callback, never()).onConditionsChanged(anyBoolean()); - - // Update the inner condition to true and ensure that the nested condition is not triggered. - mCondition2.fakeUpdateCondition(true); - verify(callback, never()).onConditionsChanged(anyBoolean()); - mCondition2.fakeUpdateCondition(false); - - // Set outer condition and make sure the inner condition becomes active and reports that - // conditions aren't met - mCondition1.fakeUpdateCondition(true); - mExecutor.runAllReady(); - - verify(callback).onActiveChanged(eq(true)); - verify(callback).onConditionsChanged(eq(false)); - - Mockito.clearInvocations(callback); - - // Update the inner condition and make sure the callback is updated. - mCondition2.fakeUpdateCondition(true); - mExecutor.runAllReady(); - - verify(callback).onConditionsChanged(true); - - Mockito.clearInvocations(callback); - // Invalidate outer condition and make sure callback is informed, but the last state is - // not affected. - mCondition1.fakeUpdateCondition(false); - mExecutor.runAllReady(); - - verify(callback).onActiveChanged(eq(false)); - verify(callback, never()).onConditionsChanged(anyBoolean()); - } - - /** - * Ensure preconditions are applied to every subscription added to a monitor. - */ - @Test - public void testPreconditionMonitor() { - final Monitor.Callback callback = - mock(Monitor.Callback.class); - - mCondition2.fakeUpdateCondition(true); - final Monitor monitor = new Monitor(mExecutor, new HashSet<>(Arrays.asList(mCondition1))); - - monitor.addSubscription(new Monitor.Subscription.Builder(callback) - .addCondition(mCondition2) - .build()); - - mExecutor.runAllReady(); - - verify(callback, never()).onActiveChanged(anyBoolean()); - verify(callback, never()).onConditionsChanged(anyBoolean()); - - mCondition1.fakeUpdateCondition(true); - mExecutor.runAllReady(); - - verify(callback).onActiveChanged(eq(true)); - verify(callback).onConditionsChanged(eq(true)); - } - - @Test - public void testLoggingCallback() { - final Monitor monitor = new Monitor(mExecutor, Collections.emptySet(), mLogBuffer); - - final FakeCondition condition = new FakeCondition(mScope); - final FakeCondition overridingCondition = new FakeCondition( - mScope, - /* initialValue= */ false, - /* overriding= */ true); - - final Monitor.Callback callback = mock(Monitor.Callback.class); - monitor.addSubscription(getDefaultBuilder(callback) - .addCondition(condition) - .addCondition(overridingCondition) - .build()); - mExecutor.runAllReady(); - - // condition set to true - condition.fakeUpdateCondition(true); - mExecutor.runAllReady(); - verify(mLogBuffer).logChange("", "FakeCondition", "True"); - - // condition set to false - condition.fakeUpdateCondition(false); - mExecutor.runAllReady(); - verify(mLogBuffer).logChange("", "FakeCondition", "False"); - - // condition unset - condition.fakeClearCondition(); - mExecutor.runAllReady(); - verify(mLogBuffer).logChange("", "FakeCondition", "Invalid"); - - // overriding condition set to true - overridingCondition.fakeUpdateCondition(true); - mExecutor.runAllReady(); - verify(mLogBuffer).logChange("", "FakeCondition[OVRD]", "True"); - } -} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/condition/ConditionMonitorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/condition/ConditionMonitorTest.kt new file mode 100644 index 000000000000..dc45c5317b8e --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/condition/ConditionMonitorTest.kt @@ -0,0 +1,605 @@ +/* + * 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 androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.testScope +import com.android.systemui.plugins.log.TableLogBufferBase +import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.time.FakeSystemClock +import java.util.Arrays +import java.util.function.Consumer +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +@SmallTest +@RunWith(AndroidJUnit4::class) +class ConditionMonitorTest : SysuiTestCase() { + private val kosmos = Kosmos() + + private lateinit var condition1: FakeCondition + private lateinit var condition2: FakeCondition + private lateinit var condition3: FakeCondition + private lateinit var conditions: HashSet<Condition> + private val executor = FakeExecutor(FakeSystemClock()) + + @Mock private lateinit var logBuffer: TableLogBufferBase + + private lateinit var conditionMonitor: Monitor + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + + condition1 = Mockito.spy(FakeCondition(kosmos.testScope)) + condition2 = Mockito.spy(FakeCondition(kosmos.testScope)) + condition3 = Mockito.spy(FakeCondition(kosmos.testScope)) + conditions = HashSet(listOf(condition1, condition2, condition3)) + + conditionMonitor = Monitor(executor) + } + + fun getDefaultBuilder(callback: Monitor.Callback): Monitor.Subscription.Builder { + return Monitor.Subscription.Builder(callback).addConditions(conditions) + } + + private fun createMockCondition(): Condition { + val condition: Condition = mock() + whenever(condition.isConditionSet).thenReturn(true) + return condition + } + + @Test + fun testOverridingCondition() = + kosmos.runTest { + val overridingCondition = createMockCondition() + val regularCondition = createMockCondition() + val callback: Monitor.Callback = mock() + val referenceCallback: Monitor.Callback = mock() + + val monitor = Monitor(executor) + + monitor.addSubscription( + getDefaultBuilder(callback) + .addCondition(overridingCondition) + .addCondition(regularCondition) + .build() + ) + + monitor.addSubscription( + getDefaultBuilder(referenceCallback).addCondition(regularCondition).build() + ) + + executor.runAllReady() + + whenever(overridingCondition.isOverridingCondition).thenReturn(true) + whenever(overridingCondition.isConditionMet).thenReturn(true) + whenever(regularCondition.isConditionMet).thenReturn(false) + + val callbackCaptor = argumentCaptor<Condition.Callback>() + + Mockito.verify(overridingCondition).addCallback(callbackCaptor.capture()) + + callbackCaptor.lastValue.onConditionChanged(overridingCondition) + executor.runAllReady() + + Mockito.verify(callback).onConditionsChanged(eq(true)) + Mockito.verify(referenceCallback).onConditionsChanged(eq(false)) + Mockito.clearInvocations(callback) + Mockito.clearInvocations(referenceCallback) + + whenever(regularCondition.isConditionMet).thenReturn(true) + whenever(overridingCondition.isConditionMet).thenReturn(false) + + callbackCaptor.lastValue.onConditionChanged(overridingCondition) + executor.runAllReady() + + Mockito.verify(callback).onConditionsChanged(eq(false)) + Mockito.verify(referenceCallback, Mockito.never()).onConditionsChanged(anyBoolean()) + } + + /** + * Ensures that when multiple overriding conditions are present, it is the aggregate of those + * conditions that are considered. + */ + @Test + fun testMultipleOverridingConditions() = + kosmos.runTest { + val overridingCondition = createMockCondition() + val overridingCondition2 = createMockCondition() + val regularCondition = createMockCondition() + val callback: Monitor.Callback = mock() + + val monitor = Monitor(executor) + + monitor.addSubscription( + getDefaultBuilder(callback) + .addCondition(overridingCondition) + .addCondition(overridingCondition2) + .build() + ) + + executor.runAllReady() + + whenever(overridingCondition.isOverridingCondition).thenReturn(true) + whenever(overridingCondition.isConditionMet).thenReturn(true) + whenever(overridingCondition2.isOverridingCondition).thenReturn(true) + whenever(overridingCondition.isConditionMet).thenReturn(false) + whenever(regularCondition.isConditionMet).thenReturn(true) + + val mCallbackCaptor = argumentCaptor<Condition.Callback>() + + Mockito.verify(overridingCondition).addCallback(mCallbackCaptor.capture()) + + mCallbackCaptor.lastValue.onConditionChanged(overridingCondition) + executor.runAllReady() + + Mockito.verify(callback).onConditionsChanged(eq(false)) + Mockito.clearInvocations(callback) + } + + // Ensure that updating a callback that is removed doesn't result in an exception due to the + // absence of the condition. + @Test + fun testUpdateRemovedCallback() = + kosmos.runTest { + val callback1: Monitor.Callback = mock() + val subscription1 = + conditionMonitor.addSubscription(getDefaultBuilder(callback1).build()) + val monitorCallback = argumentCaptor<Condition.Callback>() + executor.runAllReady() + Mockito.verify(condition1).addCallback(monitorCallback.capture()) + // This will execute first before the handler for onConditionChanged. + conditionMonitor.removeSubscription(subscription1) + monitorCallback.lastValue.onConditionChanged(condition1) + executor.runAllReady() + } + + @Test + fun addCallback_addFirstCallback_addCallbackToAllConditions() { + val callback1: Monitor.Callback = mock() + conditionMonitor.addSubscription(getDefaultBuilder(callback1).build()) + executor.runAllReady() + conditions.forEach( + Consumer { condition: Condition -> Mockito.verify(condition).addCallback(any()) } + ) + + val callback2: Monitor.Callback = mock() + conditionMonitor.addSubscription(getDefaultBuilder(callback2).build()) + executor.runAllReady() + conditions.forEach( + Consumer { condition: Condition -> + Mockito.verify(condition, Mockito.times(1)).addCallback(any()) + } + ) + } + + @Test + fun addCallback_addFirstCallback_reportWithDefaultValue() = + kosmos.runTest { + val callback: Monitor.Callback = mock() + conditionMonitor.addSubscription(getDefaultBuilder(callback).build()) + executor.runAllReady() + Mockito.verify(callback).onConditionsChanged(false) + } + + @Test + fun addCallback_addSecondCallback_reportWithExistingValue() = + kosmos.runTest { + val callback1: Monitor.Callback = mock() + val condition: Condition = mock() + + whenever(condition.isConditionMet).thenReturn(true) + val monitor = Monitor(executor) + monitor.addSubscription( + Monitor.Subscription.Builder(callback1).addCondition(condition).build() + ) + + val callback2: Monitor.Callback = mock() + monitor.addSubscription( + Monitor.Subscription.Builder(callback2).addCondition(condition).build() + ) + executor.runAllReady() + Mockito.verify(callback2).onConditionsChanged(eq(true)) + } + + @Test + fun addCallback_noConditions_reportAllConditionsMet() = + kosmos.runTest { + val monitor = Monitor(executor) + val callback: Monitor.Callback = mock() + + monitor.addSubscription(Monitor.Subscription.Builder(callback).build()) + executor.runAllReady() + Mockito.verify(callback).onConditionsChanged(true) + } + + @Test + fun addCallback_preCondition_noConditions_reportAllConditionsMet() = + kosmos.runTest { + val monitor = Monitor(executor, HashSet<Condition?>(Arrays.asList(condition1))) + val callback: Monitor.Callback = mock() + + monitor.addSubscription(Monitor.Subscription.Builder(callback).build()) + executor.runAllReady() + Mockito.verify(callback, Mockito.never()).onConditionsChanged(true) + condition1.fakeUpdateCondition(true) + executor.runAllReady() + Mockito.verify(callback).onConditionsChanged(true) + } + + @Test + fun removeCallback_noFailureOnDoubleRemove() = + kosmos.runTest { + val condition: Condition = mock() + val monitor = Monitor(executor) + val callback: Monitor.Callback = mock() + val token = + monitor.addSubscription( + Monitor.Subscription.Builder(callback).addCondition(condition).build() + ) + monitor.removeSubscription(token) + executor.runAllReady() + // Ensure second removal doesn't cause an exception. + monitor.removeSubscription(token) + executor.runAllReady() + } + + @Test + fun removeCallback_shouldNoLongerReceiveUpdate() = + kosmos.runTest { + val condition: Condition = mock() + val monitor = Monitor(executor) + val callback: Monitor.Callback = mock() + val token = + monitor.addSubscription( + Monitor.Subscription.Builder(callback).addCondition(condition).build() + ) + monitor.removeSubscription(token) + executor.runAllReady() + Mockito.clearInvocations(callback) + + val conditionCallbackCaptor = argumentCaptor<Condition.Callback>() + Mockito.verify(condition).addCallback(conditionCallbackCaptor.capture()) + + val conditionCallback = conditionCallbackCaptor.lastValue + Mockito.verify(condition).removeCallback(conditionCallback) + } + + @Test + fun removeCallback_removeLastCallback_removeCallbackFromAllConditions() = + kosmos.runTest { + val callback1: Monitor.Callback = mock() + val callback2: Monitor.Callback = mock() + val subscription1 = + conditionMonitor.addSubscription(getDefaultBuilder(callback1).build()) + val subscription2 = + conditionMonitor.addSubscription(getDefaultBuilder(callback2).build()) + + conditionMonitor.removeSubscription(subscription1) + executor.runAllReady() + conditions.forEach( + Consumer { condition: Condition -> + verify(condition, Mockito.never()).removeCallback(any()) + } + ) + + conditionMonitor.removeSubscription(subscription2) + executor.runAllReady() + conditions.forEach( + Consumer { condition: Condition -> Mockito.verify(condition).removeCallback(any()) } + ) + } + + @Test + fun updateCallbacks_allConditionsMet_reportTrue() = + kosmos.runTest { + val callback: Monitor.Callback = mock() + conditionMonitor.addSubscription(getDefaultBuilder(callback).build()) + Mockito.clearInvocations(callback) + + condition1.fakeUpdateCondition(true) + condition2.fakeUpdateCondition(true) + condition3.fakeUpdateCondition(true) + executor.runAllReady() + + Mockito.verify(callback).onConditionsChanged(true) + } + + @Test + fun updateCallbacks_oneConditionStoppedMeeting_reportFalse() = + kosmos.runTest { + val callback: Monitor.Callback = mock() + conditionMonitor.addSubscription(getDefaultBuilder(callback).build()) + + condition1.fakeUpdateCondition(true) + condition2.fakeUpdateCondition(true) + condition3.fakeUpdateCondition(true) + Mockito.clearInvocations(callback) + + condition1.fakeUpdateCondition(false) + executor.runAllReady() + Mockito.verify(callback).onConditionsChanged(false) + } + + @Test + fun updateCallbacks_shouldOnlyUpdateWhenValueChanges() = + kosmos.runTest { + val callback: Monitor.Callback = mock() + conditionMonitor.addSubscription(getDefaultBuilder(callback).build()) + executor.runAllReady() + Mockito.verify(callback).onConditionsChanged(false) + Mockito.clearInvocations(callback) + + condition1.fakeUpdateCondition(true) + executor.runAllReady() + Mockito.verify(callback, Mockito.never()).onConditionsChanged(anyBoolean()) + + condition2.fakeUpdateCondition(true) + executor.runAllReady() + Mockito.verify(callback, Mockito.never()).onConditionsChanged(anyBoolean()) + + condition3.fakeUpdateCondition(true) + executor.runAllReady() + Mockito.verify(callback).onConditionsChanged(true) + } + + @Test + fun clearCondition_shouldUpdateValue() = + kosmos.runTest { + condition1.fakeUpdateCondition(false) + condition2.fakeUpdateCondition(true) + condition3.fakeUpdateCondition(true) + + val callback: Monitor.Callback = mock() + conditionMonitor.addSubscription(getDefaultBuilder(callback).build()) + executor.runAllReady() + Mockito.verify(callback).onConditionsChanged(false) + + condition1.clearCondition() + executor.runAllReady() + Mockito.verify(callback).onConditionsChanged(true) + } + + @Test + fun unsetCondition_shouldNotAffectValue() = + kosmos.runTest { + val settableCondition = FakeCondition(testScope, null, false) + condition1.fakeUpdateCondition(true) + condition2.fakeUpdateCondition(true) + condition3.fakeUpdateCondition(true) + + val callback: Monitor.Callback = mock() + + conditionMonitor.addSubscription( + getDefaultBuilder(callback).addCondition(settableCondition).build() + ) + + executor.runAllReady() + Mockito.verify(callback).onConditionsChanged(true) + } + + @Test + fun setUnsetCondition_shouldAffectValue() = + kosmos.runTest { + val settableCondition = FakeCondition(testScope, null, false) + condition1.fakeUpdateCondition(true) + condition2.fakeUpdateCondition(true) + condition3.fakeUpdateCondition(true) + + val callback: Monitor.Callback = mock() + + conditionMonitor.addSubscription( + getDefaultBuilder(callback).addCondition(settableCondition).build() + ) + + executor.runAllReady() + Mockito.verify(callback).onConditionsChanged(true) + Mockito.clearInvocations(callback) + + settableCondition.fakeUpdateCondition(false) + executor.runAllReady() + Mockito.verify(callback).onConditionsChanged(false) + Mockito.clearInvocations(callback) + + settableCondition.clearCondition() + executor.runAllReady() + Mockito.verify(callback).onConditionsChanged(true) + } + + @Test + fun clearingOverridingCondition_shouldBeExcluded() = + kosmos.runTest { + val overridingCondition = FakeCondition(testScope, true, true) + condition1.fakeUpdateCondition(false) + condition2.fakeUpdateCondition(false) + condition3.fakeUpdateCondition(false) + + val callback: Monitor.Callback = mock() + + conditionMonitor.addSubscription( + getDefaultBuilder(callback).addCondition(overridingCondition).build() + ) + + executor.runAllReady() + Mockito.verify(callback).onConditionsChanged(true) + Mockito.clearInvocations(callback) + + overridingCondition.clearCondition() + executor.runAllReady() + Mockito.verify(callback).onConditionsChanged(false) + } + + @Test + fun settingUnsetOverridingCondition_shouldBeIncluded() = + kosmos.runTest { + val overridingCondition = FakeCondition(testScope, null, true) + condition1.fakeUpdateCondition(false) + condition2.fakeUpdateCondition(false) + condition3.fakeUpdateCondition(false) + + val callback: Monitor.Callback = mock() + + conditionMonitor.addSubscription( + getDefaultBuilder(callback).addCondition(overridingCondition).build() + ) + + executor.runAllReady() + Mockito.verify(callback).onConditionsChanged(false) + Mockito.clearInvocations(callback) + + overridingCondition.fakeUpdateCondition(true) + executor.runAllReady() + Mockito.verify(callback).onConditionsChanged(true) + } + + /** + * Ensures that the result of a condition being true leads to its nested condition being + * activated. + */ + @Test + fun testNestedCondition() = + kosmos.runTest { + condition1.fakeUpdateCondition(false) + val callback: Monitor.Callback = mock() + + condition2.fakeUpdateCondition(false) + + // Create a nested condition + conditionMonitor.addSubscription( + Monitor.Subscription.Builder( + Monitor.Subscription.Builder(callback).addCondition(condition2).build() + ) + .addCondition(condition1) + .build() + ) + + executor.runAllReady() + + // Ensure the nested condition callback is not called at all. + Mockito.verify(callback, Mockito.never()).onActiveChanged(anyBoolean()) + Mockito.verify(callback, Mockito.never()).onConditionsChanged(anyBoolean()) + + // Update the inner condition to true and ensure that the nested condition is not + // triggered. + condition2.fakeUpdateCondition(true) + Mockito.verify(callback, Mockito.never()).onConditionsChanged(anyBoolean()) + condition2.fakeUpdateCondition(false) + + // Set outer condition and make sure the inner condition becomes active and reports that + // conditions aren't met + condition1.fakeUpdateCondition(true) + executor.runAllReady() + + Mockito.verify(callback).onActiveChanged(eq(true)) + Mockito.verify(callback).onConditionsChanged(eq(false)) + + Mockito.clearInvocations(callback) + + // Update the inner condition and make sure the callback is updated. + condition2.fakeUpdateCondition(true) + executor.runAllReady() + + Mockito.verify(callback).onConditionsChanged(true) + + Mockito.clearInvocations(callback) + // Invalidate outer condition and make sure callback is informed, but the last state is + // not affected. + condition1.fakeUpdateCondition(false) + executor.runAllReady() + + Mockito.verify(callback).onActiveChanged(eq(false)) + Mockito.verify(callback, Mockito.never()).onConditionsChanged(anyBoolean()) + } + + /** Ensure preconditions are applied to every subscription added to a monitor. */ + @Test + fun testPreconditionMonitor() { + val callback: Monitor.Callback = mock() + + condition2.fakeUpdateCondition(true) + val monitor = Monitor(executor, HashSet<Condition?>(listOf(condition1))) + + monitor.addSubscription( + Monitor.Subscription.Builder(callback).addCondition(condition2).build() + ) + + executor.runAllReady() + + Mockito.verify(callback, Mockito.never()).onActiveChanged(anyBoolean()) + Mockito.verify(callback, Mockito.never()).onConditionsChanged(anyBoolean()) + + condition1.fakeUpdateCondition(true) + executor.runAllReady() + + Mockito.verify(callback).onActiveChanged(eq(true)) + Mockito.verify(callback).onConditionsChanged(eq(true)) + } + + @Test + fun testLoggingCallback() = + kosmos.runTest { + val monitor = Monitor(executor, emptySet(), logBuffer) + + val condition = FakeCondition(testScope) + val overridingCondition = + FakeCondition(testScope, /* initialValue= */ false, /* overriding= */ true) + + val callback: Monitor.Callback = mock() + monitor.addSubscription( + getDefaultBuilder(callback) + .addCondition(condition) + .addCondition(overridingCondition) + .build() + ) + executor.runAllReady() + + // condition set to true + condition.fakeUpdateCondition(true) + executor.runAllReady() + Mockito.verify(logBuffer).logChange("", "FakeCondition", "True") + + // condition set to false + condition.fakeUpdateCondition(false) + executor.runAllReady() + Mockito.verify(logBuffer).logChange("", "FakeCondition", "False") + + // condition unset + condition.fakeClearCondition() + executor.runAllReady() + Mockito.verify(logBuffer).logChange("", "FakeCondition", "Invalid") + + // overriding condition set to true + overridingCondition.fakeUpdateCondition(true) + executor.runAllReady() + Mockito.verify(logBuffer).logChange("", "FakeCondition[OVRD]", "True") + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/condition/ConditionTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/condition/ConditionTest.java deleted file mode 100644 index a224843e592a..000000000000 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/condition/ConditionTest.java +++ /dev/null @@ -1,163 +0,0 @@ -/* - * 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 static com.google.common.truth.Truth.assertThat; - -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.filters.SmallTest; - -import com.android.systemui.SysuiTestCase; - -import kotlinx.coroutines.CoroutineScope; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -@SmallTest -@RunWith(AndroidJUnit4.class) -public class ConditionTest extends SysuiTestCase { - @Mock - CoroutineScope mScope; - - private FakeCondition mCondition; - - @Before - public void setup() { - MockitoAnnotations.initMocks(this); - mCondition = spy(new FakeCondition(mScope)); - } - - @Test - public void addCallback_addFirstCallback_triggerStart() { - final Condition.Callback callback = mock( - Condition.Callback.class); - mCondition.addCallback(callback); - verify(mCondition).start(); - } - - @Test - public void addCallback_addMultipleCallbacks_triggerStartOnlyOnce() { - final Condition.Callback callback1 = mock( - Condition.Callback.class); - final Condition.Callback callback2 = mock( - Condition.Callback.class); - final Condition.Callback callback3 = mock( - Condition.Callback.class); - - mCondition.addCallback(callback1); - mCondition.addCallback(callback2); - mCondition.addCallback(callback3); - - verify(mCondition, times(1)).start(); - } - - @Test - public void addCallback_alreadyStarted_triggerUpdate() { - final Condition.Callback callback1 = mock( - Condition.Callback.class); - mCondition.addCallback(callback1); - - mCondition.fakeUpdateCondition(true); - - final Condition.Callback callback2 = mock( - Condition.Callback.class); - mCondition.addCallback(callback2); - verify(callback2).onConditionChanged(mCondition); - assertThat(mCondition.isConditionMet()).isTrue(); - } - - @Test - public void removeCallback_removeLastCallback_triggerStop() { - final Condition.Callback callback = mock( - Condition.Callback.class); - mCondition.addCallback(callback); - verify(mCondition, never()).stop(); - - mCondition.removeCallback(callback); - verify(mCondition).stop(); - } - - @Test - public void updateCondition_falseToTrue_reportTrue() { - mCondition.fakeUpdateCondition(false); - - final Condition.Callback callback = mock( - Condition.Callback.class); - mCondition.addCallback(callback); - - mCondition.fakeUpdateCondition(true); - verify(callback).onConditionChanged(eq(mCondition)); - assertThat(mCondition.isConditionMet()).isTrue(); - } - - @Test - public void updateCondition_trueToFalse_reportFalse() { - mCondition.fakeUpdateCondition(true); - - final Condition.Callback callback = mock( - Condition.Callback.class); - mCondition.addCallback(callback); - - mCondition.fakeUpdateCondition(false); - verify(callback).onConditionChanged(eq(mCondition)); - assertThat(mCondition.isConditionMet()).isFalse(); - } - - @Test - public void updateCondition_trueToTrue_reportNothing() { - mCondition.fakeUpdateCondition(true); - - final Condition.Callback callback = mock( - Condition.Callback.class); - mCondition.addCallback(callback); - - mCondition.fakeUpdateCondition(true); - verify(callback, never()).onConditionChanged(eq(mCondition)); - } - - @Test - public void updateCondition_falseToFalse_reportNothing() { - mCondition.fakeUpdateCondition(false); - - final Condition.Callback callback = mock( - Condition.Callback.class); - mCondition.addCallback(callback); - - mCondition.fakeUpdateCondition(false); - verify(callback, never()).onConditionChanged(eq(mCondition)); - } - - @Test - public void clearCondition_reportsNotSet() { - mCondition.fakeUpdateCondition(false); - assertThat(mCondition.isConditionSet()).isTrue(); - mCondition.clearCondition(); - assertThat(mCondition.isConditionSet()).isFalse(); - } -} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/condition/ConditionTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/condition/ConditionTest.kt new file mode 100644 index 000000000000..f72185b72de6 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/condition/ConditionTest.kt @@ -0,0 +1,154 @@ +/* + * 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 androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.runCurrent +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.testScope +import com.google.common.truth.Truth +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito +import org.mockito.Mockito.mock +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock + +@SmallTest +@RunWith(AndroidJUnit4::class) +class ConditionTest : SysuiTestCase() { + private val kosmos = Kosmos() + private lateinit var underTest: FakeCondition + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + underTest = Mockito.spy(FakeCondition(kosmos.testScope)) + } + + @Test + fun addCallback_addFirstCallback_triggerStart() = + kosmos.runTest { + val callback = mock<Condition.Callback>() + underTest.addCallback(callback) + runCurrent() + Mockito.verify(underTest).start() + } + + @Test + fun addCallback_addMultipleCallbacks_triggerStartOnlyOnce() = + kosmos.runTest { + val callback1 = mock<Condition.Callback>() + val callback2 = mock<Condition.Callback>() + val callback3 = mock<Condition.Callback>() + + underTest.addCallback(callback1) + underTest.addCallback(callback2) + underTest.addCallback(callback3) + + runCurrent() + Mockito.verify(underTest).start() + } + + @Test + fun addCallback_alreadyStarted_triggerUpdate() = + kosmos.runTest { + val callback1 = mock<Condition.Callback>() + underTest.addCallback(callback1) + + underTest.fakeUpdateCondition(true) + + val callback2 = mock<Condition.Callback>() + underTest.addCallback(callback2) + Mockito.verify(callback2).onConditionChanged(underTest) + Truth.assertThat(underTest.isConditionMet).isTrue() + } + + @Test + fun removeCallback_removeLastCallback_triggerStop() = + kosmos.runTest { + val callback = mock<Condition.Callback>() + underTest.addCallback(callback) + Mockito.verify(underTest, Mockito.never()).stop() + + underTest.removeCallback(callback) + Mockito.verify(underTest).stop() + } + + @Test + fun updateCondition_falseToTrue_reportTrue() = + kosmos.runTest { + underTest.fakeUpdateCondition(false) + + val callback = mock<Condition.Callback>() + underTest.addCallback(callback) + + underTest.fakeUpdateCondition(true) + Mockito.verify(callback).onConditionChanged(eq(underTest)) + Truth.assertThat(underTest.isConditionMet).isTrue() + } + + @Test + fun updateCondition_trueToFalse_reportFalse() = + kosmos.runTest { + underTest.fakeUpdateCondition(true) + + val callback = mock<Condition.Callback>() + underTest.addCallback(callback) + + underTest.fakeUpdateCondition(false) + Mockito.verify(callback).onConditionChanged(eq(underTest)) + Truth.assertThat(underTest.isConditionMet).isFalse() + } + + @Test + fun updateCondition_trueToTrue_reportNothing() = + kosmos.runTest { + underTest.fakeUpdateCondition(true) + + val callback = mock<Condition.Callback>() + underTest.addCallback(callback) + + underTest.fakeUpdateCondition(true) + Mockito.verify(callback, Mockito.never()).onConditionChanged(eq(underTest)) + } + + @Test + fun updateCondition_falseToFalse_reportNothing() = + kosmos.runTest { + underTest.fakeUpdateCondition(false) + + val callback = mock<Condition.Callback>() + underTest.addCallback(callback) + + underTest.fakeUpdateCondition(false) + Mockito.verify(callback, Mockito.never()).onConditionChanged(eq(underTest)) + } + + @Test + fun clearCondition_reportsNotSet() = + kosmos.runTest { + underTest.fakeUpdateCondition(false) + Truth.assertThat(underTest.isConditionSet).isTrue() + underTest.clearCondition() + Truth.assertThat(underTest.isConditionSet).isFalse() + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/condition/FakeCondition.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/condition/FakeCondition.java deleted file mode 100644 index da660e2f6009..000000000000 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/condition/FakeCondition.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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 kotlinx.coroutines.CoroutineScope; - -/** - * Fake implementation of {@link Condition}, and provides a way for tests to update - * condition fulfillment. - */ -public class FakeCondition extends Condition { - FakeCondition(CoroutineScope scope) { - super(scope); - } - - FakeCondition(CoroutineScope scope, Boolean initialValue, boolean overriding) { - super(scope, initialValue, overriding); - } - - @Override - public void start() { - } - - @Override - public void stop() { - } - - @Override - public int getStartStrategy() { - return START_EAGERLY; - } - - public void fakeUpdateCondition(boolean isConditionMet) { - updateCondition(isConditionMet); - } - - public void fakeClearCondition() { - clearCondition(); - } -} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/condition/FakeCondition.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/condition/FakeCondition.kt new file mode 100644 index 000000000000..340249c269b1 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/condition/FakeCondition.kt @@ -0,0 +1,46 @@ +/* + * 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 kotlinx.coroutines.CoroutineScope + +/** + * Fake implementation of [Condition], and provides a way for tests to update condition fulfillment. + */ +class FakeCondition : Condition { + constructor(scope: CoroutineScope) : super(scope) + + constructor( + scope: CoroutineScope, + initialValue: Boolean?, + overriding: Boolean, + ) : super(scope, initialValue, overriding) + + public override suspend fun start() {} + + public override fun stop() {} + + override val startStrategy: Int + get() = START_EAGERLY + + fun fakeUpdateCondition(isConditionMet: Boolean) { + updateCondition(isConditionMet) + } + + fun fakeClearCondition() { + clearCondition() + } +} diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/condition/CombinedCondition.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/condition/CombinedCondition.kt index f276a82baaef..c30580fa01e7 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/condition/CombinedCondition.kt +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/condition/CombinedCondition.kt @@ -37,51 +37,43 @@ import kotlinx.coroutines.launch * @param operand The [Evaluator.ConditionOperand] to apply to the conditions. */ @OptIn(ExperimentalCoroutinesApi::class) -class CombinedCondition -constructor( +class CombinedCondition( private val scope: CoroutineScope, private val conditions: Collection<Condition>, @Evaluator.ConditionOperand private val operand: Int, ) : Condition(scope, null, false) { - private var job: Job? = null private val _startStrategy by lazy { calculateStartStrategy() } - override fun start() { - job = - scope.launch { - val groupedConditions = conditions.groupBy { it.isOverridingCondition } + override suspend fun start() { + val groupedConditions = conditions.groupBy { it.isOverridingCondition } - lazilyEvaluate( - conditions = groupedConditions.getOrDefault(true, emptyList()), - filterUnknown = true, + 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, ) - .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) - } - } + } else { + flowOf(overriddenValue) + } + } + .collect { conditionMet -> + if (conditionMet == null) { + clearCondition() + } else { + updateCondition(conditionMet) + } } } - override fun stop() { - job?.cancel() - job = null - } + override fun stop() {} /** * Evaluates a list of conditions lazily with support for short-circuiting. Conditions are diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/condition/Condition.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/condition/Condition.kt index 69236b48ed51..44803bd22b45 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/condition/Condition.kt +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/condition/Condition.kt @@ -22,6 +22,8 @@ import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleOwner import java.lang.ref.WeakReference import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch /** * Base class for a condition that needs to be fulfilled in order for [Monitor] to inform its @@ -43,11 +45,12 @@ protected constructor( ) { private val mTag: String = javaClass.simpleName - private val _callbacks = mutableListOf<WeakReference<Callback>>() - private var _started = false + private val callbacks = mutableListOf<WeakReference<Callback>>() + private var started = false + private var currentJob: Job? = null /** Starts monitoring the condition. */ - protected abstract fun start() + protected abstract suspend fun start() /** Stops monitoring the condition. */ protected abstract fun stop() @@ -64,21 +67,21 @@ protected constructor( */ fun addCallback(callback: Callback) { if (shouldLog()) Log.d(mTag, "adding callback") - _callbacks.add(WeakReference(callback)) + callbacks.add(WeakReference(callback)) - if (_started) { + if (started) { callback.onConditionChanged(this) return } - start() - _started = true + currentJob = _scope.launch { 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() + val iterator = callbacks.iterator() while (iterator.hasNext()) { val cb = iterator.next().get() if (cb == null || cb === callback) { @@ -86,12 +89,15 @@ protected constructor( } } - if (_callbacks.isNotEmpty() || !_started) { + if (callbacks.isNotEmpty() || !started) { return } stop() - _started = false + currentJob?.cancel() + currentJob = null + + started = false } /** @@ -151,7 +157,7 @@ protected constructor( } private fun sendUpdate() { - val iterator = _callbacks.iterator() + val iterator = callbacks.iterator() while (iterator.hasNext()) { val cb = iterator.next().get() if (cb == null) { diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/condition/ConditionExtensions.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/condition/ConditionExtensions.kt index 0f535cdfa5cc..849b3d6d8d8b 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/condition/ConditionExtensions.kt +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/condition/ConditionExtensions.kt @@ -19,7 +19,7 @@ fun Flow<Boolean>.toCondition( return object : Condition(scope, initialValue, false) { var job: Job? = null - override fun start() { + override suspend fun start() { job = scope.launch { collect { updateCondition(it) } } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/DeviceInactiveCondition.java b/packages/SystemUI/src/com/android/systemui/communal/DeviceInactiveCondition.java deleted file mode 100644 index 4be9601f4277..000000000000 --- a/packages/SystemUI/src/com/android/systemui/communal/DeviceInactiveCondition.java +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright (C) 2024 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.communal; - -import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_ASLEEP; - -import com.android.keyguard.KeyguardUpdateMonitor; -import com.android.keyguard.KeyguardUpdateMonitorCallback; -import com.android.systemui.dagger.qualifiers.Application; -import com.android.systemui.keyguard.WakefulnessLifecycle; -import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; -import com.android.systemui.keyguard.shared.model.DozeStateModel; -import com.android.systemui.shared.condition.Condition; -import com.android.systemui.statusbar.policy.KeyguardStateController; -import com.android.systemui.util.kotlin.JavaAdapter; - -import kotlinx.coroutines.CoroutineScope; -import kotlinx.coroutines.Job; - -import javax.inject.Inject; - -/** - * Condition which estimates device inactivity in order to avoid launching a full-screen activity - * while the user is actively using the device. - */ -public class DeviceInactiveCondition extends Condition { - private final KeyguardStateController mKeyguardStateController; - private final WakefulnessLifecycle mWakefulnessLifecycle; - private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; - private final KeyguardInteractor mKeyguardInteractor; - private final JavaAdapter mJavaAdapter; - private Job mAnyDozeListenerJob; - private boolean mAnyDoze; - private final KeyguardStateController.Callback mKeyguardStateCallback = - new KeyguardStateController.Callback() { - @Override - public void onKeyguardShowingChanged() { - updateState(); - } - }; - private final WakefulnessLifecycle.Observer mWakefulnessObserver = - new WakefulnessLifecycle.Observer() { - @Override - public void onStartedGoingToSleep() { - updateState(); - } - }; - private final KeyguardUpdateMonitorCallback mKeyguardUpdateCallback = - new KeyguardUpdateMonitorCallback() { - @Override - public void onDreamingStateChanged(boolean dreaming) { - updateState(); - } - }; - - @Inject - public DeviceInactiveCondition(@Application CoroutineScope scope, - KeyguardStateController keyguardStateController, - WakefulnessLifecycle wakefulnessLifecycle, KeyguardUpdateMonitor keyguardUpdateMonitor, - KeyguardInteractor keyguardInteractor, JavaAdapter javaAdapter) { - super(scope); - mKeyguardStateController = keyguardStateController; - mWakefulnessLifecycle = wakefulnessLifecycle; - mKeyguardUpdateMonitor = keyguardUpdateMonitor; - mKeyguardInteractor = keyguardInteractor; - mJavaAdapter = javaAdapter; - } - - @Override - protected void start() { - updateState(); - mKeyguardStateController.addCallback(mKeyguardStateCallback); - mKeyguardUpdateMonitor.registerCallback(mKeyguardUpdateCallback); - mWakefulnessLifecycle.addObserver(mWakefulnessObserver); - mAnyDozeListenerJob = mJavaAdapter.alwaysCollectFlow( - mKeyguardInteractor.getDozeTransitionModel(), dozeModel -> { - mAnyDoze = !DozeStateModel.Companion.isDozeOff(dozeModel.getTo()); - updateState(); - }); - } - - @Override - protected void stop() { - mKeyguardStateController.removeCallback(mKeyguardStateCallback); - mKeyguardUpdateMonitor.removeCallback(mKeyguardUpdateCallback); - mWakefulnessLifecycle.removeObserver(mWakefulnessObserver); - mAnyDozeListenerJob.cancel(null); - } - - @Override - public int getStartStrategy() { - return START_EAGERLY; - } - - private void updateState() { - final boolean asleep = mWakefulnessLifecycle.getWakefulness() == WAKEFULNESS_ASLEEP; - // Doze/AoD is also a dream, but we should never override it with low light as to the user - // it's totally unrelated. - updateCondition(!mAnyDoze && (asleep || mKeyguardStateController.isShowing() - || mKeyguardUpdateMonitor.isDreaming())); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/communal/DeviceInactiveCondition.kt b/packages/SystemUI/src/com/android/systemui/communal/DeviceInactiveCondition.kt new file mode 100644 index 000000000000..11c285b3e4e9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/DeviceInactiveCondition.kt @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2024 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.communal + +import com.android.keyguard.KeyguardUpdateMonitor +import com.android.keyguard.KeyguardUpdateMonitorCallback +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.keyguard.WakefulnessLifecycle +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.keyguard.shared.model.DozeStateModel.Companion.isDozeOff +import com.android.systemui.keyguard.shared.model.DozeTransitionModel +import com.android.systemui.shared.condition.Condition +import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.util.kotlin.JavaAdapter +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job + +/** + * Condition which estimates device inactivity in order to avoid launching a full-screen activity + * while the user is actively using the device. + */ +class DeviceInactiveCondition +@Inject +constructor( + @Application scope: CoroutineScope, + private val keyguardStateController: KeyguardStateController, + private val wakefulnessLifecycle: WakefulnessLifecycle, + private val keyguardUpdateMonitor: KeyguardUpdateMonitor, + private val keyguardInteractor: KeyguardInteractor, + private val javaAdapter: JavaAdapter, +) : Condition(scope) { + private var anyDozeListenerJob: Job? = null + private var anyDoze = false + private val keyguardStateCallback: KeyguardStateController.Callback = + object : KeyguardStateController.Callback { + override fun onKeyguardShowingChanged() { + updateState() + } + } + private val wakefulnessObserver: WakefulnessLifecycle.Observer = + object : WakefulnessLifecycle.Observer { + override fun onStartedGoingToSleep() { + updateState() + } + } + private val keyguardUpdateCallback: KeyguardUpdateMonitorCallback = + object : KeyguardUpdateMonitorCallback() { + override fun onDreamingStateChanged(dreaming: Boolean) { + updateState() + } + } + + override suspend fun start() { + updateState() + keyguardStateController.addCallback(keyguardStateCallback) + keyguardUpdateMonitor.registerCallback(keyguardUpdateCallback) + wakefulnessLifecycle.addObserver(wakefulnessObserver) + anyDozeListenerJob = + javaAdapter.alwaysCollectFlow(keyguardInteractor.dozeTransitionModel) { + dozeModel: DozeTransitionModel -> + anyDoze = !isDozeOff(dozeModel.to) + updateState() + } + } + + override fun stop() { + keyguardStateController.removeCallback(keyguardStateCallback) + keyguardUpdateMonitor.removeCallback(keyguardUpdateCallback) + wakefulnessLifecycle.removeObserver(wakefulnessObserver) + anyDozeListenerJob?.cancel(null) + } + + override val startStrategy: Int + get() = START_EAGERLY + + private fun updateState() { + val asleep = wakefulnessLifecycle.wakefulness == WakefulnessLifecycle.WAKEFULNESS_ASLEEP + // Doze/AoD is also a dream, but we should never override it with low light as to the user + // it's totally unrelated. + updateCondition( + !anyDoze && + (asleep || keyguardStateController.isShowing || keyguardUpdateMonitor.isDreaming) + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/dreams/conditions/AssistantAttentionCondition.java b/packages/SystemUI/src/com/android/systemui/dreams/conditions/AssistantAttentionCondition.java deleted file mode 100644 index c17094b7456c..000000000000 --- a/packages/SystemUI/src/com/android/systemui/dreams/conditions/AssistantAttentionCondition.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * 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.dreams.conditions; - -import com.android.systemui.assist.AssistManager; -import com.android.systemui.assist.AssistManager.VisualQueryAttentionListener; -import com.android.systemui.dagger.qualifiers.Application; -import com.android.systemui.shared.condition.Condition; - -import kotlinx.coroutines.CoroutineScope; - -import javax.inject.Inject; - -/** - * {@link AssistantAttentionCondition} provides a signal when assistant has the user's attention. - */ -public class AssistantAttentionCondition extends Condition { - private final AssistManager mAssistManager; - - private final VisualQueryAttentionListener mVisualQueryAttentionListener = - new VisualQueryAttentionListener() { - @Override - public void onAttentionGained() { - updateCondition(true); - } - - @Override - public void onAttentionLost() { - updateCondition(false); - } - }; - - @Inject - public AssistantAttentionCondition( - @Application CoroutineScope scope, - AssistManager assistManager) { - super(scope); - mAssistManager = assistManager; - } - - @Override - protected void start() { - mAssistManager.addVisualQueryAttentionListener(mVisualQueryAttentionListener); - } - - @Override - protected void stop() { - mAssistManager.removeVisualQueryAttentionListener(mVisualQueryAttentionListener); - } - - @Override - public int getStartStrategy() { - return START_EAGERLY; - } -} diff --git a/packages/SystemUI/src/com/android/systemui/dreams/conditions/AssistantAttentionCondition.kt b/packages/SystemUI/src/com/android/systemui/dreams/conditions/AssistantAttentionCondition.kt new file mode 100644 index 000000000000..838163b1d80a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dreams/conditions/AssistantAttentionCondition.kt @@ -0,0 +1,51 @@ +/* + * 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.dreams.conditions + +import com.android.systemui.assist.AssistManager +import com.android.systemui.assist.AssistManager.VisualQueryAttentionListener +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.shared.condition.Condition +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope + +/** [AssistantAttentionCondition] provides a signal when assistant has the user's attention. */ +class AssistantAttentionCondition +@Inject +constructor(@Application scope: CoroutineScope, private val assistManager: AssistManager) : + Condition(scope) { + private val visualQueryAttentionListener: VisualQueryAttentionListener = + object : VisualQueryAttentionListener { + override fun onAttentionGained() { + updateCondition(true) + } + + override fun onAttentionLost() { + updateCondition(false) + } + } + + override suspend fun start() { + assistManager.addVisualQueryAttentionListener(visualQueryAttentionListener) + } + + public override fun stop() { + assistManager.removeVisualQueryAttentionListener(visualQueryAttentionListener) + } + + override val startStrategy: Int + get() = START_EAGERLY +} diff --git a/packages/SystemUI/src/com/android/systemui/dreams/conditions/DreamCondition.java b/packages/SystemUI/src/com/android/systemui/dreams/conditions/DreamCondition.java deleted file mode 100644 index fb4ed1419688..000000000000 --- a/packages/SystemUI/src/com/android/systemui/dreams/conditions/DreamCondition.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * 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.dreams.conditions; - -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 kotlinx.coroutines.CoroutineScope; - -import javax.inject.Inject; - -/** - * {@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 - public void onDreamingStateChanged(boolean dreaming) { - updateCondition(dreaming); - } - }; - - @Inject - public DreamCondition( - @Application CoroutineScope scope, - DreamManager dreamManager, - KeyguardUpdateMonitor monitor) { - super(scope); - mDreamManager = dreamManager; - mUpdateMonitor = monitor; - } - - @Override - protected void start() { - mUpdateMonitor.registerCallback(mUpdateCallback); - updateCondition(mDreamManager.isDreaming()); - } - - @Override - protected void stop() { - mUpdateMonitor.removeCallback(mUpdateCallback); - } - - @Override - public int getStartStrategy() { - return START_EAGERLY; - } -} diff --git a/packages/SystemUI/src/com/android/systemui/dreams/conditions/DreamCondition.kt b/packages/SystemUI/src/com/android/systemui/dreams/conditions/DreamCondition.kt new file mode 100644 index 000000000000..6968132b7da3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dreams/conditions/DreamCondition.kt @@ -0,0 +1,52 @@ +/* + * 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.dreams.conditions + +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 + +/** [DreamCondition] provides a signal when a dream begins and ends. */ +class DreamCondition +@Inject +constructor( + @Application scope: CoroutineScope, + private val _dreamManager: DreamManager, + private val _updateMonitor: KeyguardUpdateMonitor, +) : Condition(scope) { + private val _updateCallback: KeyguardUpdateMonitorCallback = + object : KeyguardUpdateMonitorCallback() { + override fun onDreamingStateChanged(dreaming: Boolean) { + updateCondition(dreaming) + } + } + + override suspend fun start() { + _updateMonitor.registerCallback(_updateCallback) + updateCondition(_dreamManager.isDreaming) + } + + override fun stop() { + _updateMonitor.removeCallback(_updateCallback) + } + + override val startStrategy: Int + get() = START_EAGERLY +} diff --git a/packages/SystemUI/src/com/android/systemui/lowlightclock/AmbientLightModeMonitor.kt b/packages/SystemUI/src/com/android/systemui/lowlightclock/AmbientLightModeMonitor.kt index 9e32dd8d74ae..c1658e1f1694 100644 --- a/packages/SystemUI/src/com/android/systemui/lowlightclock/AmbientLightModeMonitor.kt +++ b/packages/SystemUI/src/com/android/systemui/lowlightclock/AmbientLightModeMonitor.kt @@ -122,7 +122,7 @@ constructor( } /** Interface of the ambient light mode callback, which gets triggered when the mode changes. */ - interface Callback { + fun interface Callback { fun onChange(@AmbientLightMode mode: Int) } diff --git a/packages/SystemUI/src/com/android/systemui/lowlightclock/DirectBootCondition.kt b/packages/SystemUI/src/com/android/systemui/lowlightclock/DirectBootCondition.kt index 57d709835b2c..cdac61cea10b 100644 --- a/packages/SystemUI/src/com/android/systemui/lowlightclock/DirectBootCondition.kt +++ b/packages/SystemUI/src/com/android/systemui/lowlightclock/DirectBootCondition.kt @@ -45,7 +45,7 @@ constructor( .cancellable() .distinctUntilChanged() - override fun start() { + override suspend fun start() { job = coroutineScope.launch { directBootFlow.collect { updateCondition(it) } } updateCondition(!userManager.isUserUnlocked) } diff --git a/packages/SystemUI/src/com/android/systemui/lowlightclock/ForceLowLightCondition.java b/packages/SystemUI/src/com/android/systemui/lowlightclock/ForceLowLightCondition.java deleted file mode 100644 index 5ec81a9a94a1..000000000000 --- a/packages/SystemUI/src/com/android/systemui/lowlightclock/ForceLowLightCondition.java +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright (C) 2024 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.lowlightclock; - -import android.text.TextUtils; -import android.util.Log; - -import androidx.annotation.NonNull; - -import com.android.systemui.dagger.qualifiers.Application; -import com.android.systemui.shared.condition.Condition; -import com.android.systemui.statusbar.commandline.Command; -import com.android.systemui.statusbar.commandline.CommandRegistry; - -import kotlinx.coroutines.CoroutineScope; - -import java.io.PrintWriter; -import java.util.List; - -import javax.inject.Inject; - -/** - * This condition registers for and fulfills cmd shell commands to force a device into or out of - * low-light conditions. - */ -public class ForceLowLightCondition extends Condition { - /** - * Command root - */ - public static final String COMMAND_ROOT = "low-light"; - /** - * Command for forcing device into low light. - */ - public static final String COMMAND_ENABLE_LOW_LIGHT = "enable"; - - /** - * Command for preventing a device from entering low light. - */ - public static final String COMMAND_DISABLE_LOW_LIGHT = "disable"; - - /** - * Command for clearing previously forced low-light conditions. - */ - public static final String COMMAND_CLEAR_LOW_LIGHT = "clear"; - - private static final String TAG = "ForceLowLightCondition"; - private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); - - /** - * Default Constructor. - * - * @param commandRegistry command registry to register commands with. - */ - @Inject - public ForceLowLightCondition( - @Application CoroutineScope scope, - CommandRegistry commandRegistry - ) { - super(scope, null, true); - - if (DEBUG) { - Log.d(TAG, "registering commands"); - } - commandRegistry.registerCommand(COMMAND_ROOT, () -> new Command() { - @Override - public void execute(@NonNull PrintWriter pw, @NonNull List<String> args) { - if (args.size() != 1) { - pw.println("no command specified"); - help(pw); - return; - } - - final String cmd = args.get(0); - - if (TextUtils.equals(cmd, COMMAND_ENABLE_LOW_LIGHT)) { - logAndPrint(pw, "forcing low light"); - updateCondition(true); - } else if (TextUtils.equals(cmd, COMMAND_DISABLE_LOW_LIGHT)) { - logAndPrint(pw, "forcing to not enter low light"); - updateCondition(false); - } else if (TextUtils.equals(cmd, COMMAND_CLEAR_LOW_LIGHT)) { - logAndPrint(pw, "clearing any forced low light"); - clearCondition(); - } else { - pw.println("invalid command"); - help(pw); - } - } - - @Override - public void help(@NonNull PrintWriter pw) { - pw.println("Usage: adb shell cmd statusbar low-light <cmd>"); - pw.println("Supported commands:"); - pw.println(" - enable"); - pw.println(" forces device into low-light"); - pw.println(" - disable"); - pw.println(" forces device to not enter low-light"); - pw.println(" - clear"); - pw.println(" clears any previously forced state"); - } - - private void logAndPrint(PrintWriter pw, String message) { - pw.println(message); - if (DEBUG) { - Log.d(TAG, message); - } - } - }); - } - - @Override - protected void start() { - } - - @Override - protected void stop() { - } - - @Override - public int getStartStrategy() { - return START_EAGERLY; - } -} diff --git a/packages/SystemUI/src/com/android/systemui/lowlightclock/ForceLowLightCondition.kt b/packages/SystemUI/src/com/android/systemui/lowlightclock/ForceLowLightCondition.kt new file mode 100644 index 000000000000..f75894da2c85 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/lowlightclock/ForceLowLightCondition.kt @@ -0,0 +1,115 @@ +/* + * 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.lowlightclock + +import android.text.TextUtils +import android.util.Log +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.shared.condition.Condition +import com.android.systemui.statusbar.commandline.Command +import com.android.systemui.statusbar.commandline.CommandRegistry +import java.io.PrintWriter +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope + +/** + * This condition registers for and fulfills cmd shell commands to force a device into or out of + * low-light conditions. + */ +class ForceLowLightCondition +@Inject +constructor(@Application scope: CoroutineScope, commandRegistry: CommandRegistry) : + Condition(scope, null, true) { + /** + * Default Constructor. + * + * @param commandRegistry command registry to register commands with. + */ + init { + if (DEBUG) { + Log.d(TAG, "registering commands") + } + commandRegistry.registerCommand(COMMAND_ROOT) { + object : Command { + override fun execute(pw: PrintWriter, args: List<String>) { + if (args.size != 1) { + pw.println("no command specified") + help(pw) + return + } + + val cmd = args[0] + + if (TextUtils.equals(cmd, COMMAND_ENABLE_LOW_LIGHT)) { + logAndPrint(pw, "forcing low light") + updateCondition(true) + } else if (TextUtils.equals(cmd, COMMAND_DISABLE_LOW_LIGHT)) { + logAndPrint(pw, "forcing to not enter low light") + updateCondition(false) + } else if (TextUtils.equals(cmd, COMMAND_CLEAR_LOW_LIGHT)) { + logAndPrint(pw, "clearing any forced low light") + clearCondition() + } else { + pw.println("invalid command") + help(pw) + } + } + + override fun help(pw: PrintWriter) { + pw.println("Usage: adb shell cmd statusbar low-light <cmd>") + pw.println("Supported commands:") + pw.println(" - enable") + pw.println(" forces device into low-light") + pw.println(" - disable") + pw.println(" forces device to not enter low-light") + pw.println(" - clear") + pw.println(" clears any previously forced state") + } + + private fun logAndPrint(pw: PrintWriter, message: String) { + pw.println(message) + if (DEBUG) { + Log.d(TAG, message) + } + } + } + } + } + + override suspend fun start() {} + + override fun stop() {} + + override val startStrategy: Int + get() = START_EAGERLY + + companion object { + /** Command root */ + const val COMMAND_ROOT: String = "low-light" + + /** Command for forcing device into low light. */ + const val COMMAND_ENABLE_LOW_LIGHT: String = "enable" + + /** Command for preventing a device from entering low light. */ + const val COMMAND_DISABLE_LOW_LIGHT: String = "disable" + + /** Command for clearing previously forced low-light conditions. */ + const val COMMAND_CLEAR_LOW_LIGHT: String = "clear" + + private const val TAG = "ForceLowLightCondition" + private val DEBUG = Log.isLoggable(TAG, Log.DEBUG) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightCondition.java b/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightCondition.java deleted file mode 100644 index c1a24e7e020e..000000000000 --- a/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightCondition.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright (C) 2024 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.lowlightclock; - -import com.android.internal.logging.UiEventLogger; -import com.android.systemui.dagger.qualifiers.Application; -import com.android.systemui.shared.condition.Condition; - -import kotlinx.coroutines.CoroutineScope; - -import javax.inject.Inject; - -/** - * Condition for monitoring when the device enters and exits lowlight mode. - */ -public class LowLightCondition extends Condition { - private final AmbientLightModeMonitor mAmbientLightModeMonitor; - private final UiEventLogger mUiEventLogger; - - @Inject - public LowLightCondition(@Application CoroutineScope scope, - AmbientLightModeMonitor ambientLightModeMonitor, - UiEventLogger uiEventLogger) { - super(scope); - mAmbientLightModeMonitor = ambientLightModeMonitor; - mUiEventLogger = uiEventLogger; - } - - @Override - protected void start() { - mAmbientLightModeMonitor.start(this::onLowLightChanged); - } - - @Override - protected void stop() { - mAmbientLightModeMonitor.stop(); - - // Reset condition met to false. - updateCondition(false); - } - - @Override - public int getStartStrategy() { - // As this condition keeps the lowlight sensor active, it should only run when needed. - return START_WHEN_NEEDED; - } - - private void onLowLightChanged(int lowLightMode) { - if (lowLightMode == AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_UNDECIDED) { - // Ignore undecided mode changes. - return; - } - - final boolean isLowLight = lowLightMode == AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK; - if (isLowLight == isConditionMet()) { - // No change in condition, don't do anything. - return; - } - mUiEventLogger.log(isLowLight ? LowLightDockEvent.AMBIENT_LIGHT_TO_DARK - : LowLightDockEvent.AMBIENT_LIGHT_TO_LIGHT); - updateCondition(isLowLight); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightCondition.kt b/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightCondition.kt new file mode 100644 index 000000000000..a29c666ff792 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightCondition.kt @@ -0,0 +1,65 @@ +/* + * 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.lowlightclock + +import com.android.internal.logging.UiEventLogger +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.shared.condition.Condition +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope + +/** Condition for monitoring when the device enters and exits lowlight mode. */ +class LowLightCondition +@Inject +constructor( + @Application scope: CoroutineScope, + private val ambientLightModeMonitor: AmbientLightModeMonitor, + private val uiEventLogger: UiEventLogger, +) : Condition(scope) { + override suspend fun start() { + ambientLightModeMonitor.start { lowLightMode: Int -> onLowLightChanged(lowLightMode) } + } + + override fun stop() { + ambientLightModeMonitor.stop() + + // Reset condition met to false. + updateCondition(false) + } + + override val startStrategy: Int + get() = // As this condition keeps the lowlight sensor active, it should only run when + // needed. + START_WHEN_NEEDED + + private fun onLowLightChanged(lowLightMode: Int) { + if (lowLightMode == AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_UNDECIDED) { + // Ignore undecided mode changes. + return + } + + val isLowLight = lowLightMode == AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK + if (isLowLight == isConditionMet) { + // No change in condition, don't do anything. + return + } + uiEventLogger.log( + if (isLowLight) LowLightDockEvent.AMBIENT_LIGHT_TO_DARK + else LowLightDockEvent.AMBIENT_LIGHT_TO_LIGHT + ) + updateCondition(isLowLight) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/lowlightclock/ScreenSaverEnabledCondition.java b/packages/SystemUI/src/com/android/systemui/lowlightclock/ScreenSaverEnabledCondition.java deleted file mode 100644 index 81572554cb86..000000000000 --- a/packages/SystemUI/src/com/android/systemui/lowlightclock/ScreenSaverEnabledCondition.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright (C) 2024 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.lowlightclock; - -import android.content.res.Resources; -import android.database.ContentObserver; -import android.os.UserHandle; -import android.provider.Settings; -import android.util.Log; - -import com.android.systemui.dagger.qualifiers.Application; -import com.android.systemui.dagger.qualifiers.Main; -import com.android.systemui.shared.condition.Condition; -import com.android.systemui.util.settings.SecureSettings; - -import kotlinx.coroutines.CoroutineScope; - -import javax.inject.Inject; - -/** - * Condition for monitoring if the screensaver setting is enabled. - */ -public class ScreenSaverEnabledCondition extends Condition { - private static final String TAG = ScreenSaverEnabledCondition.class.getSimpleName(); - - private final boolean mScreenSaverEnabledByDefaultConfig; - private final SecureSettings mSecureSettings; - - private final ContentObserver mScreenSaverSettingObserver = new ContentObserver(null) { - @Override - public void onChange(boolean selfChange) { - updateScreenSaverEnabledSetting(); - } - }; - - @Inject - public ScreenSaverEnabledCondition(@Application CoroutineScope scope, @Main Resources resources, - SecureSettings secureSettings) { - super(scope); - mScreenSaverEnabledByDefaultConfig = resources.getBoolean( - com.android.internal.R.bool.config_dreamsEnabledByDefault); - mSecureSettings = secureSettings; - } - - @Override - protected void start() { - mSecureSettings.registerContentObserverForUserSync( - Settings.Secure.SCREENSAVER_ENABLED, - mScreenSaverSettingObserver, UserHandle.USER_CURRENT); - updateScreenSaverEnabledSetting(); - } - - @Override - protected void stop() { - mSecureSettings.unregisterContentObserverSync(mScreenSaverSettingObserver); - } - - @Override - public int getStartStrategy() { - return START_EAGERLY; - } - - private void updateScreenSaverEnabledSetting() { - final boolean enabled = mSecureSettings.getIntForUser( - Settings.Secure.SCREENSAVER_ENABLED, - mScreenSaverEnabledByDefaultConfig ? 1 : 0, - UserHandle.USER_CURRENT) != 0; - if (!enabled) { - Log.i(TAG, "Disabling low-light clock because screen saver has been disabled"); - } - updateCondition(enabled); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/lowlightclock/ScreenSaverEnabledCondition.kt b/packages/SystemUI/src/com/android/systemui/lowlightclock/ScreenSaverEnabledCondition.kt new file mode 100644 index 000000000000..f6b0f6276440 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/lowlightclock/ScreenSaverEnabledCondition.kt @@ -0,0 +1,81 @@ +/* + * 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.lowlightclock + +import android.content.res.Resources +import android.database.ContentObserver +import android.os.UserHandle +import android.provider.Settings +import android.util.Log +import com.android.internal.R +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.shared.condition.Condition +import com.android.systemui.util.settings.SecureSettings +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope + +/** Condition for monitoring if the screensaver setting is enabled. */ +class ScreenSaverEnabledCondition +@Inject +constructor( + @Application scope: CoroutineScope, + @Main resources: Resources, + private val secureSettings: SecureSettings, +) : Condition(scope) { + private val screenSaverEnabledByDefaultConfig = + resources.getBoolean(R.bool.config_dreamsEnabledByDefault) + + private val screenSaverSettingObserver: ContentObserver = + object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + updateScreenSaverEnabledSetting() + } + } + + public override suspend fun start() { + secureSettings.registerContentObserverForUserSync( + Settings.Secure.SCREENSAVER_ENABLED, + screenSaverSettingObserver, + UserHandle.USER_CURRENT, + ) + updateScreenSaverEnabledSetting() + } + + override fun stop() { + secureSettings.unregisterContentObserverSync(screenSaverSettingObserver) + } + + override val startStrategy: Int + get() = START_EAGERLY + + private fun updateScreenSaverEnabledSetting() { + val enabled = + secureSettings.getIntForUser( + Settings.Secure.SCREENSAVER_ENABLED, + if (screenSaverEnabledByDefaultConfig) 1 else 0, + UserHandle.USER_CURRENT, + ) != 0 + if (!enabled) { + Log.i(TAG, "Disabling low-light clock because screen saver has been disabled") + } + updateCondition(enabled) + } + + companion object { + private val TAG: String = ScreenSaverEnabledCondition::class.java.simpleName + } +} diff --git a/packages/SystemUI/src/com/android/systemui/process/condition/SystemProcessCondition.java b/packages/SystemUI/src/com/android/systemui/process/condition/SystemProcessCondition.java deleted file mode 100644 index ea2bf6a44884..000000000000 --- a/packages/SystemUI/src/com/android/systemui/process/condition/SystemProcessCondition.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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.process.condition; - -import com.android.systemui.dagger.qualifiers.Application; -import com.android.systemui.process.ProcessWrapper; -import com.android.systemui.shared.condition.Condition; - -import kotlinx.coroutines.CoroutineScope; - -import javax.inject.Inject; - -/** - * {@link SystemProcessCondition} checks to make sure the current process is being ran by the - * System User. - */ -public class SystemProcessCondition extends Condition { - private final ProcessWrapper mProcessWrapper; - - @Inject - public SystemProcessCondition(@Application CoroutineScope scope, - ProcessWrapper processWrapper) { - super(scope); - mProcessWrapper = processWrapper; - } - - @Override - protected void start() { - updateCondition(mProcessWrapper.isSystemUser()); - } - - @Override - protected void stop() { - } - - @Override - public int getStartStrategy() { - return START_EAGERLY; - } -} diff --git a/packages/SystemUI/src/com/android/systemui/process/condition/SystemProcessCondition.kt b/packages/SystemUI/src/com/android/systemui/process/condition/SystemProcessCondition.kt new file mode 100644 index 000000000000..9a424c3aab41 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/process/condition/SystemProcessCondition.kt @@ -0,0 +1,39 @@ +/* + * 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.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 + +/** + * [SystemProcessCondition] checks to make sure the current process is being ran by the System User. + */ +class SystemProcessCondition +@Inject +constructor(@Application scope: CoroutineScope, private val processWrapper: ProcessWrapper) : + Condition(scope) { + override val startStrategy: Int + get() = START_EAGERLY + + override suspend fun start() { + updateCondition(processWrapper.isSystemUser) + } + + override fun stop() {} +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/ForceLowLightConditionTest.java b/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/ForceLowLightConditionTest.java deleted file mode 100644 index 7297e0f3bff5..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/ForceLowLightConditionTest.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright (C) 2024 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.lowlightclock; - -import static com.google.common.truth.Truth.assertThat; - -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.verify; - -import android.testing.AndroidTestingRunner; - -import androidx.test.filters.SmallTest; - -import com.android.systemui.SysuiTestCase; -import com.android.systemui.shared.condition.Condition; -import com.android.systemui.statusbar.commandline.Command; -import com.android.systemui.statusbar.commandline.CommandRegistry; - -import kotlin.jvm.functions.Function0; - -import kotlinx.coroutines.CoroutineScope; - -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.io.PrintWriter; -import java.util.Arrays; - -@SmallTest -@RunWith(AndroidTestingRunner.class) -public class ForceLowLightConditionTest extends SysuiTestCase { - @Mock - private CommandRegistry mCommandRegistry; - - @Mock - private Condition.Callback mCallback; - - @Mock - private PrintWriter mPrintWriter; - - @Mock - CoroutineScope mScope; - - private ForceLowLightCondition mCondition; - private Command mCommand; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - mCondition = new ForceLowLightCondition(mScope, mCommandRegistry); - mCondition.addCallback(mCallback); - ArgumentCaptor<Function0<Command>> commandCaptor = - ArgumentCaptor.forClass(Function0.class); - verify(mCommandRegistry).registerCommand(eq(ForceLowLightCondition.COMMAND_ROOT), - commandCaptor.capture()); - mCommand = commandCaptor.getValue().invoke(); - } - - @Test - public void testEnableLowLight() { - mCommand.execute(mPrintWriter, - Arrays.asList(ForceLowLightCondition.COMMAND_ENABLE_LOW_LIGHT)); - verify(mCallback).onConditionChanged(mCondition); - assertThat(mCondition.isConditionSet()).isTrue(); - assertThat(mCondition.isConditionMet()).isTrue(); - } - - @Test - public void testDisableLowLight() { - mCommand.execute(mPrintWriter, - Arrays.asList(ForceLowLightCondition.COMMAND_DISABLE_LOW_LIGHT)); - verify(mCallback).onConditionChanged(mCondition); - assertThat(mCondition.isConditionSet()).isTrue(); - assertThat(mCondition.isConditionMet()).isFalse(); - } - - @Test - public void testClearEnableLowLight() { - mCommand.execute(mPrintWriter, - Arrays.asList(ForceLowLightCondition.COMMAND_ENABLE_LOW_LIGHT)); - verify(mCallback).onConditionChanged(mCondition); - assertThat(mCondition.isConditionSet()).isTrue(); - assertThat(mCondition.isConditionMet()).isTrue(); - Mockito.clearInvocations(mCallback); - mCommand.execute(mPrintWriter, - Arrays.asList(ForceLowLightCondition.COMMAND_CLEAR_LOW_LIGHT)); - verify(mCallback).onConditionChanged(mCondition); - assertThat(mCondition.isConditionSet()).isFalse(); - assertThat(mCondition.isConditionMet()).isFalse(); - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/ForceLowLightConditionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/ForceLowLightConditionTest.kt new file mode 100644 index 000000000000..f8ce242c39d2 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/ForceLowLightConditionTest.kt @@ -0,0 +1,98 @@ +/* + * 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.lowlightclock + +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.testScope +import com.android.systemui.shared.condition.Condition +import com.android.systemui.statusbar.commandline.Command +import com.android.systemui.statusbar.commandline.CommandRegistry +import com.google.common.truth.Truth +import java.io.PrintWriter +import java.util.Arrays +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.eq + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class ForceLowLightConditionTest : SysuiTestCase() { + private val kosmos = Kosmos() + + @Mock private lateinit var commandRegistry: CommandRegistry + + @Mock private lateinit var callback: Condition.Callback + + @Mock private lateinit var printWriter: PrintWriter + + private lateinit var condition: ForceLowLightCondition + private lateinit var command: Command + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + condition = ForceLowLightCondition(kosmos.testScope, commandRegistry) + condition.addCallback(callback) + val commandCaptor = argumentCaptor<() -> Command>() + Mockito.verify(commandRegistry) + .registerCommand(eq(ForceLowLightCondition.COMMAND_ROOT), commandCaptor.capture()) + command = commandCaptor.lastValue.invoke() + } + + @Test + fun testEnableLowLight() = + kosmos.runTest { + command.execute( + printWriter, + Arrays.asList(ForceLowLightCondition.COMMAND_ENABLE_LOW_LIGHT), + ) + Mockito.verify(callback).onConditionChanged(condition) + Truth.assertThat(condition.isConditionSet).isTrue() + Truth.assertThat(condition.isConditionMet).isTrue() + } + + @Test + fun testDisableLowLight() = + kosmos.runTest { + command.execute(printWriter, listOf(ForceLowLightCondition.COMMAND_DISABLE_LOW_LIGHT)) + Mockito.verify(callback).onConditionChanged(condition) + Truth.assertThat(condition.isConditionSet).isTrue() + Truth.assertThat(condition.isConditionMet).isFalse() + } + + @Test + fun testClearEnableLowLight() = + kosmos.runTest { + command.execute(printWriter, listOf(ForceLowLightCondition.COMMAND_ENABLE_LOW_LIGHT)) + Mockito.verify(callback).onConditionChanged(condition) + Truth.assertThat(condition.isConditionSet).isTrue() + Truth.assertThat(condition.isConditionMet).isTrue() + Mockito.clearInvocations(callback) + command.execute(printWriter, listOf(ForceLowLightCondition.COMMAND_CLEAR_LOW_LIGHT)) + Mockito.verify(callback).onConditionChanged(condition) + Truth.assertThat(condition.isConditionSet).isFalse() + Truth.assertThat(condition.isConditionMet).isFalse() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/LowLightConditionTest.java b/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/LowLightConditionTest.java deleted file mode 100644 index 2c216244985e..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/LowLightConditionTest.java +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright (C) 2024 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.lowlightclock; - -import static com.google.common.truth.Truth.assertThat; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -import android.testing.AndroidTestingRunner; - -import androidx.test.filters.SmallTest; - -import com.android.internal.logging.UiEventLogger; -import com.android.systemui.SysuiTestCase; - -import kotlinx.coroutines.CoroutineScope; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -@SmallTest -@RunWith(AndroidTestingRunner.class) -public class LowLightConditionTest extends SysuiTestCase { - @Mock - private AmbientLightModeMonitor mAmbientLightModeMonitor; - @Mock - private UiEventLogger mUiEventLogger; - @Mock - CoroutineScope mScope; - private LowLightCondition mCondition; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - - mCondition = new LowLightCondition(mScope, mAmbientLightModeMonitor, mUiEventLogger); - mCondition.start(); - } - - @Test - public void testLowLightFalse() { - changeLowLightMode(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_LIGHT); - assertThat(mCondition.isConditionMet()).isFalse(); - } - - @Test - public void testLowLightTrue() { - changeLowLightMode(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK); - assertThat(mCondition.isConditionMet()).isTrue(); - } - - @Test - public void testUndecidedLowLightStateIgnored() { - changeLowLightMode(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK); - assertThat(mCondition.isConditionMet()).isTrue(); - changeLowLightMode(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_UNDECIDED); - assertThat(mCondition.isConditionMet()).isTrue(); - } - - @Test - public void testLowLightChange() { - changeLowLightMode(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_LIGHT); - assertThat(mCondition.isConditionMet()).isFalse(); - changeLowLightMode(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK); - assertThat(mCondition.isConditionMet()).isTrue(); - } - - @Test - public void testResetIsConditionMetUponStop() { - changeLowLightMode(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK); - assertThat(mCondition.isConditionMet()).isTrue(); - - mCondition.stop(); - assertThat(mCondition.isConditionMet()).isFalse(); - } - - @Test - public void testLoggingAmbientLightNotLowToLow() { - changeLowLightMode(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK); - // Only logged once. - verify(mUiEventLogger, times(1)).log(any()); - // Logged with the correct state. - verify(mUiEventLogger).log(LowLightDockEvent.AMBIENT_LIGHT_TO_DARK); - } - - @Test - public void testLoggingAmbientLightLowToLow() { - changeLowLightMode(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK); - reset(mUiEventLogger); - - changeLowLightMode(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK); - // Doesn't log. - verify(mUiEventLogger, never()).log(any()); - } - - @Test - public void testLoggingAmbientLightNotLowToNotLow() { - changeLowLightMode(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_LIGHT); - // Doesn't log. - verify(mUiEventLogger, never()).log(any()); - } - - @Test - public void testLoggingAmbientLightLowToNotLow() { - changeLowLightMode(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK); - reset(mUiEventLogger); - - changeLowLightMode(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_LIGHT); - // Only logged once. - verify(mUiEventLogger).log(any()); - // Logged with the correct state. - verify(mUiEventLogger).log(LowLightDockEvent.AMBIENT_LIGHT_TO_LIGHT); - } - - private void changeLowLightMode(int mode) { - ArgumentCaptor<AmbientLightModeMonitor.Callback> ambientLightCallbackCaptor = - ArgumentCaptor.forClass(AmbientLightModeMonitor.Callback.class); - verify(mAmbientLightModeMonitor).start(ambientLightCallbackCaptor.capture()); - ambientLightCallbackCaptor.getValue().onChange(mode); - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/LowLightConditionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/LowLightConditionTest.kt new file mode 100644 index 000000000000..da68442a70cc --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/LowLightConditionTest.kt @@ -0,0 +1,157 @@ +/* + * 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.lowlightclock + +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.internal.logging.UiEventLogger +import com.android.systemui.SysuiTestCase +import com.android.systemui.condition.testStart +import com.android.systemui.condition.testStop +import com.android.systemui.kosmos.runCurrent +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import com.google.common.truth.Truth +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.argumentCaptor + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class LowLightConditionTest : SysuiTestCase() { + private val kosmos = testKosmos() + + @Mock private lateinit var ambientLightModeMonitor: AmbientLightModeMonitor + + @Mock private lateinit var uiEventLogger: UiEventLogger + + private lateinit var condition: LowLightCondition + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + condition = LowLightCondition(kosmos.testScope, ambientLightModeMonitor, uiEventLogger) + } + + @Test + fun testLowLightFalse() = + kosmos.runTest { + testStart(condition) + changeLowLightMode(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_LIGHT) + Truth.assertThat(condition.isConditionMet).isFalse() + } + + @Test + fun testLowLightTrue() = + kosmos.runTest { + testStart(condition) + changeLowLightMode(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK) + Truth.assertThat(condition.isConditionMet).isTrue() + } + + @Test + fun testUndecidedLowLightStateIgnored() = + kosmos.runTest { + testStart(condition) + changeLowLightMode(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK) + Truth.assertThat(condition.isConditionMet).isTrue() + changeLowLightMode(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_UNDECIDED) + Truth.assertThat(condition.isConditionMet).isTrue() + } + + @Test + fun testLowLightChange() = + kosmos.runTest { + testStart(condition) + changeLowLightMode(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_LIGHT) + Truth.assertThat(condition.isConditionMet).isFalse() + changeLowLightMode(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK) + Truth.assertThat(condition.isConditionMet).isTrue() + } + + @Test + fun testResetIsConditionMetUponStop() = + kosmos.runTest { + testStart(condition) + runCurrent() + changeLowLightMode(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK) + Truth.assertThat(condition.isConditionMet).isTrue() + + testStop(condition) + Truth.assertThat(condition.isConditionMet).isFalse() + } + + @Test + fun testLoggingAmbientLightNotLowToLow() = + kosmos.runTest { + testStart(condition) + changeLowLightMode(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK) + // Only logged once. + Mockito.verify(uiEventLogger, Mockito.times(1)).log(ArgumentMatchers.any()) + // Logged with the correct state. + Mockito.verify(uiEventLogger).log(LowLightDockEvent.AMBIENT_LIGHT_TO_DARK) + } + + @Test + fun testLoggingAmbientLightLowToLow() = + kosmos.runTest { + testStart(condition) + changeLowLightMode(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK) + Mockito.reset(uiEventLogger) + + changeLowLightMode(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK) + // Doesn't log. + Mockito.verify(uiEventLogger, Mockito.never()).log(ArgumentMatchers.any()) + } + + @Test + fun testLoggingAmbientLightNotLowToNotLow() = + kosmos.runTest { + testStart(condition) + changeLowLightMode(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_LIGHT) + // Doesn't log. + Mockito.verify(uiEventLogger, Mockito.never()).log(ArgumentMatchers.any()) + } + + @Test + fun testLoggingAmbientLightLowToNotLow() = + kosmos.runTest { + testStart(condition) + runCurrent() + changeLowLightMode(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK) + Mockito.reset(uiEventLogger) + + changeLowLightMode(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_LIGHT) + // Only logged once. + Mockito.verify(uiEventLogger).log(ArgumentMatchers.any()) + // Logged with the correct state. + Mockito.verify(uiEventLogger).log(LowLightDockEvent.AMBIENT_LIGHT_TO_LIGHT) + } + + private fun changeLowLightMode(mode: Int) { + val ambientLightCallbackCaptor = argumentCaptor<AmbientLightModeMonitor.Callback>() + + Mockito.verify(ambientLightModeMonitor).start(ambientLightCallbackCaptor.capture()) + ambientLightCallbackCaptor.lastValue.onChange(mode) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/ScreenSaverEnabledConditionTest.java b/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/ScreenSaverEnabledConditionTest.java deleted file mode 100644 index 366c071fb93f..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/ScreenSaverEnabledConditionTest.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright (C) 2024 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.lowlightclock; - -import static com.google.common.truth.Truth.assertThat; - -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.content.res.Resources; -import android.database.ContentObserver; -import android.os.UserHandle; -import android.provider.Settings; -import android.testing.AndroidTestingRunner; - -import androidx.test.filters.SmallTest; - -import com.android.systemui.SysuiTestCase; -import com.android.systemui.util.settings.SecureSettings; - -import kotlinx.coroutines.CoroutineScope; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -@SmallTest -@RunWith(AndroidTestingRunner.class) -public class ScreenSaverEnabledConditionTest extends SysuiTestCase { - @Mock - private Resources mResources; - @Mock - private SecureSettings mSecureSettings; - @Mock - CoroutineScope mScope; - @Captor - private ArgumentCaptor<ContentObserver> mSettingsObserverCaptor; - private ScreenSaverEnabledCondition mCondition; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - // Default dreams to enabled by default - doReturn(true).when(mResources).getBoolean( - com.android.internal.R.bool.config_dreamsEnabledByDefault); - - mCondition = new ScreenSaverEnabledCondition(mScope, mResources, mSecureSettings); - } - - @Test - public void testScreenSaverInitiallyEnabled() { - setScreenSaverEnabled(true); - mCondition.start(); - assertThat(mCondition.isConditionMet()).isTrue(); - } - - @Test - public void testScreenSaverInitiallyDisabled() { - setScreenSaverEnabled(false); - mCondition.start(); - assertThat(mCondition.isConditionMet()).isFalse(); - } - - @Test - public void testScreenSaverStateChanges() { - setScreenSaverEnabled(false); - mCondition.start(); - assertThat(mCondition.isConditionMet()).isFalse(); - - setScreenSaverEnabled(true); - final ContentObserver observer = captureSettingsObserver(); - observer.onChange(/* selfChange= */ false); - assertThat(mCondition.isConditionMet()).isTrue(); - } - - private void setScreenSaverEnabled(boolean enabled) { - when(mSecureSettings.getIntForUser(eq(Settings.Secure.SCREENSAVER_ENABLED), anyInt(), - eq(UserHandle.USER_CURRENT))).thenReturn(enabled ? 1 : 0); - } - - private ContentObserver captureSettingsObserver() { - verify(mSecureSettings).registerContentObserverForUserSync( - eq(Settings.Secure.SCREENSAVER_ENABLED), - mSettingsObserverCaptor.capture(), eq(UserHandle.USER_CURRENT)); - return mSettingsObserverCaptor.getValue(); - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/ScreenSaverEnabledConditionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/ScreenSaverEnabledConditionTest.kt new file mode 100644 index 000000000000..29237a71d085 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/ScreenSaverEnabledConditionTest.kt @@ -0,0 +1,115 @@ +/* + * 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.lowlightclock + +import android.content.res.Resources +import android.database.ContentObserver +import android.os.UserHandle +import android.provider.Settings +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.internal.R +import com.android.systemui.SysuiTestCase +import com.android.systemui.condition.testStart +import com.android.systemui.kosmos.runCurrent +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import com.android.systemui.util.settings.SecureSettings +import com.google.common.truth.Truth +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.eq +import org.mockito.kotlin.whenever + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class ScreenSaverEnabledConditionTest : SysuiTestCase() { + private val kosmos = testKosmos() + + @Mock private lateinit var resources: Resources + + @Mock private lateinit var secureSettings: SecureSettings + + private val settingsObserverCaptor = argumentCaptor<ContentObserver>() + private lateinit var condition: ScreenSaverEnabledCondition + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + // Default dreams to enabled by default + whenever(resources.getBoolean(R.bool.config_dreamsEnabledByDefault)).thenReturn(true) + + condition = ScreenSaverEnabledCondition(kosmos.testScope, resources, secureSettings) + } + + @Test + fun testScreenSaverInitiallyEnabled() = + kosmos.runTest { + setScreenSaverEnabled(true) + testStart(condition) + Truth.assertThat(condition.isConditionMet).isTrue() + } + + @Test + fun testScreenSaverInitiallyDisabled() = + kosmos.runTest { + setScreenSaverEnabled(false) + testStart(condition) + Truth.assertThat(condition.isConditionMet).isFalse() + } + + @Test + fun testScreenSaverStateChanges() = + kosmos.runTest { + setScreenSaverEnabled(false) + testStart(condition) + Truth.assertThat(condition.isConditionMet).isFalse() + + setScreenSaverEnabled(true) + runCurrent() + val observer = captureSettingsObserver() + observer.onChange(/* selfChange= */ false) + Truth.assertThat(condition.isConditionMet).isTrue() + } + + private fun setScreenSaverEnabled(enabled: Boolean) { + whenever( + secureSettings.getIntForUser( + eq(Settings.Secure.SCREENSAVER_ENABLED), + any(), + eq(UserHandle.USER_CURRENT), + ) + ) + .thenReturn(if (enabled) 1 else 0) + } + + private fun captureSettingsObserver(): ContentObserver { + Mockito.verify(secureSettings) + .registerContentObserverForUserSync( + eq(Settings.Secure.SCREENSAVER_ENABLED), + settingsObserverCaptor.capture(), + eq(UserHandle.USER_CURRENT), + ) + return settingsObserverCaptor.lastValue + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/condition/CombinedConditionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/condition/CombinedConditionTest.kt index 116a2caa6dda..e1e9aa3d193e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shared/condition/CombinedConditionTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/condition/CombinedConditionTest.kt @@ -35,8 +35,7 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class CombinedConditionTest : SysuiTestCase() { - class FakeCondition - constructor( + class FakeCondition( scope: CoroutineScope, initialValue: Boolean?, overriding: Boolean = false, @@ -46,7 +45,7 @@ class CombinedConditionTest : SysuiTestCase() { val started: Boolean get() = _started - override fun start() { + override suspend fun start() { _started = true } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/condition/KosmosConditionTestExtensions.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/condition/KosmosConditionTestExtensions.kt new file mode 100644 index 000000000000..c976b578437f --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/condition/KosmosConditionTestExtensions.kt @@ -0,0 +1,36 @@ +/* + * 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.condition + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.runCurrent +import com.android.systemui.shared.condition.Condition + +private val testCallback = + Condition.Callback { + // This is a no-op + } + +fun Kosmos.testStart(condition: Condition) { + condition.addCallback(testCallback) + runCurrent() +} + +fun Kosmos.testStop(condition: Condition) { + condition.removeCallback(testCallback) + runCurrent() +} |