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