1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.statusbar.events
18 
19 import android.graphics.Insets
20 import android.graphics.Rect
21 import android.os.Process
22 import android.testing.TestableLooper.RunWithLooper
23 import android.view.View
24 import android.widget.FrameLayout
25 import androidx.test.ext.junit.runners.AndroidJUnit4
26 import androidx.test.filters.SmallTest
27 import com.android.systemui.SysuiTestCase
28 import com.android.systemui.animation.AnimatorTestRule
29 import com.android.systemui.dump.DumpManager
30 import com.android.systemui.privacy.OngoingPrivacyChip
31 import com.android.systemui.statusbar.BatteryStatusChip
32 import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState
33 import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.AnimatingIn
34 import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.AnimatingOut
35 import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.AnimationQueued
36 import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.Idle
37 import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.RunningChipAnim
38 import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.ShowingPersistentDot
39 import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
40 import com.android.systemui.statusbar.window.StatusBarWindowController
41 import com.android.systemui.statusbar.window.StatusBarWindowControllerStore
42 import com.android.systemui.util.time.FakeSystemClock
43 import kotlinx.coroutines.CoroutineScope
44 import kotlinx.coroutines.ExperimentalCoroutinesApi
45 import kotlinx.coroutines.test.StandardTestDispatcher
46 import kotlinx.coroutines.test.TestScope
47 import kotlinx.coroutines.test.advanceTimeBy
48 import kotlinx.coroutines.test.runTest
49 import org.junit.Assert.assertEquals
50 import org.junit.Before
51 import org.junit.Rule
52 import org.junit.Test
53 import org.junit.runner.RunWith
54 import org.mockito.Mock
55 import org.mockito.Mockito.anyBoolean
56 import org.mockito.Mockito.never
57 import org.mockito.Mockito.times
58 import org.mockito.Mockito.verify
59 import org.mockito.MockitoAnnotations
60 import org.mockito.kotlin.any
61 import org.mockito.kotlin.eq
62 import org.mockito.kotlin.mock
63 import org.mockito.kotlin.whenever
64 
65 @RunWith(AndroidJUnit4::class)
66 @RunWithLooper(setAsMainLooper = true)
67 @OptIn(ExperimentalCoroutinesApi::class)
68 @SmallTest
69 class SystemStatusAnimationSchedulerImplTest : SysuiTestCase() {
70 
71     @Mock private lateinit var systemEventCoordinator: SystemEventCoordinator
72 
73     @Mock private lateinit var statusBarWindowController: StatusBarWindowController
74     @Mock private lateinit var statusBarWindowControllerStore: StatusBarWindowControllerStore
75 
76     @Mock private lateinit var statusBarContentInsetProvider: StatusBarContentInsetsProvider
77 
78     @Mock private lateinit var dumpManager: DumpManager
79 
80     @Mock private lateinit var listener: SystemStatusAnimationCallback
81 
82     @Mock private lateinit var logger: SystemStatusAnimationSchedulerLogger
83 
84     private lateinit var systemClock: FakeSystemClock
85     private lateinit var chipAnimationController: SystemEventChipAnimationController
86     private lateinit var systemStatusAnimationScheduler: SystemStatusAnimationScheduler
87 
88     @get:Rule val animatorTestRule = AnimatorTestRule(this)
89 
90     @Before
setupnull91     fun setup() {
92         MockitoAnnotations.initMocks(this)
93 
94         whenever(statusBarWindowControllerStore.defaultDisplay)
95             .thenReturn(statusBarWindowController)
96         systemClock = FakeSystemClock()
97         chipAnimationController =
98             SystemEventChipAnimationControllerImpl(
99                 mContext,
100                 statusBarWindowController,
101                 statusBarContentInsetProvider,
102             )
103 
104         // StatusBarContentInsetProvider is mocked. Ensure that it returns some mocked values.
105         whenever(statusBarContentInsetProvider.getStatusBarContentInsetsForCurrentRotation())
106             .thenReturn(Insets.of(/* left= */ 10, /* top= */ 10, /* right= */ 10, /* bottom= */ 0))
107         whenever(statusBarContentInsetProvider.getStatusBarContentAreaForCurrentRotation())
108             .thenReturn(Rect(/* left= */ 10, /* top= */ 10, /* right= */ 990, /* bottom= */ 100))
109 
110         // StatusBarWindowController is mocked. The addViewToWindow function needs to be mocked to
111         // ensure that the chip view is added to a parent view
112         whenever(statusBarWindowController.addViewToWindow(any(), any())).then {
113             val statusbarFake = FrameLayout(mContext)
114             statusbarFake.layout(/* l= */ 0, /* t= */ 0, /* r= */ 1000, /* b= */ 100)
115             statusbarFake.addView(
116                 it.arguments[0] as View,
117                 it.arguments[1] as FrameLayout.LayoutParams,
118             )
119         }
120     }
121 
122     @Test
<lambda>null123     fun testBatteryStatusEvent_standardAnimationLifecycle() = runTest {
124         // Instantiate class under test with TestScope from runTest
125         initializeSystemStatusAnimationScheduler(testScope = this)
126 
127         val batteryChip = createAndScheduleFakeBatteryEvent()
128 
129         // assert that animation is queued
130         assertEquals(AnimationQueued, systemStatusAnimationScheduler.animationState.value)
131 
132         // skip debounce delay
133         advanceTimeBy(DEBOUNCE_DELAY + 1)
134         // status chip starts animating in after debounce delay
135         assertEquals(AnimatingIn, systemStatusAnimationScheduler.animationState.value)
136         assertEquals(0f, batteryChip.contentView.alpha)
137         assertEquals(0f, batteryChip.view.alpha)
138         verify(listener, times(1)).onSystemEventAnimationBegin()
139 
140         // skip appear animation
141         animatorTestRule.advanceTimeBy(APPEAR_ANIMATION_DURATION)
142         advanceTimeBy(APPEAR_ANIMATION_DURATION)
143         // assert that status chip is visible
144         assertEquals(RunningChipAnim, systemStatusAnimationScheduler.animationState.value)
145         assertEquals(1f, batteryChip.contentView.alpha)
146         assertEquals(1f, batteryChip.view.alpha)
147 
148         // skip status chip display time
149         advanceTimeBy(DISPLAY_LENGTH + 1)
150         // assert that it is still visible but switched to the AnimatingOut state
151         assertEquals(AnimatingOut, systemStatusAnimationScheduler.animationState.value)
152         assertEquals(1f, batteryChip.contentView.alpha)
153         assertEquals(1f, batteryChip.view.alpha)
154         verify(listener, times(1)).onSystemEventAnimationFinish(false)
155 
156         // skip disappear animation
157         animatorTestRule.advanceTimeBy(DISAPPEAR_ANIMATION_DURATION)
158         // assert that it is not visible anymore
159         assertEquals(Idle, systemStatusAnimationScheduler.animationState.value)
160         assertEquals(0f, batteryChip.contentView.alpha)
161         assertEquals(0f, batteryChip.view.alpha)
162     }
163 
164     /** Regression test for b/294104969. */
165     @Test
<lambda>null166     fun testPrivacyStatusEvent_beforeSystemUptime_stillDisplayed() = runTest {
167         initializeSystemStatusAnimationScheduler(testScope = this, advancePastMinUptime = false)
168 
169         // WHEN the uptime hasn't quite passed the minimum required uptime...
170         systemClock.setUptimeMillis(Process.getStartUptimeMillis() + MIN_UPTIME / 2)
171 
172         // BUT the event is a privacy event
173         createAndScheduleFakePrivacyEvent()
174 
175         // THEN the privacy event still happens
176         assertEquals(AnimationQueued, systemStatusAnimationScheduler.animationState.value)
177     }
178 
179     @Test
<lambda>null180     fun testPrivacyStatusEvent_standardAnimationLifecycle() = runTest {
181         // Instantiate class under test with TestScope from runTest
182         initializeSystemStatusAnimationScheduler(testScope = this)
183 
184         val privacyChip = createAndScheduleFakePrivacyEvent()
185 
186         // assert that animation is queued
187         assertEquals(AnimationQueued, systemStatusAnimationScheduler.animationState.value)
188 
189         // skip debounce delay
190         advanceTimeBy(DEBOUNCE_DELAY + 1)
191         // status chip starts animating in after debounce delay
192         assertEquals(AnimatingIn, systemStatusAnimationScheduler.animationState.value)
193         assertEquals(0f, privacyChip.view.alpha)
194         verify(listener, times(1)).onSystemEventAnimationBegin()
195 
196         // skip appear animation
197         animatorTestRule.advanceTimeBy(APPEAR_ANIMATION_DURATION)
198         advanceTimeBy(APPEAR_ANIMATION_DURATION + 1)
199         // assert that status chip is visible
200         assertEquals(RunningChipAnim, systemStatusAnimationScheduler.animationState.value)
201         assertEquals(1f, privacyChip.view.alpha)
202 
203         // skip status chip display time
204         advanceTimeBy(DISPLAY_LENGTH + 1)
205         // assert that it is still visible but switched to the AnimatingOut state
206         assertEquals(AnimatingOut, systemStatusAnimationScheduler.animationState.value)
207         assertEquals(1f, privacyChip.view.alpha)
208         verify(listener, times(1)).onSystemEventAnimationFinish(true)
209         verify(listener, times(1)).onSystemStatusAnimationTransitionToPersistentDot(any())
210 
211         // skip transition to persistent dot
212         advanceTimeBy(DISAPPEAR_ANIMATION_DURATION + 1)
213         animatorTestRule.advanceTimeBy(DISAPPEAR_ANIMATION_DURATION)
214         // assert that it the dot is now visible
215         assertEquals(ShowingPersistentDot, systemStatusAnimationScheduler.animationState.value)
216         assertEquals(1f, privacyChip.view.alpha)
217 
218         // notify SystemStatusAnimationScheduler to remove persistent dot
219         systemStatusAnimationScheduler.removePersistentDot()
220         // assert that Idle state is entered
221         assertEquals(Idle, systemStatusAnimationScheduler.animationState.value)
222         verify(listener, times(1)).onHidePersistentDot()
223     }
224 
225     @Test
<lambda>null226     fun testHighPriorityEvent_takesPrecedenceOverScheduledLowPriorityEvent() = runTest {
227         // Instantiate class under test with TestScope from runTest
228         initializeSystemStatusAnimationScheduler(testScope = this)
229 
230         // create and schedule low priority event
231         val batteryChip = createAndScheduleFakeBatteryEvent()
232         batteryChip.view.alpha = 0f
233 
234         // assert that animation is queued
235         assertEquals(AnimationQueued, systemStatusAnimationScheduler.animationState.value)
236 
237         // create and schedule high priority event
238         val privacyChip = createAndScheduleFakePrivacyEvent()
239 
240         // assert that animation is queued
241         assertEquals(AnimationQueued, systemStatusAnimationScheduler.animationState.value)
242 
243         // skip debounce delay and appear animation duration
244         fastForwardAnimationToState(RunningChipAnim)
245 
246         // high priority status chip is visible while low priority status chip is not visible
247         assertEquals(RunningChipAnim, systemStatusAnimationScheduler.animationState.value)
248         assertEquals(1f, privacyChip.view.alpha)
249         assertEquals(0f, batteryChip.view.alpha)
250     }
251 
252     @Test
<lambda>null253     fun testHighPriorityEvent_cancelsCurrentlyDisplayedLowPriorityEvent() = runTest {
254         // Instantiate class under test with TestScope from runTest
255         initializeSystemStatusAnimationScheduler(testScope = this)
256 
257         // create and schedule low priority event
258         val batteryChip = createAndScheduleFakeBatteryEvent()
259 
260         // fast forward to RunningChipAnim state
261         fastForwardAnimationToState(RunningChipAnim)
262 
263         // assert that chip is displayed
264         assertEquals(RunningChipAnim, systemStatusAnimationScheduler.animationState.value)
265         assertEquals(1f, batteryChip.view.alpha)
266 
267         // create and schedule high priority event
268         val privacyChip = createAndScheduleFakePrivacyEvent()
269 
270         // ensure that the event cancellation coroutine is started by the test scope
271         testScheduler.runCurrent()
272 
273         // assert that currently displayed chip is immediately animated out
274         assertEquals(AnimatingOut, systemStatusAnimationScheduler.animationState.value)
275 
276         // skip disappear animation
277         animatorTestRule.advanceTimeBy(DISAPPEAR_ANIMATION_DURATION)
278 
279         // assert that high priority privacy chip animation is queued
280         assertEquals(AnimationQueued, systemStatusAnimationScheduler.animationState.value)
281 
282         // skip debounce delay and appear animation
283         advanceTimeBy(DEBOUNCE_DELAY + APPEAR_ANIMATION_DURATION + 1)
284         animatorTestRule.advanceTimeBy(APPEAR_ANIMATION_DURATION)
285 
286         // high priority status chip is visible while low priority status chip is not visible
287         assertEquals(RunningChipAnim, systemStatusAnimationScheduler.animationState.value)
288         assertEquals(1f, privacyChip.view.alpha)
289         assertEquals(0f, batteryChip.view.alpha)
290     }
291 
292     @Test
<lambda>null293     fun testHighPriorityEvent_cancelsCurrentlyAnimatedLowPriorityEvent() = runTest {
294         // Instantiate class under test with TestScope from runTest
295         initializeSystemStatusAnimationScheduler(testScope = this)
296 
297         // create and schedule low priority event
298         val batteryChip = createAndScheduleFakeBatteryEvent()
299 
300         // skip debounce delay
301         advanceTimeBy(DEBOUNCE_DELAY + 1)
302 
303         // assert that chip is animated in
304         assertEquals(AnimatingIn, systemStatusAnimationScheduler.animationState.value)
305 
306         // create and schedule high priority event
307         val privacyChip = createAndScheduleFakePrivacyEvent()
308 
309         // ensure that the event cancellation coroutine is started by the test scope
310         testScheduler.runCurrent()
311 
312         // assert that currently animated chip keeps animating
313         assertEquals(AnimatingIn, systemStatusAnimationScheduler.animationState.value)
314 
315         // skip appear animation
316         animatorTestRule.advanceTimeBy(APPEAR_ANIMATION_DURATION)
317         advanceTimeBy(APPEAR_ANIMATION_DURATION + 1)
318 
319         // assert that low priority chip is animated out immediately after finishing the appear
320         // animation
321         assertEquals(AnimatingOut, systemStatusAnimationScheduler.animationState.value)
322 
323         // skip disappear animation
324         animatorTestRule.advanceTimeBy(DISAPPEAR_ANIMATION_DURATION)
325 
326         // assert that high priority privacy chip animation is queued
327         assertEquals(AnimationQueued, systemStatusAnimationScheduler.animationState.value)
328 
329         // skip debounce delay and appear animation
330         advanceTimeBy(DEBOUNCE_DELAY + APPEAR_ANIMATION_DURATION + 1)
331         animatorTestRule.advanceTimeBy(APPEAR_ANIMATION_DURATION)
332 
333         // high priority status chip is visible while low priority status chip is not visible
334         assertEquals(RunningChipAnim, systemStatusAnimationScheduler.animationState.value)
335         assertEquals(1f, privacyChip.view.alpha)
336         assertEquals(0f, batteryChip.view.alpha)
337     }
338 
339     @Test
<lambda>null340     fun testHighPriorityEvent_isNotReplacedByLowPriorityEvent() = runTest {
341         // Instantiate class under test with TestScope from runTest
342         initializeSystemStatusAnimationScheduler(testScope = this)
343 
344         // create and schedule high priority event
345         val privacyChip = createAndScheduleFakePrivacyEvent()
346 
347         // create and schedule low priority event
348         val batteryChip = createAndScheduleFakeBatteryEvent()
349         batteryChip.view.alpha = 0f
350 
351         // skip debounce delay and appear animation
352         advanceTimeBy(DEBOUNCE_DELAY + APPEAR_ANIMATION_DURATION + 1)
353         animatorTestRule.advanceTimeBy(APPEAR_ANIMATION_DURATION)
354 
355         // high priority status chip is visible while low priority status chip is not visible
356         assertEquals(RunningChipAnim, systemStatusAnimationScheduler.animationState.value)
357         assertEquals(1f, privacyChip.view.alpha)
358         assertEquals(0f, batteryChip.view.alpha)
359     }
360 
361     @Test
<lambda>null362     fun testPrivacyDot_isRemoved() = runTest {
363         // Instantiate class under test with TestScope from runTest
364         initializeSystemStatusAnimationScheduler(testScope = this)
365 
366         // create and schedule high priority event
367         createAndScheduleFakePrivacyEvent()
368 
369         // skip chip animation lifecycle and fast forward to ShowingPersistentDot state
370         fastForwardAnimationToState(ShowingPersistentDot)
371         assertEquals(ShowingPersistentDot, systemStatusAnimationScheduler.animationState.value)
372         verify(listener, times(1)).onSystemStatusAnimationTransitionToPersistentDot(any())
373 
374         // remove persistent dot and verify that animationState changes to Idle
375         systemStatusAnimationScheduler.removePersistentDot()
376         assertEquals(Idle, systemStatusAnimationScheduler.animationState.value)
377         verify(listener, times(1)).onHidePersistentDot()
378     }
379 
380     @Test
<lambda>null381     fun testAccessibilityAnnouncement_announced() = runTest {
382         // Instantiate class under test with TestScope from runTest
383         initializeSystemStatusAnimationScheduler(testScope = this)
384         val accessibilityDesc = "Some desc"
385         val mockView = mock<View>()
386         val mockAnimatableView =
387             mock<BackgroundAnimatableView> { whenever(it.view).thenReturn(mockView) }
388 
389         scheduleFakeEventWithView(
390             accessibilityDesc,
391             mockAnimatableView,
392             shouldAnnounceAccessibilityEvent = true,
393         )
394         fastForwardAnimationToState(AnimatingOut)
395 
396         verify(mockView).announceForAccessibility(eq(accessibilityDesc))
397     }
398 
399     @Test
<lambda>null400     fun testAccessibilityAnnouncement_nullDesc_noAnnouncement() = runTest {
401         // Instantiate class under test with TestScope from runTest
402         initializeSystemStatusAnimationScheduler(testScope = this)
403         val accessibilityDesc = null
404         val mockView = mock<View>()
405         val mockAnimatableView =
406             mock<BackgroundAnimatableView> { whenever(it.view).thenReturn(mockView) }
407 
408         scheduleFakeEventWithView(
409             accessibilityDesc,
410             mockAnimatableView,
411             shouldAnnounceAccessibilityEvent = true,
412         )
413         fastForwardAnimationToState(AnimatingOut)
414 
415         verify(mockView, never()).announceForAccessibility(any())
416     }
417 
418     @Test
<lambda>null419     fun testAccessibilityAnnouncement_notNeeded_noAnnouncement() = runTest {
420         // Instantiate class under test with TestScope from runTest
421         initializeSystemStatusAnimationScheduler(testScope = this)
422         val accessibilityDesc = "something"
423         val mockView = mock<View>()
424         val mockAnimatableView =
425             mock<BackgroundAnimatableView> { whenever(it.view).thenReturn(mockView) }
426 
427         scheduleFakeEventWithView(
428             accessibilityDesc,
429             mockAnimatableView,
430             shouldAnnounceAccessibilityEvent = false,
431         )
432         fastForwardAnimationToState(AnimatingOut)
433 
434         verify(mockView, never()).announceForAccessibility(any())
435     }
436 
437     @Test
<lambda>null438     fun testPrivacyDot_isRemovedDuringChipAnimation() = runTest {
439         // Instantiate class under test with TestScope from runTest
440         initializeSystemStatusAnimationScheduler(testScope = this)
441 
442         // create and schedule high priority event
443         createAndScheduleFakePrivacyEvent()
444 
445         // skip chip animation lifecycle and fast forward to RunningChipAnim state
446         fastForwardAnimationToState(RunningChipAnim)
447         assertEquals(RunningChipAnim, systemStatusAnimationScheduler.animationState.value)
448 
449         // request removal of persistent dot
450         systemStatusAnimationScheduler.removePersistentDot()
451 
452         // skip display time and verify that disappear animation is run
453         advanceTimeBy(DISPLAY_LENGTH + 1)
454         assertEquals(AnimatingOut, systemStatusAnimationScheduler.animationState.value)
455 
456         // skip disappear animation and verify that animationState changes to Idle instead of
457         // ShowingPersistentDot
458         animatorTestRule.advanceTimeBy(DISAPPEAR_ANIMATION_DURATION)
459         assertEquals(Idle, systemStatusAnimationScheduler.animationState.value)
460         // verify that the persistent dot callbacks are not invoked
461         verify(listener, never()).onSystemStatusAnimationTransitionToPersistentDot(any())
462         verify(listener, never()).onHidePersistentDot()
463     }
464 
465     @Test
<lambda>null466     fun testPrivacyDot_isRemovedDuringChipDisappearAnimation() = runTest {
467         // Instantiate class under test with TestScope from runTest
468         initializeSystemStatusAnimationScheduler(testScope = this)
469 
470         // create and schedule high priority event
471         createAndScheduleFakePrivacyEvent()
472 
473         // fast forward to AnimatingOut state
474         fastForwardAnimationToState(AnimatingOut)
475         assertEquals(AnimatingOut, systemStatusAnimationScheduler.animationState.value)
476         verify(listener, times(1)).onSystemStatusAnimationTransitionToPersistentDot(any())
477 
478         // remove persistent dot
479         systemStatusAnimationScheduler.removePersistentDot()
480 
481         // verify that the onHidePersistentDot callback is invoked
482         verify(listener, times(1)).onHidePersistentDot()
483 
484         // skip disappear animation
485         animatorTestRule.advanceTimeBy(DISAPPEAR_ANIMATION_DURATION)
486         testScheduler.runCurrent()
487 
488         // verify that animationState changes to Idle
489         assertEquals(Idle, systemStatusAnimationScheduler.animationState.value)
490     }
491 
492     @Test
<lambda>null493     fun testPrivacyEvent_forceVisibleIsUpdated_whenRescheduledDuringQueuedState() = runTest {
494         // Instantiate class under test with TestScope from runTest
495         initializeSystemStatusAnimationScheduler(testScope = this)
496 
497         // create and schedule privacy event
498         createAndScheduleFakePrivacyEvent()
499         // request removal of persistent dot (sets forceVisible to false)
500         systemStatusAnimationScheduler.removePersistentDot()
501         // create and schedule a privacy event again (resets forceVisible to true)
502         createAndScheduleFakePrivacyEvent()
503 
504         // skip chip animation lifecycle and fast forward to ShowingPersistentDot state
505         fastForwardAnimationToState(ShowingPersistentDot)
506 
507         // verify that we reach ShowingPersistentDot and that listener callback is invoked
508         assertEquals(ShowingPersistentDot, systemStatusAnimationScheduler.animationState.value)
509         verify(listener, times(1)).onSystemStatusAnimationTransitionToPersistentDot(any())
510     }
511 
512     @Test
<lambda>null513     fun testPrivacyEvent_forceVisibleIsUpdated_whenRescheduledDuringAnimatingState() = runTest {
514         // Instantiate class under test with TestScope from runTest
515         initializeSystemStatusAnimationScheduler(testScope = this)
516 
517         // create and schedule privacy event
518         createAndScheduleFakePrivacyEvent()
519         // request removal of persistent dot (sets forceVisible to false)
520         systemStatusAnimationScheduler.removePersistentDot()
521         fastForwardAnimationToState(RunningChipAnim)
522 
523         // create and schedule a privacy event again (resets forceVisible to true)
524         createAndScheduleFakePrivacyEvent()
525 
526         // skip status chip display time
527         advanceTimeBy(DISPLAY_LENGTH + 1)
528         assertEquals(AnimatingOut, systemStatusAnimationScheduler.animationState.value)
529         verify(listener, times(1)).onSystemEventAnimationFinish(anyBoolean())
530 
531         // skip disappear animation
532         animatorTestRule.advanceTimeBy(DISAPPEAR_ANIMATION_DURATION)
533 
534         // verify that we reach ShowingPersistentDot and that listener callback is invoked
535         assertEquals(ShowingPersistentDot, systemStatusAnimationScheduler.animationState.value)
536         verify(listener, times(1)).onSystemStatusAnimationTransitionToPersistentDot(any())
537     }
538 
539     @Test
<lambda>null540     fun testNewEvent_isScheduled_whenPostedDuringRemovalAnimation() = runTest {
541         // Instantiate class under test with TestScope from runTest
542         initializeSystemStatusAnimationScheduler(testScope = this)
543 
544         // create and schedule high priority event
545         createAndScheduleFakePrivacyEvent()
546 
547         // skip chip animation lifecycle and fast forward to AnimatingOut state
548         fastForwardAnimationToState(AnimatingOut)
549         assertEquals(AnimatingOut, systemStatusAnimationScheduler.animationState.value)
550         verify(listener, times(1)).onSystemStatusAnimationTransitionToPersistentDot(any())
551 
552         // request removal of persistent dot
553         systemStatusAnimationScheduler.removePersistentDot()
554 
555         // schedule another high priority event while the event is animating out
556         createAndScheduleFakePrivacyEvent()
557 
558         // verify that the state is still AnimatingOut
559         assertEquals(AnimatingOut, systemStatusAnimationScheduler.animationState.value)
560 
561         // skip disappear animation duration and verify that new state is AnimationQueued
562         animatorTestRule.advanceTimeBy(DISAPPEAR_ANIMATION_DURATION)
563         testScheduler.runCurrent()
564         assertEquals(AnimationQueued, systemStatusAnimationScheduler.animationState.value)
565         // also verify that onHidePersistentDot callback is called
566         verify(listener, times(1)).onHidePersistentDot()
567     }
568 
569     @Test
<lambda>null570     fun testDotIsRemoved_evenIfAnimatorCallbackIsDelayed() = runTest {
571         // Instantiate class under test with TestScope from runTest
572         initializeSystemStatusAnimationScheduler(testScope = this)
573 
574         // create and schedule high priority event
575         createAndScheduleFakePrivacyEvent()
576 
577         // skip chip animation lifecycle and fast forward to AnimatingOut state
578         fastForwardAnimationToState(AnimatingOut)
579         assertEquals(AnimatingOut, systemStatusAnimationScheduler.animationState.value)
580         verify(listener, times(1)).onSystemStatusAnimationTransitionToPersistentDot(any())
581 
582         // request removal of persistent dot
583         systemStatusAnimationScheduler.removePersistentDot()
584 
585         // verify that the state is still AnimatingOut
586         assertEquals(AnimatingOut, systemStatusAnimationScheduler.animationState.value)
587 
588         // skip disappear animation duration
589         testScheduler.advanceTimeBy(DISAPPEAR_ANIMATION_DURATION + 1)
590         // In an old implementation this would trigger a coroutine timeout causing the
591         // onHidePersistentDot callback to be missed.
592         testScheduler.runCurrent()
593 
594         // advance animator time to invoke onAnimationEnd callback
595         animatorTestRule.advanceTimeBy(DISAPPEAR_ANIMATION_DURATION)
596         testScheduler.runCurrent()
597 
598         // verify that onHidePersistentDot is invoked despite the animator callback being delayed
599         // (it's invoked more than DISAPPEAR_ANIMATION_DURATION after the dot removal was requested)
600         verify(listener, times(1)).onHidePersistentDot()
601         // verify that animationState is Idle
602         assertEquals(Idle, systemStatusAnimationScheduler.animationState.value)
603     }
604 
fastForwardAnimationToStatenull605     private fun TestScope.fastForwardAnimationToState(animationState: SystemEventAnimationState) {
606         // this function should only be called directly after posting a status event
607         assertEquals(AnimationQueued, systemStatusAnimationScheduler.animationState.value)
608         if (animationState == Idle || animationState == AnimationQueued) return
609         // skip debounce delay
610         advanceTimeBy(DEBOUNCE_DELAY + 1)
611 
612         // status chip starts animating in after debounce delay
613         assertEquals(AnimatingIn, systemStatusAnimationScheduler.animationState.value)
614         verify(listener, times(1)).onSystemEventAnimationBegin()
615         if (animationState == AnimatingIn) return
616 
617         // skip appear animation
618         animatorTestRule.advanceTimeBy(APPEAR_ANIMATION_DURATION)
619         advanceTimeBy(APPEAR_ANIMATION_DURATION)
620         assertEquals(RunningChipAnim, systemStatusAnimationScheduler.animationState.value)
621         if (animationState == RunningChipAnim) return
622 
623         // skip status chip display time
624         advanceTimeBy(DISPLAY_LENGTH + 1)
625         assertEquals(AnimatingOut, systemStatusAnimationScheduler.animationState.value)
626         verify(listener, times(1)).onSystemEventAnimationFinish(anyBoolean())
627         if (animationState == AnimatingOut) return
628 
629         // skip disappear animation
630         animatorTestRule.advanceTimeBy(DISAPPEAR_ANIMATION_DURATION)
631     }
632 
createAndScheduleFakePrivacyEventnull633     private fun createAndScheduleFakePrivacyEvent(): OngoingPrivacyChip {
634         val privacyChip = OngoingPrivacyChip(mContext)
635         val fakePrivacyStatusEvent = FakePrivacyStatusEvent(viewCreator = { privacyChip })
636         systemStatusAnimationScheduler.onStatusEvent(fakePrivacyStatusEvent)
637         return privacyChip
638     }
639 
scheduleFakeEventWithViewnull640     private fun scheduleFakeEventWithView(
641         desc: String?,
642         view: BackgroundAnimatableView,
643         shouldAnnounceAccessibilityEvent: Boolean,
644     ) {
645         val fakeEvent =
646             FakeStatusEvent(
647                 viewCreator = { view },
648                 contentDescription = desc,
649                 shouldAnnounceAccessibilityEvent = shouldAnnounceAccessibilityEvent,
650             )
651         systemStatusAnimationScheduler.onStatusEvent(fakeEvent)
652     }
653 
createAndScheduleFakeBatteryEventnull654     private fun createAndScheduleFakeBatteryEvent(): BatteryStatusChip {
655         val batteryChip = BatteryStatusChip(mContext)
656         val fakeBatteryEvent =
657             FakeStatusEvent(viewCreator = { batteryChip }, priority = 50, forceVisible = false)
658         systemStatusAnimationScheduler.onStatusEvent(fakeBatteryEvent)
659         return batteryChip
660     }
661 
initializeSystemStatusAnimationSchedulernull662     private fun initializeSystemStatusAnimationScheduler(
663         testScope: TestScope,
664         advancePastMinUptime: Boolean = true,
665     ) {
666         systemStatusAnimationScheduler =
667             SystemStatusAnimationSchedulerImpl(
668                 systemEventCoordinator,
669                 chipAnimationController,
670                 statusBarWindowControllerStore,
671                 dumpManager,
672                 systemClock,
673                 CoroutineScope(StandardTestDispatcher(testScope.testScheduler)),
674                 logger,
675             )
676         // add a mock listener
677         systemStatusAnimationScheduler.addCallback(listener)
678 
679         if (advancePastMinUptime) {
680             // ensure that isTooEarly() check in SystemStatusAnimationScheduler does not return true
681             systemClock.advanceTime(Process.getStartUptimeMillis() + MIN_UPTIME)
682         }
683     }
684 }
685