1 /* 2 * Copyright (C) 2020 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.media.controls.ui.controller 18 19 import android.graphics.Rect 20 import android.provider.Settings 21 import android.testing.TestableLooper 22 import android.view.ViewGroup 23 import android.widget.FrameLayout 24 import androidx.test.ext.junit.runners.AndroidJUnit4 25 import androidx.test.filters.SmallTest 26 import com.android.keyguard.KeyguardViewController 27 import com.android.systemui.SysuiTestCase 28 import com.android.systemui.communal.data.repository.fakeCommunalSceneRepository 29 import com.android.systemui.communal.shared.model.CommunalScenes 30 import com.android.systemui.communal.ui.viewmodel.communalTransitionViewModel 31 import com.android.systemui.controls.controller.ControlsControllerImplTest.Companion.eq 32 import com.android.systemui.dreams.DreamOverlayStateController 33 import com.android.systemui.keyguard.WakefulnessLifecycle 34 import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository 35 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository 36 import com.android.systemui.keyguard.domain.interactor.keyguardInteractor 37 import com.android.systemui.keyguard.shared.model.KeyguardState 38 import com.android.systemui.kosmos.testScope 39 import com.android.systemui.media.controls.domain.pipeline.MediaDataManager 40 import com.android.systemui.media.controls.ui.view.MediaCarouselScrollHandler 41 import com.android.systemui.media.controls.ui.view.MediaHost 42 import com.android.systemui.media.controls.ui.view.MediaHostState 43 import com.android.systemui.media.controls.util.MediaFlags 44 import com.android.systemui.media.dream.MediaDreamComplication 45 import com.android.systemui.plugins.statusbar.StatusBarStateController 46 import com.android.systemui.res.R 47 import com.android.systemui.shade.domain.interactor.ShadeInteractor 48 import com.android.systemui.statusbar.StatusBarState 49 import com.android.systemui.statusbar.SysuiStatusBarStateController 50 import com.android.systemui.statusbar.phone.KeyguardBypassController 51 import com.android.systemui.statusbar.policy.FakeConfigurationController 52 import com.android.systemui.statusbar.policy.KeyguardStateController 53 import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController 54 import com.android.systemui.testKosmos 55 import com.android.systemui.util.animation.UniqueObjectHostView 56 import com.android.systemui.util.mockito.mock 57 import com.android.systemui.util.mockito.nullable 58 import com.android.systemui.util.settings.FakeSettings 59 import com.android.systemui.utils.os.FakeHandler 60 import com.google.common.truth.Truth.assertThat 61 import kotlinx.coroutines.ExperimentalCoroutinesApi 62 import kotlinx.coroutines.flow.MutableStateFlow 63 import kotlinx.coroutines.test.runCurrent 64 import kotlinx.coroutines.test.runTest 65 import org.junit.Assert.assertNotNull 66 import org.junit.Before 67 import org.junit.Rule 68 import org.junit.Test 69 import org.junit.runner.RunWith 70 import org.mockito.ArgumentCaptor 71 import org.mockito.ArgumentMatchers 72 import org.mockito.ArgumentMatchers.anyBoolean 73 import org.mockito.ArgumentMatchers.anyLong 74 import org.mockito.Captor 75 import org.mockito.Mock 76 import org.mockito.Mockito.clearInvocations 77 import org.mockito.Mockito.times 78 import org.mockito.Mockito.verify 79 import org.mockito.Mockito.`when` as whenever 80 import org.mockito.junit.MockitoJUnit 81 import org.mockito.kotlin.any 82 import org.mockito.kotlin.anyOrNull 83 84 @OptIn(ExperimentalCoroutinesApi::class) 85 @SmallTest 86 @RunWith(AndroidJUnit4::class) 87 @TestableLooper.RunWithLooper(setAsMainLooper = true) 88 class MediaHierarchyManagerTest : SysuiTestCase() { 89 90 private val kosmos = testKosmos() 91 92 @Mock private lateinit var lockHost: MediaHost 93 @Mock private lateinit var qsHost: MediaHost 94 @Mock private lateinit var qqsHost: MediaHost 95 @Mock private lateinit var hubModeHost: MediaHost 96 @Mock private lateinit var bypassController: KeyguardBypassController 97 @Mock private lateinit var keyguardStateController: KeyguardStateController 98 @Mock private lateinit var statusBarStateController: SysuiStatusBarStateController 99 @Mock private lateinit var mediaCarouselController: MediaCarouselController 100 @Mock private lateinit var mediaCarouselScrollHandler: MediaCarouselScrollHandler 101 @Mock private lateinit var wakefulnessLifecycle: WakefulnessLifecycle 102 @Mock private lateinit var keyguardViewController: KeyguardViewController 103 @Mock private lateinit var mediaDataManager: MediaDataManager 104 @Mock private lateinit var uniqueObjectHostView: UniqueObjectHostView 105 @Mock private lateinit var dreamOverlayStateController: DreamOverlayStateController 106 @Mock private lateinit var shadeInteractor: ShadeInteractor 107 @Mock lateinit var logger: MediaViewLogger 108 @Mock private lateinit var mediaFlags: MediaFlags 109 @Captor 110 private lateinit var wakefullnessObserver: ArgumentCaptor<(WakefulnessLifecycle.Observer)> 111 @Captor 112 private lateinit var statusBarCallback: ArgumentCaptor<(StatusBarStateController.StateListener)> 113 @Captor 114 private lateinit var dreamOverlayCallback: 115 ArgumentCaptor<(DreamOverlayStateController.Callback)> 116 @JvmField @Rule val mockito = MockitoJUnit.rule() 117 private val testScope = kosmos.testScope 118 private lateinit var mediaHierarchyManager: MediaHierarchyManager 119 private lateinit var isQsBypassingShade: MutableStateFlow<Boolean> 120 private lateinit var shadeExpansion: MutableStateFlow<Float> 121 private lateinit var mediaFrame: ViewGroup 122 private val configurationController = FakeConfigurationController() 123 private val settings = FakeSettings() 124 private lateinit var testableLooper: TestableLooper 125 private lateinit var fakeHandler: FakeHandler 126 private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository 127 private val keyguardRepository = kosmos.fakeKeyguardRepository 128 129 @Before setupnull130 fun setup() { 131 context 132 .getOrCreateTestableResources() 133 .addOverride(R.bool.config_use_split_notification_shade, false) 134 mediaFrame = FrameLayout(context) 135 testableLooper = TestableLooper.get(this) 136 fakeHandler = FakeHandler(testableLooper.looper) 137 whenever(mediaCarouselController.mediaFrame).thenReturn(mediaFrame) 138 isQsBypassingShade = MutableStateFlow(false) 139 shadeExpansion = MutableStateFlow(0f) 140 whenever(shadeInteractor.isQsBypassingShade).thenReturn(isQsBypassingShade) 141 whenever(shadeInteractor.shadeExpansion).thenReturn(shadeExpansion) 142 whenever(mediaFlags.isSceneContainerEnabled()).thenReturn(false) 143 mediaHierarchyManager = 144 MediaHierarchyManager( 145 context, 146 statusBarStateController, 147 keyguardStateController, 148 bypassController, 149 mediaCarouselController, 150 mediaDataManager, 151 keyguardViewController, 152 dreamOverlayStateController, 153 kosmos.keyguardInteractor, 154 kosmos.communalTransitionViewModel, 155 configurationController, 156 wakefulnessLifecycle, 157 shadeInteractor, 158 settings, 159 fakeHandler, 160 testScope.backgroundScope, 161 ResourcesSplitShadeStateController(), 162 logger, 163 mediaFlags, 164 ) 165 verify(wakefulnessLifecycle).addObserver(wakefullnessObserver.capture()) 166 verify(statusBarStateController).addCallback(statusBarCallback.capture()) 167 verify(dreamOverlayStateController).addCallback(dreamOverlayCallback.capture()) 168 setupHost(lockHost, MediaHierarchyManager.LOCATION_LOCKSCREEN, LOCKSCREEN_TOP) 169 setupHost(qsHost, MediaHierarchyManager.LOCATION_QS, QS_TOP) 170 setupHost(qqsHost, MediaHierarchyManager.LOCATION_QQS, QQS_TOP) 171 setupHost(hubModeHost, MediaHierarchyManager.LOCATION_COMMUNAL_HUB, COMMUNAL_TOP) 172 whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE) 173 whenever(mediaDataManager.hasActiveMedia()).thenReturn(true) 174 whenever(mediaCarouselController.mediaCarouselScrollHandler) 175 .thenReturn(mediaCarouselScrollHandler) 176 val observer = wakefullnessObserver.value 177 assertNotNull("lifecycle observer wasn't registered", observer) 178 observer.onFinishedWakingUp() 179 // We'll use the viewmanager to verify a few calls below, let's reset this. 180 clearInvocations(mediaCarouselController) 181 } 182 setupHostnull183 private fun setupHost(host: MediaHost, location: Int, top: Int) { 184 whenever(host.location).thenReturn(location) 185 whenever(host.currentBounds).thenReturn(Rect(0, top, 0, top)) 186 whenever(host.hostView).thenReturn(uniqueObjectHostView) 187 whenever(host.visible).thenReturn(true) 188 mediaHierarchyManager.register(host) 189 } 190 191 @Test testHostViewSetOnRegisternull192 fun testHostViewSetOnRegister() { 193 val host = mediaHierarchyManager.register(lockHost) 194 verify(lockHost).hostView = eq(host) 195 } 196 197 @Test testBlockedWhenScreenTurningOffnull198 fun testBlockedWhenScreenTurningOff() { 199 // Let's set it onto QS: 200 mediaHierarchyManager.qsExpansion = 1.0f 201 verify(mediaCarouselController) 202 .onDesiredLocationChanged( 203 ArgumentMatchers.anyInt(), 204 any<MediaHostState>(), 205 anyBoolean(), 206 anyLong(), 207 anyLong() 208 ) 209 val observer = wakefullnessObserver.value 210 assertNotNull("lifecycle observer wasn't registered", observer) 211 observer.onStartedGoingToSleep() 212 clearInvocations(mediaCarouselController) 213 mediaHierarchyManager.qsExpansion = 0.0f 214 verify(mediaCarouselController, times(0)) 215 .onDesiredLocationChanged( 216 ArgumentMatchers.anyInt(), 217 any<MediaHostState>(), 218 anyBoolean(), 219 anyLong(), 220 anyLong() 221 ) 222 } 223 224 @Test testBlockedWhenConfigurationChangesAndScreenOffnull225 fun testBlockedWhenConfigurationChangesAndScreenOff() { 226 // Let's set it onto QS: 227 mediaHierarchyManager.qsExpansion = 1.0f 228 verify(mediaCarouselController) 229 .onDesiredLocationChanged( 230 ArgumentMatchers.anyInt(), 231 any<MediaHostState>(), 232 anyBoolean(), 233 anyLong(), 234 anyLong() 235 ) 236 val observer = wakefullnessObserver.value 237 assertNotNull("lifecycle observer wasn't registered", observer) 238 observer.onStartedGoingToSleep() 239 clearInvocations(mediaCarouselController) 240 configurationController.notifyConfigurationChanged() 241 verify(mediaCarouselController, times(0)) 242 .onDesiredLocationChanged( 243 ArgumentMatchers.anyInt(), 244 any<MediaHostState>(), 245 anyBoolean(), 246 anyLong(), 247 anyLong() 248 ) 249 } 250 251 @Test testAllowedWhenConfigurationChangesnull252 fun testAllowedWhenConfigurationChanges() { 253 // Let's set it onto QS: 254 mediaHierarchyManager.qsExpansion = 1.0f 255 verify(mediaCarouselController) 256 .onDesiredLocationChanged( 257 ArgumentMatchers.anyInt(), 258 any<MediaHostState>(), 259 anyBoolean(), 260 anyLong(), 261 anyLong() 262 ) 263 clearInvocations(mediaCarouselController) 264 configurationController.notifyConfigurationChanged() 265 verify(mediaCarouselController) 266 .onDesiredLocationChanged( 267 ArgumentMatchers.anyInt(), 268 any<MediaHostState>(), 269 anyBoolean(), 270 anyLong(), 271 anyLong() 272 ) 273 } 274 275 @Test testAllowedWhenNotTurningOffnull276 fun testAllowedWhenNotTurningOff() { 277 // Let's set it onto QS: 278 mediaHierarchyManager.qsExpansion = 1.0f 279 verify(mediaCarouselController) 280 .onDesiredLocationChanged( 281 ArgumentMatchers.anyInt(), 282 any<MediaHostState>(), 283 anyBoolean(), 284 anyLong(), 285 anyLong() 286 ) 287 val observer = wakefullnessObserver.value 288 assertNotNull("lifecycle observer wasn't registered", observer) 289 clearInvocations(mediaCarouselController) 290 mediaHierarchyManager.qsExpansion = 0.0f 291 verify(mediaCarouselController) 292 .onDesiredLocationChanged( 293 ArgumentMatchers.anyInt(), 294 any<MediaHostState>(), 295 anyBoolean(), 296 anyLong(), 297 anyLong() 298 ) 299 } 300 301 @Test testGoingToFullShadenull302 fun testGoingToFullShade() { 303 goToLockscreen() 304 305 // Let's transition all the way to full shade 306 mediaHierarchyManager.setTransitionToFullShadeAmount(100000f) 307 verify(mediaCarouselController) 308 .onDesiredLocationChanged( 309 eq(MediaHierarchyManager.LOCATION_QQS), 310 any<MediaHostState>(), 311 eq(false), 312 anyLong(), 313 anyLong() 314 ) 315 clearInvocations(mediaCarouselController) 316 317 // Let's go back to the lock screen 318 mediaHierarchyManager.setTransitionToFullShadeAmount(0.0f) 319 verify(mediaCarouselController) 320 .onDesiredLocationChanged( 321 eq(MediaHierarchyManager.LOCATION_LOCKSCREEN), 322 any<MediaHostState>(), 323 eq(false), 324 anyLong(), 325 anyLong() 326 ) 327 328 // Let's make sure alpha is set 329 mediaHierarchyManager.setTransitionToFullShadeAmount(2.0f) 330 assertThat(mediaFrame.alpha).isNotEqualTo(1.0f) 331 } 332 333 @Test testTransformationOnLockScreenIsFadingnull334 fun testTransformationOnLockScreenIsFading() { 335 goToLockscreen() 336 expandQS() 337 338 val transformType = mediaHierarchyManager.calculateTransformationType() 339 assertThat(transformType).isEqualTo(MediaHierarchyManager.TRANSFORMATION_TYPE_FADE) 340 } 341 342 @Test calculateTransformationType_notOnLockscreen_returnsTransitionnull343 fun calculateTransformationType_notOnLockscreen_returnsTransition() { 344 expandQS() 345 346 val transformType = mediaHierarchyManager.calculateTransformationType() 347 348 assertThat(transformType).isEqualTo(MediaHierarchyManager.TRANSFORMATION_TYPE_TRANSITION) 349 } 350 351 @Test calculateTransformationType_onLockscreen_returnsTransitionnull352 fun calculateTransformationType_onLockscreen_returnsTransition() { 353 goToLockscreen() 354 expandQS() 355 356 val transformType = mediaHierarchyManager.calculateTransformationType() 357 358 assertThat(transformType).isEqualTo(MediaHierarchyManager.TRANSFORMATION_TYPE_FADE) 359 } 360 361 @Test calculateTransformationType_onLockShade_inSplitShade_goingToFullShade_returnsTransitionnull362 fun calculateTransformationType_onLockShade_inSplitShade_goingToFullShade_returnsTransition() { 363 enableSplitShade() 364 goToLockscreen() 365 expandQS() 366 mediaHierarchyManager.setTransitionToFullShadeAmount(10000f) 367 368 val transformType = mediaHierarchyManager.calculateTransformationType() 369 assertThat(transformType).isEqualTo(MediaHierarchyManager.TRANSFORMATION_TYPE_TRANSITION) 370 } 371 372 @Test calculateTransformationType_onLockSplitShade_goingToFullShade_mediaInvisible_returnsFadenull373 fun calculateTransformationType_onLockSplitShade_goingToFullShade_mediaInvisible_returnsFade() { 374 enableSplitShade() 375 goToLockscreen() 376 expandQS() 377 whenever(lockHost.visible).thenReturn(false) 378 mediaHierarchyManager.setTransitionToFullShadeAmount(10000f) 379 380 val transformType = mediaHierarchyManager.calculateTransformationType() 381 assertThat(transformType).isEqualTo(MediaHierarchyManager.TRANSFORMATION_TYPE_FADE) 382 } 383 384 @Test calculateTransformationType_onLockShade_inSplitShade_notExpanding_returnsFadenull385 fun calculateTransformationType_onLockShade_inSplitShade_notExpanding_returnsFade() { 386 enableSplitShade() 387 goToLockscreen() 388 goToLockedShade() 389 expandQS() 390 mediaHierarchyManager.setTransitionToFullShadeAmount(0f) 391 392 val transformType = mediaHierarchyManager.calculateTransformationType() 393 assertThat(transformType).isEqualTo(MediaHierarchyManager.TRANSFORMATION_TYPE_FADE) 394 } 395 396 @Test testTransformationOnLockScreenToQQSisFadingnull397 fun testTransformationOnLockScreenToQQSisFading() { 398 goToLockscreen() 399 goToLockedShade() 400 401 val transformType = mediaHierarchyManager.calculateTransformationType() 402 assertThat(transformType).isEqualTo(MediaHierarchyManager.TRANSFORMATION_TYPE_FADE) 403 } 404 405 @Test testCloseGutsRelayToCarouselnull406 fun testCloseGutsRelayToCarousel() { 407 mediaHierarchyManager.closeGuts() 408 409 verify(mediaCarouselController).closeGuts() 410 } 411 412 @Test testCloseGutsWhenDozenull413 fun testCloseGutsWhenDoze() { 414 statusBarCallback.value.onDozingChanged(true) 415 416 verify(mediaCarouselController).closeGuts() 417 } 418 419 @Test getGuidedTransformationTranslationY_notInGuidedTransformation_returnsNegativeNumbernull420 fun getGuidedTransformationTranslationY_notInGuidedTransformation_returnsNegativeNumber() { 421 assertThat(mediaHierarchyManager.getGuidedTransformationTranslationY()).isLessThan(0) 422 } 423 424 @Test getGuidedTransformationTranslationY_inGuidedTransformation_returnsCurrentTranslationnull425 fun getGuidedTransformationTranslationY_inGuidedTransformation_returnsCurrentTranslation() { 426 enterGuidedTransformation() 427 428 val expectedTranslation = LOCKSCREEN_TOP - QS_TOP 429 assertThat(mediaHierarchyManager.getGuidedTransformationTranslationY()) 430 .isEqualTo(expectedTranslation) 431 } 432 433 @Test getGuidedTransformationTranslationY_previousHostInvisible_returnsZeronull434 fun getGuidedTransformationTranslationY_previousHostInvisible_returnsZero() { 435 goToLockscreen() 436 enterGuidedTransformation() 437 whenever(lockHost.visible).thenReturn(false) 438 439 assertThat(mediaHierarchyManager.getGuidedTransformationTranslationY()).isEqualTo(0) 440 } 441 442 @Test isCurrentlyInGuidedTransformation_hostsVisible_returnsTruenull443 fun isCurrentlyInGuidedTransformation_hostsVisible_returnsTrue() { 444 goToLockscreen() 445 enterGuidedTransformation() 446 whenever(lockHost.visible).thenReturn(true) 447 whenever(qsHost.visible).thenReturn(true) 448 whenever(qqsHost.visible).thenReturn(true) 449 450 assertThat(mediaHierarchyManager.isCurrentlyInGuidedTransformation()).isTrue() 451 } 452 453 @OptIn(ExperimentalCoroutinesApi::class) 454 @Test isCurrentlyInGuidedTransformation_hostsVisible_expandImmediateEnabled_returnsFalsenull455 fun isCurrentlyInGuidedTransformation_hostsVisible_expandImmediateEnabled_returnsFalse() = 456 testScope.runTest { 457 runCurrent() 458 isQsBypassingShade.value = true 459 runCurrent() 460 goToLockscreen() 461 enterGuidedTransformation() 462 whenever(lockHost.visible).thenReturn(true) 463 whenever(qsHost.visible).thenReturn(true) 464 whenever(qqsHost.visible).thenReturn(true) 465 466 assertThat(mediaHierarchyManager.isCurrentlyInGuidedTransformation()).isFalse() 467 } 468 469 @Test isCurrentlyInGuidedTransformation_hostNotVisible_returnsFalse_with_activenull470 fun isCurrentlyInGuidedTransformation_hostNotVisible_returnsFalse_with_active() { 471 goToLockscreen() 472 enterGuidedTransformation() 473 whenever(lockHost.visible).thenReturn(false) 474 whenever(qsHost.visible).thenReturn(true) 475 whenever(qqsHost.visible).thenReturn(true) 476 whenever(mediaDataManager.hasActiveMediaOrRecommendation()).thenReturn(true) 477 478 assertThat(mediaHierarchyManager.isCurrentlyInGuidedTransformation()).isFalse() 479 } 480 481 @Test isCurrentlyInGuidedTransformation_hostNotVisible_returnsTrue_without_activenull482 fun isCurrentlyInGuidedTransformation_hostNotVisible_returnsTrue_without_active() { 483 // To keep the appearing behavior, we need to be in a guided transition 484 goToLockscreen() 485 enterGuidedTransformation() 486 whenever(lockHost.visible).thenReturn(false) 487 whenever(qsHost.visible).thenReturn(true) 488 whenever(qqsHost.visible).thenReturn(true) 489 whenever(mediaDataManager.hasActiveMediaOrRecommendation()).thenReturn(false) 490 491 assertThat(mediaHierarchyManager.isCurrentlyInGuidedTransformation()).isTrue() 492 } 493 494 @Test isCurrentlyInGuidedTransformation_desiredLocationIsHub_returnsFalsenull495 fun isCurrentlyInGuidedTransformation_desiredLocationIsHub_returnsFalse() = 496 testScope.runTest { 497 goToLockscreen() 498 keyguardTransitionRepository.sendTransitionSteps( 499 from = KeyguardState.LOCKSCREEN, 500 to = KeyguardState.GLANCEABLE_HUB, 501 testScope = testScope, 502 ) 503 kosmos.fakeCommunalSceneRepository.changeScene(CommunalScenes.Communal) 504 runCurrent() 505 mediaHierarchyManager.qsExpansion = 0f 506 mediaHierarchyManager.setTransitionToFullShadeAmount(123f) 507 508 whenever(lockHost.visible).thenReturn(true) 509 whenever(qsHost.visible).thenReturn(true) 510 whenever(qqsHost.visible).thenReturn(true) 511 whenever(hubModeHost.visible).thenReturn(true) 512 513 assertThat(mediaHierarchyManager.isCurrentlyInGuidedTransformation()).isFalse() 514 } 515 516 @Test testDreamnull517 fun testDream() { 518 goToDream() 519 setMediaDreamComplicationEnabled(true) 520 verify(mediaCarouselController) 521 .onDesiredLocationChanged( 522 eq(MediaHierarchyManager.LOCATION_DREAM_OVERLAY), 523 nullable(), 524 eq(false), 525 anyLong(), 526 anyLong() 527 ) 528 clearInvocations(mediaCarouselController) 529 530 setMediaDreamComplicationEnabled(false) 531 verify(mediaCarouselController) 532 .onDesiredLocationChanged( 533 eq(MediaHierarchyManager.LOCATION_QQS), 534 any<MediaHostState>(), 535 eq(false), 536 anyLong(), 537 anyLong() 538 ) 539 } 540 541 @Test testCommunalLocationnull542 fun testCommunalLocation() = 543 testScope.runTest { 544 keyguardTransitionRepository.sendTransitionSteps( 545 from = KeyguardState.LOCKSCREEN, 546 to = KeyguardState.GLANCEABLE_HUB, 547 testScope = testScope, 548 ) 549 kosmos.fakeCommunalSceneRepository.changeScene(CommunalScenes.Communal) 550 runCurrent() 551 verify(mediaCarouselController) 552 .onDesiredLocationChanged( 553 eq(MediaHierarchyManager.LOCATION_COMMUNAL_HUB), 554 nullable(), 555 eq(false), 556 anyLong(), 557 anyLong() 558 ) 559 clearInvocations(mediaCarouselController) 560 561 keyguardTransitionRepository.sendTransitionSteps( 562 from = KeyguardState.GLANCEABLE_HUB, 563 to = KeyguardState.LOCKSCREEN, 564 testScope = testScope, 565 ) 566 kosmos.fakeCommunalSceneRepository.changeScene(CommunalScenes.Blank) 567 runCurrent() 568 verify(mediaCarouselController) 569 .onDesiredLocationChanged( 570 eq(MediaHierarchyManager.LOCATION_QQS), 571 any<MediaHostState>(), 572 eq(false), 573 anyLong(), 574 anyLong() 575 ) 576 } 577 578 @Test testCommunalLocation_showsOverLockscreennull579 fun testCommunalLocation_showsOverLockscreen() = 580 testScope.runTest { 581 // Device is on lock screen. 582 whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD) 583 584 // UMO goes to communal from the lock screen. 585 keyguardTransitionRepository.sendTransitionSteps( 586 from = KeyguardState.LOCKSCREEN, 587 to = KeyguardState.GLANCEABLE_HUB, 588 testScope = testScope, 589 ) 590 kosmos.fakeCommunalSceneRepository.changeScene(CommunalScenes.Communal) 591 runCurrent() 592 verify(mediaCarouselController) 593 .onDesiredLocationChanged( 594 eq(MediaHierarchyManager.LOCATION_COMMUNAL_HUB), 595 nullable(), 596 eq(false), 597 anyLong(), 598 anyLong() 599 ) 600 } 601 602 @Test testCommunalLocation_showsUntilQsExpandsnull603 fun testCommunalLocation_showsUntilQsExpands() = 604 testScope.runTest { 605 // Device is on lock screen. 606 whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD) 607 608 keyguardTransitionRepository.sendTransitionSteps( 609 from = KeyguardState.LOCKSCREEN, 610 to = KeyguardState.GLANCEABLE_HUB, 611 testScope = testScope, 612 ) 613 kosmos.fakeCommunalSceneRepository.changeScene(CommunalScenes.Communal) 614 runCurrent() 615 verify(mediaCarouselController) 616 .onDesiredLocationChanged( 617 eq(MediaHierarchyManager.LOCATION_COMMUNAL_HUB), 618 nullable(), 619 eq(false), 620 anyLong(), 621 anyLong() 622 ) 623 clearInvocations(mediaCarouselController) 624 625 // Start opening the shade. 626 mediaHierarchyManager.qsExpansion = 0.1f 627 runCurrent() 628 629 // UMO goes to the shade instead. 630 verify(mediaCarouselController) 631 .onDesiredLocationChanged( 632 eq(MediaHierarchyManager.LOCATION_QS), 633 any<MediaHostState>(), 634 eq(false), 635 anyLong(), 636 anyLong() 637 ) 638 } 639 640 @Test testCommunalLocation_whenDreamingAndShadeExpandingnull641 fun testCommunalLocation_whenDreamingAndShadeExpanding() = 642 testScope.runTest { 643 keyguardRepository.setDreaming(true) 644 runCurrent() 645 keyguardTransitionRepository.sendTransitionSteps( 646 from = KeyguardState.DREAMING, 647 to = KeyguardState.GLANCEABLE_HUB, 648 testScope = testScope, 649 ) 650 kosmos.fakeCommunalSceneRepository.changeScene(CommunalScenes.Communal) 651 runCurrent() 652 // Mock the behavior for dreaming that pulling down shade will immediately set QS as 653 // expanded 654 expandQS() 655 // Starts opening the shade 656 shadeExpansion.value = 0.1f 657 runCurrent() 658 659 // UMO shows on hub 660 verify(mediaCarouselController) 661 .onDesiredLocationChanged( 662 eq(MediaHierarchyManager.LOCATION_COMMUNAL_HUB), 663 anyOrNull(), 664 eq(false), 665 anyLong(), 666 anyLong() 667 ) 668 clearInvocations(mediaCarouselController) 669 670 // The shade is opened enough to make QS elements visible 671 shadeExpansion.value = 0.5f 672 runCurrent() 673 674 // UMO shows on QS 675 verify(mediaCarouselController) 676 .onDesiredLocationChanged( 677 eq(MediaHierarchyManager.LOCATION_QS), 678 any<MediaHostState>(), 679 eq(false), 680 anyLong(), 681 anyLong() 682 ) 683 } 684 685 @Test testQsExpandedChanged_noQqsMedianull686 fun testQsExpandedChanged_noQqsMedia() { 687 // When we are looking at QQS with active media 688 whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE) 689 whenever(statusBarStateController.isExpanded).thenReturn(true) 690 691 // When there is no longer any active media 692 whenever(mediaDataManager.hasActiveMediaOrRecommendation()).thenReturn(false) 693 mediaHierarchyManager.qsExpanded = false 694 695 // Then the carousel is set to not visible 696 verify(mediaCarouselScrollHandler).visibleToUser = false 697 assertThat(mediaCarouselScrollHandler.visibleToUser).isFalse() 698 } 699 enableSplitShadenull700 private fun enableSplitShade() { 701 context 702 .getOrCreateTestableResources() 703 .addOverride(R.bool.config_use_split_notification_shade, true) 704 configurationController.notifyConfigurationChanged() 705 } 706 goToLockscreennull707 private fun goToLockscreen() { 708 whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD) 709 settings.putInt(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, 1) 710 statusBarCallback.value.onStatePreChange(StatusBarState.SHADE, StatusBarState.KEYGUARD) 711 whenever(dreamOverlayStateController.isOverlayActive).thenReturn(false) 712 dreamOverlayCallback.value.onStateChanged() 713 clearInvocations(mediaCarouselController) 714 } 715 goToLockedShadenull716 private fun goToLockedShade() { 717 whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE_LOCKED) 718 statusBarCallback.value.onStatePreChange( 719 StatusBarState.KEYGUARD, 720 StatusBarState.SHADE_LOCKED 721 ) 722 } 723 goToDreamnull724 private fun goToDream() { 725 whenever(dreamOverlayStateController.isOverlayActive).thenReturn(true) 726 dreamOverlayCallback.value.onStateChanged() 727 } 728 setMediaDreamComplicationEnablednull729 private fun setMediaDreamComplicationEnabled(enabled: Boolean) { 730 val complications = if (enabled) listOf(mock<MediaDreamComplication>()) else emptyList() 731 whenever(dreamOverlayStateController.complications).thenReturn(complications) 732 dreamOverlayCallback.value.onComplicationsChanged() 733 } 734 expandQSnull735 private fun expandQS() { 736 mediaHierarchyManager.qsExpansion = 1.0f 737 } 738 enterGuidedTransformationnull739 private fun enterGuidedTransformation() { 740 mediaHierarchyManager.qsExpansion = 1.0f 741 goToLockscreen() 742 mediaHierarchyManager.setTransitionToFullShadeAmount(123f) 743 } 744 745 companion object { 746 private const val QQS_TOP = 123 747 private const val QS_TOP = 456 748 private const val LOCKSCREEN_TOP = 789 749 private const val COMMUNAL_TOP = 111 750 } 751 } 752