diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/FakeStatusEvent.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/FakeStatusEvent.kt new file mode 100644 index 0000000000000..cd0646543e69d --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/FakeStatusEvent.kt @@ -0,0 +1,29 @@ +/* + * 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.statusbar.events + +/** + * This is a freely configurable implementation of [StatusEvent]. It is intended to be used in + * tests. + */ +class FakeStatusEvent( + override val viewCreator: ViewCreator, + override val priority: Int = 50, + override var forceVisible: Boolean = false, + override val showAnimation: Boolean = true, + override var contentDescription: String? = "", +) : StatusEvent diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt new file mode 100644 index 0000000000000..08a9f3139d71e --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt @@ -0,0 +1,470 @@ +/* + * 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.statusbar.events + +import android.graphics.Rect +import android.os.Process +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper.RunWithLooper +import android.view.View +import android.widget.FrameLayout +import androidx.core.animation.AnimatorTestRule +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.dump.DumpManager +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.privacy.OngoingPrivacyChip +import com.android.systemui.statusbar.BatteryStatusChip +import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider +import com.android.systemui.statusbar.window.StatusBarWindowController +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.time.FakeSystemClock +import junit.framework.Assert.assertEquals +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.anyBoolean +import org.mockito.Mockito.never +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@RunWith(AndroidTestingRunner::class) +@RunWithLooper(setAsMainLooper = true) +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +class SystemStatusAnimationSchedulerImplTest : SysuiTestCase() { + + @Mock private lateinit var systemEventCoordinator: SystemEventCoordinator + @Mock private lateinit var statusBarWindowController: StatusBarWindowController + @Mock private lateinit var statusBarContentInsetProvider: StatusBarContentInsetsProvider + @Mock private lateinit var dumpManager: DumpManager + @Mock private lateinit var listener: SystemStatusAnimationCallback + + private lateinit var systemClock: FakeSystemClock + private lateinit var chipAnimationController: SystemEventChipAnimationController + private lateinit var systemStatusAnimationScheduler: SystemStatusAnimationScheduler + private val fakeFeatureFlags = FakeFeatureFlags() + + @get:Rule val animatorTestRule = AnimatorTestRule() + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + + fakeFeatureFlags.set(Flags.PLUG_IN_STATUS_BAR_CHIP, true) + + systemClock = FakeSystemClock() + chipAnimationController = + SystemEventChipAnimationController( + mContext, + statusBarWindowController, + statusBarContentInsetProvider, + fakeFeatureFlags + ) + + // ensure that isTooEarly() check in SystemStatusAnimationScheduler does not return true + systemClock.advanceTime(Process.getStartUptimeMillis() + MIN_UPTIME) + + // StatusBarContentInsetProvider is mocked. Ensure that it returns some mocked values. + whenever(statusBarContentInsetProvider.getStatusBarContentInsetsForCurrentRotation()) + .thenReturn(android.util.Pair(10, 10)) + whenever(statusBarContentInsetProvider.getStatusBarContentAreaForCurrentRotation()) + .thenReturn(Rect(10, 0, 990, 100)) + + // StatusBarWindowController is mocked. The addViewToWindow function needs to be mocked to + // ensure that the chip view is added to a parent view + whenever(statusBarWindowController.addViewToWindow(any(), any())).then { + val statusbarFake = FrameLayout(mContext) + statusbarFake.layout(0, 0, 1000, 100) + statusbarFake.addView( + it.arguments[0] as View, + it.arguments[1] as FrameLayout.LayoutParams + ) + } + } + + @Test + fun testBatteryStatusEvent_standardAnimationLifecycle() = runTest { + // Instantiate class under test with TestScope from runTest + initializeSystemStatusAnimationScheduler(testScope = this) + + val batteryChip = createAndScheduleFakeBatteryEvent() + + // assert that animation is queued + assertEquals(ANIMATION_QUEUED, systemStatusAnimationScheduler.getAnimationState()) + + // skip debounce delay + advanceTimeBy(DEBOUNCE_DELAY + 1) + // status chip starts animating in after debounce delay + assertEquals(ANIMATING_IN, systemStatusAnimationScheduler.getAnimationState()) + assertEquals(0f, batteryChip.contentView.alpha) + assertEquals(0f, batteryChip.view.alpha) + verify(listener, times(1)).onSystemEventAnimationBegin() + + // skip appear animation + animatorTestRule.advanceTimeBy(APPEAR_ANIMATION_DURATION) + advanceTimeBy(APPEAR_ANIMATION_DURATION) + // assert that status chip is visible + assertEquals(RUNNING_CHIP_ANIM, systemStatusAnimationScheduler.getAnimationState()) + assertEquals(1f, batteryChip.contentView.alpha) + assertEquals(1f, batteryChip.view.alpha) + + // skip status chip display time + advanceTimeBy(DISPLAY_LENGTH + 1) + // assert that it is still visible but switched to the ANIMATING_OUT state + assertEquals(ANIMATING_OUT, systemStatusAnimationScheduler.getAnimationState()) + assertEquals(1f, batteryChip.contentView.alpha) + assertEquals(1f, batteryChip.view.alpha) + verify(listener, times(1)).onSystemEventAnimationFinish(false) + + // skip disappear animation + animatorTestRule.advanceTimeBy(DISAPPEAR_ANIMATION_DURATION) + // assert that it is not visible anymore + assertEquals(IDLE, systemStatusAnimationScheduler.getAnimationState()) + assertEquals(0f, batteryChip.contentView.alpha) + assertEquals(0f, batteryChip.view.alpha) + } + + @Test + fun testPrivacyStatusEvent_standardAnimationLifecycle() = runTest { + // Instantiate class under test with TestScope from runTest + initializeSystemStatusAnimationScheduler(testScope = this) + + val privacyChip = createAndScheduleFakePrivacyEvent() + + // assert that animation is queued + assertEquals(ANIMATION_QUEUED, systemStatusAnimationScheduler.getAnimationState()) + + // skip debounce delay + advanceTimeBy(DEBOUNCE_DELAY + 1) + // status chip starts animating in after debounce delay + assertEquals(ANIMATING_IN, systemStatusAnimationScheduler.getAnimationState()) + assertEquals(0f, privacyChip.view.alpha) + verify(listener, times(1)).onSystemEventAnimationBegin() + + // skip appear animation + animatorTestRule.advanceTimeBy(APPEAR_ANIMATION_DURATION) + advanceTimeBy(APPEAR_ANIMATION_DURATION + 1) + // assert that status chip is visible + assertEquals(RUNNING_CHIP_ANIM, systemStatusAnimationScheduler.getAnimationState()) + assertEquals(1f, privacyChip.view.alpha) + + // skip status chip display time + advanceTimeBy(DISPLAY_LENGTH + 1) + // assert that it is still visible but switched to the ANIMATING_OUT state + assertEquals(ANIMATING_OUT, systemStatusAnimationScheduler.getAnimationState()) + assertEquals(1f, privacyChip.view.alpha) + verify(listener, times(1)).onSystemEventAnimationFinish(true) + verify(listener, times(1)).onSystemStatusAnimationTransitionToPersistentDot(any()) + + // skip transition to persistent dot + advanceTimeBy(DISAPPEAR_ANIMATION_DURATION + 1) + animatorTestRule.advanceTimeBy(DISAPPEAR_ANIMATION_DURATION) + // assert that it the dot is now visible + assertEquals(SHOWING_PERSISTENT_DOT, systemStatusAnimationScheduler.getAnimationState()) + assertEquals(1f, privacyChip.view.alpha) + + // notify SystemStatusAnimationScheduler to remove persistent dot + systemStatusAnimationScheduler.removePersistentDot() + // assert that IDLE state is entered + assertEquals(IDLE, systemStatusAnimationScheduler.getAnimationState()) + verify(listener, times(1)).onHidePersistentDot() + } + + @Test + fun testHighPriorityEvent_takesPrecedenceOverScheduledLowPriorityEvent() = runTest { + // Instantiate class under test with TestScope from runTest + initializeSystemStatusAnimationScheduler(testScope = this) + + // create and schedule low priority event + val batteryChip = createAndScheduleFakeBatteryEvent() + batteryChip.view.alpha = 0f + + // assert that animation is queued + assertEquals(ANIMATION_QUEUED, systemStatusAnimationScheduler.getAnimationState()) + + // create and schedule high priority event + val privacyChip = createAndScheduleFakePrivacyEvent() + + // assert that animation is queued + assertEquals(ANIMATION_QUEUED, systemStatusAnimationScheduler.getAnimationState()) + + // skip debounce delay and appear animation duration + fastForwardAnimationToState(RUNNING_CHIP_ANIM) + + // high priority status chip is visible while low priority status chip is not visible + assertEquals(RUNNING_CHIP_ANIM, systemStatusAnimationScheduler.getAnimationState()) + assertEquals(1f, privacyChip.view.alpha) + assertEquals(0f, batteryChip.view.alpha) + } + + @Test + fun testHighPriorityEvent_cancelsCurrentlyDisplayedLowPriorityEvent() = runTest { + // Instantiate class under test with TestScope from runTest + initializeSystemStatusAnimationScheduler(testScope = this) + + // create and schedule low priority event + val batteryChip = createAndScheduleFakeBatteryEvent() + + // fast forward to RUNNING_CHIP_ANIM state + fastForwardAnimationToState(RUNNING_CHIP_ANIM) + + // assert that chip is displayed + assertEquals(RUNNING_CHIP_ANIM, systemStatusAnimationScheduler.getAnimationState()) + assertEquals(1f, batteryChip.view.alpha) + + // create and schedule high priority event + val privacyChip = createAndScheduleFakePrivacyEvent() + + // ensure that the event cancellation coroutine is started by the test scope + testScheduler.runCurrent() + + // assert that currently displayed chip is immediately animated out + assertEquals(ANIMATING_OUT, systemStatusAnimationScheduler.getAnimationState()) + + // skip disappear animation + animatorTestRule.advanceTimeBy(DISAPPEAR_ANIMATION_DURATION) + + // assert that high priority privacy chip animation is queued + assertEquals(ANIMATION_QUEUED, systemStatusAnimationScheduler.getAnimationState()) + + // skip debounce delay and appear animation + advanceTimeBy(DEBOUNCE_DELAY + APPEAR_ANIMATION_DURATION + 1) + animatorTestRule.advanceTimeBy(APPEAR_ANIMATION_DURATION) + + // high priority status chip is visible while low priority status chip is not visible + assertEquals(RUNNING_CHIP_ANIM, systemStatusAnimationScheduler.getAnimationState()) + assertEquals(1f, privacyChip.view.alpha) + assertEquals(0f, batteryChip.view.alpha) + } + + @Test + fun testHighPriorityEvent_cancelsCurrentlyAnimatedLowPriorityEvent() = runTest { + // Instantiate class under test with TestScope from runTest + initializeSystemStatusAnimationScheduler(testScope = this) + + // create and schedule low priority event + val batteryChip = createAndScheduleFakeBatteryEvent() + + // skip debounce delay + advanceTimeBy(DEBOUNCE_DELAY + 1) + + // assert that chip is animated in + assertEquals(ANIMATING_IN, systemStatusAnimationScheduler.getAnimationState()) + + // create and schedule high priority event + val privacyChip = createAndScheduleFakePrivacyEvent() + + // ensure that the event cancellation coroutine is started by the test scope + testScheduler.runCurrent() + + // assert that currently animated chip keeps animating + assertEquals(ANIMATING_IN, systemStatusAnimationScheduler.getAnimationState()) + + // skip appear animation + animatorTestRule.advanceTimeBy(APPEAR_ANIMATION_DURATION) + advanceTimeBy(APPEAR_ANIMATION_DURATION + 1) + + // assert that low priority chip is animated out immediately after finishing the appear + // animation + assertEquals(ANIMATING_OUT, systemStatusAnimationScheduler.getAnimationState()) + + // skip disappear animation + animatorTestRule.advanceTimeBy(DISAPPEAR_ANIMATION_DURATION) + + // assert that high priority privacy chip animation is queued + assertEquals(ANIMATION_QUEUED, systemStatusAnimationScheduler.getAnimationState()) + + // skip debounce delay and appear animation + advanceTimeBy(DEBOUNCE_DELAY + APPEAR_ANIMATION_DURATION + 1) + animatorTestRule.advanceTimeBy(APPEAR_ANIMATION_DURATION) + + // high priority status chip is visible while low priority status chip is not visible + assertEquals(RUNNING_CHIP_ANIM, systemStatusAnimationScheduler.getAnimationState()) + assertEquals(1f, privacyChip.view.alpha) + assertEquals(0f, batteryChip.view.alpha) + } + + @Test + fun testHighPriorityEvent_isNotReplacedByLowPriorityEvent() = runTest { + // Instantiate class under test with TestScope from runTest + initializeSystemStatusAnimationScheduler(testScope = this) + + // create and schedule high priority event + val privacyChip = createAndScheduleFakePrivacyEvent() + + // create and schedule low priority event + val batteryChip = createAndScheduleFakeBatteryEvent() + batteryChip.view.alpha = 0f + + // skip debounce delay and appear animation + advanceTimeBy(DEBOUNCE_DELAY + APPEAR_ANIMATION_DURATION + 1) + animatorTestRule.advanceTimeBy(APPEAR_ANIMATION_DURATION) + + // high priority status chip is visible while low priority status chip is not visible + assertEquals(RUNNING_CHIP_ANIM, systemStatusAnimationScheduler.getAnimationState()) + assertEquals(1f, privacyChip.view.alpha) + assertEquals(0f, batteryChip.view.alpha) + } + + @Test + fun testPrivacyDot_isRemoved() = runTest { + // Instantiate class under test with TestScope from runTest + initializeSystemStatusAnimationScheduler(testScope = this) + + // create and schedule high priority event + createAndScheduleFakePrivacyEvent() + + // skip chip animation lifecycle and fast forward to SHOWING_PERSISTENT_DOT state + fastForwardAnimationToState(SHOWING_PERSISTENT_DOT) + assertEquals(SHOWING_PERSISTENT_DOT, systemStatusAnimationScheduler.getAnimationState()) + verify(listener, times(1)).onSystemStatusAnimationTransitionToPersistentDot(any()) + + // remove persistent dot and verify that animationState changes to IDLE + systemStatusAnimationScheduler.removePersistentDot() + assertEquals(IDLE, systemStatusAnimationScheduler.getAnimationState()) + verify(listener, times(1)).onHidePersistentDot() + } + + @Test + fun testPrivacyDot_isRemovedDuringChipAnimation() = runTest { + // Instantiate class under test with TestScope from runTest + initializeSystemStatusAnimationScheduler(testScope = this) + + // create and schedule high priority event + createAndScheduleFakePrivacyEvent() + + // skip chip animation lifecycle and fast forward to RUNNING_CHIP_ANIM state + fastForwardAnimationToState(RUNNING_CHIP_ANIM) + assertEquals(RUNNING_CHIP_ANIM, systemStatusAnimationScheduler.getAnimationState()) + + // request removal of persistent dot + systemStatusAnimationScheduler.removePersistentDot() + + // skip display time and verify that disappear animation is run + advanceTimeBy(DISPLAY_LENGTH + 1) + assertEquals(ANIMATING_OUT, systemStatusAnimationScheduler.getAnimationState()) + + // skip disappear animation and verify that animationState changes to IDLE instead of + // SHOWING_PERSISTENT_DOT + animatorTestRule.advanceTimeBy(DISAPPEAR_ANIMATION_DURATION) + assertEquals(IDLE, systemStatusAnimationScheduler.getAnimationState()) + // verify that the persistent dot callbacks are not invoked + verify(listener, never()).onSystemStatusAnimationTransitionToPersistentDot(any()) + verify(listener, never()).onHidePersistentDot() + } + + @Test + fun testNewEvent_isScheduled_whenPostedDuringRemovalAnimation() = runTest { + // Instantiate class under test with TestScope from runTest + initializeSystemStatusAnimationScheduler(testScope = this) + + // create and schedule high priority event + createAndScheduleFakePrivacyEvent() + + // skip chip animation lifecycle and fast forward to ANIMATING_OUT state + fastForwardAnimationToState(ANIMATING_OUT) + assertEquals(ANIMATING_OUT, systemStatusAnimationScheduler.getAnimationState()) + verify(listener, times(1)).onSystemStatusAnimationTransitionToPersistentDot(any()) + + // request removal of persistent dot + systemStatusAnimationScheduler.removePersistentDot() + testScheduler.runCurrent() + + // schedule another high priority event while the event is animating out + createAndScheduleFakePrivacyEvent() + + // verify that the state is still ANIMATING_OUT + assertEquals(ANIMATING_OUT, systemStatusAnimationScheduler.getAnimationState()) + + // skip disappear animation duration and verify that new state is ANIMATION_QUEUED + animatorTestRule.advanceTimeBy(DISAPPEAR_ANIMATION_DURATION) + testScheduler.runCurrent() + assertEquals(ANIMATION_QUEUED, systemStatusAnimationScheduler.getAnimationState()) + // also verify that onHidePersistentDot callback is called + verify(listener, times(1)).onHidePersistentDot() + } + + private fun TestScope.fastForwardAnimationToState(@SystemAnimationState animationState: Int) { + // this function should only be called directly after posting a status event + assertEquals(ANIMATION_QUEUED, systemStatusAnimationScheduler.getAnimationState()) + if (animationState == IDLE || animationState == ANIMATION_QUEUED) return + // skip debounce delay + advanceTimeBy(DEBOUNCE_DELAY + 1) + + // status chip starts animating in after debounce delay + assertEquals(ANIMATING_IN, systemStatusAnimationScheduler.getAnimationState()) + verify(listener, times(1)).onSystemEventAnimationBegin() + if (animationState == ANIMATING_IN) return + + // skip appear animation + animatorTestRule.advanceTimeBy(APPEAR_ANIMATION_DURATION) + advanceTimeBy(APPEAR_ANIMATION_DURATION) + assertEquals(RUNNING_CHIP_ANIM, systemStatusAnimationScheduler.getAnimationState()) + if (animationState == RUNNING_CHIP_ANIM) return + + // skip status chip display time + advanceTimeBy(DISPLAY_LENGTH + 1) + assertEquals(ANIMATING_OUT, systemStatusAnimationScheduler.getAnimationState()) + verify(listener, times(1)).onSystemEventAnimationFinish(anyBoolean()) + if (animationState == ANIMATING_OUT) return + + // skip disappear animation + animatorTestRule.advanceTimeBy(DISAPPEAR_ANIMATION_DURATION) + } + + private fun createAndScheduleFakePrivacyEvent(): OngoingPrivacyChip { + val privacyChip = OngoingPrivacyChip(mContext) + val fakePrivacyStatusEvent = + FakeStatusEvent(viewCreator = { privacyChip }, priority = 100, forceVisible = true) + systemStatusAnimationScheduler.onStatusEvent(fakePrivacyStatusEvent) + return privacyChip + } + + private fun createAndScheduleFakeBatteryEvent(): BatteryStatusChip { + val batteryChip = BatteryStatusChip(mContext) + val fakeBatteryEvent = + FakeStatusEvent(viewCreator = { batteryChip }, priority = 50, forceVisible = false) + systemStatusAnimationScheduler.onStatusEvent(fakeBatteryEvent) + return batteryChip + } + + private fun initializeSystemStatusAnimationScheduler(testScope: TestScope) { + systemStatusAnimationScheduler = + SystemStatusAnimationSchedulerImpl( + systemEventCoordinator, + chipAnimationController, + statusBarWindowController, + dumpManager, + systemClock, + CoroutineScope(StandardTestDispatcher(testScope.testScheduler)) + ) + // add a mock listener + systemStatusAnimationScheduler.addCallback(listener) + } +}