1 /* 2 * Copyright (C) 2021 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.app.PendingIntent 20 import android.content.res.ColorStateList 21 import android.content.res.Configuration 22 import android.database.ContentObserver 23 import android.os.LocaleList 24 import android.provider.Settings 25 import android.testing.TestableLooper 26 import android.util.MathUtils.abs 27 import android.view.View 28 import androidx.test.ext.junit.runners.AndroidJUnit4 29 import androidx.test.filters.SmallTest 30 import com.android.internal.logging.InstanceId 31 import com.android.keyguard.KeyguardUpdateMonitor 32 import com.android.keyguard.KeyguardUpdateMonitorCallback 33 import com.android.systemui.SysuiTestCase 34 import com.android.systemui.dagger.qualifiers.Main 35 import com.android.systemui.dump.DumpManager 36 import com.android.systemui.flags.DisableSceneContainer 37 import com.android.systemui.flags.EnableSceneContainer 38 import com.android.systemui.flags.Flags 39 import com.android.systemui.flags.fakeFeatureFlagsClassic 40 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository 41 import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor 42 import com.android.systemui.keyguard.shared.model.KeyguardState 43 import com.android.systemui.kosmos.testDispatcher 44 import com.android.systemui.kosmos.testScope 45 import com.android.systemui.media.controls.MediaTestUtils 46 import com.android.systemui.media.controls.domain.pipeline.EMPTY_SMARTSPACE_MEDIA_DATA 47 import com.android.systemui.media.controls.domain.pipeline.MediaDataManager 48 import com.android.systemui.media.controls.shared.model.MediaData 49 import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager.Companion.LOCATION_QS 50 import com.android.systemui.media.controls.ui.view.MediaHostState 51 import com.android.systemui.media.controls.ui.view.MediaScrollView 52 import com.android.systemui.media.controls.ui.viewmodel.mediaCarouselViewModel 53 import com.android.systemui.media.controls.util.MediaFlags 54 import com.android.systemui.media.controls.util.MediaUiEventLogger 55 import com.android.systemui.plugins.ActivityStarter 56 import com.android.systemui.plugins.FalsingManager 57 import com.android.systemui.qs.PageIndicator 58 import com.android.systemui.res.R 59 import com.android.systemui.scene.data.repository.Idle 60 import com.android.systemui.scene.data.repository.setSceneTransition 61 import com.android.systemui.scene.domain.interactor.sceneInteractor 62 import com.android.systemui.scene.shared.model.Scenes 63 import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener 64 import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider 65 import com.android.systemui.statusbar.policy.ConfigurationController 66 import com.android.systemui.testKosmos 67 import com.android.systemui.util.concurrency.DelayableExecutor 68 import com.android.systemui.util.concurrency.FakeExecutor 69 import com.android.systemui.util.settings.FakeSettings 70 import com.android.systemui.util.settings.GlobalSettings 71 import com.android.systemui.util.settings.SecureSettings 72 import com.android.systemui.util.time.FakeSystemClock 73 import java.util.Locale 74 import javax.inject.Provider 75 import junit.framework.Assert.assertEquals 76 import junit.framework.Assert.assertFalse 77 import junit.framework.Assert.assertTrue 78 import kotlinx.coroutines.ExperimentalCoroutinesApi 79 import kotlinx.coroutines.test.TestDispatcher 80 import kotlinx.coroutines.test.UnconfinedTestDispatcher 81 import kotlinx.coroutines.test.runTest 82 import org.junit.After 83 import org.junit.Before 84 import org.junit.Test 85 import org.junit.runner.RunWith 86 import org.mockito.ArgumentCaptor 87 import org.mockito.Captor 88 import org.mockito.Mock 89 import org.mockito.Mockito.anyLong 90 import org.mockito.Mockito.floatThat 91 import org.mockito.Mockito.mock 92 import org.mockito.Mockito.never 93 import org.mockito.Mockito.reset 94 import org.mockito.Mockito.times 95 import org.mockito.Mockito.verify 96 import org.mockito.Mockito.`when` as whenever 97 import org.mockito.MockitoAnnotations 98 import org.mockito.kotlin.any 99 import org.mockito.kotlin.capture 100 import org.mockito.kotlin.eq 101 102 private val DATA = MediaTestUtils.emptyMediaData 103 104 private val SMARTSPACE_KEY = "smartspace" 105 private const val PAUSED_LOCAL = "paused local" 106 private const val PLAYING_LOCAL = "playing local" 107 108 @SmallTest 109 @TestableLooper.RunWithLooper(setAsMainLooper = true) 110 @RunWith(AndroidJUnit4::class) 111 class MediaCarouselControllerTest : SysuiTestCase() { 112 val kosmos = testKosmos() 113 114 @Mock lateinit var mediaControlPanelFactory: Provider<MediaControlPanel> 115 @Mock lateinit var mediaViewControllerFactory: Provider<MediaViewController> 116 @Mock lateinit var panel: MediaControlPanel 117 @Mock lateinit var visualStabilityProvider: VisualStabilityProvider 118 @Mock lateinit var mediaHostStatesManager: MediaHostStatesManager 119 @Mock lateinit var mediaHostState: MediaHostState 120 @Mock lateinit var activityStarter: ActivityStarter 121 @Mock @Main private lateinit var executor: DelayableExecutor 122 @Mock lateinit var mediaDataManager: MediaDataManager 123 @Mock lateinit var configurationController: ConfigurationController 124 @Mock lateinit var falsingManager: FalsingManager 125 @Mock lateinit var dumpManager: DumpManager 126 @Mock lateinit var logger: MediaUiEventLogger 127 @Mock lateinit var debugLogger: MediaCarouselControllerLogger 128 @Mock lateinit var mediaViewController: MediaViewController 129 @Mock lateinit var mediaCarousel: MediaScrollView 130 @Mock lateinit var pageIndicator: PageIndicator 131 @Mock lateinit var mediaFlags: MediaFlags 132 @Mock lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor 133 @Mock lateinit var globalSettings: GlobalSettings 134 private lateinit var secureSettings: SecureSettings 135 private val transitionRepository = kosmos.fakeKeyguardTransitionRepository 136 @Captor lateinit var listener: ArgumentCaptor<MediaDataManager.Listener> 137 @Captor 138 lateinit var configListener: ArgumentCaptor<ConfigurationController.ConfigurationListener> 139 @Captor lateinit var visualStabilityCallback: ArgumentCaptor<OnReorderingAllowedListener> 140 @Captor lateinit var keyguardCallback: ArgumentCaptor<KeyguardUpdateMonitorCallback> 141 @Captor lateinit var hostStateCallback: ArgumentCaptor<MediaHostStatesManager.Callback> 142 @Captor lateinit var settingsObserverCaptor: ArgumentCaptor<ContentObserver> 143 144 private val clock = FakeSystemClock() 145 private lateinit var bgExecutor: FakeExecutor 146 private lateinit var testDispatcher: TestDispatcher 147 private lateinit var mediaCarouselController: MediaCarouselController 148 149 private var originalResumeSetting = 150 Settings.Secure.getInt(context.contentResolver, Settings.Secure.MEDIA_CONTROLS_RESUME, 1) 151 152 @Before setupnull153 fun setup() { 154 MockitoAnnotations.initMocks(this) 155 secureSettings = FakeSettings() 156 context.resources.configuration.setLocales(LocaleList(Locale.US, Locale.UK)) 157 bgExecutor = FakeExecutor(clock) 158 testDispatcher = UnconfinedTestDispatcher() 159 mediaCarouselController = 160 MediaCarouselController( 161 context = context, 162 mediaControlPanelFactory = mediaControlPanelFactory, 163 visualStabilityProvider = visualStabilityProvider, 164 mediaHostStatesManager = mediaHostStatesManager, 165 activityStarter = activityStarter, 166 systemClock = clock, 167 mainDispatcher = kosmos.testDispatcher, 168 executor = executor, 169 bgExecutor = bgExecutor, 170 backgroundDispatcher = testDispatcher, 171 mediaManager = mediaDataManager, 172 configurationController = configurationController, 173 falsingManager = falsingManager, 174 dumpManager = dumpManager, 175 logger = logger, 176 debugLogger = debugLogger, 177 mediaFlags = mediaFlags, 178 keyguardUpdateMonitor = keyguardUpdateMonitor, 179 keyguardTransitionInteractor = kosmos.keyguardTransitionInteractor, 180 globalSettings = globalSettings, 181 secureSettings = secureSettings, 182 mediaCarouselViewModel = kosmos.mediaCarouselViewModel, 183 mediaViewControllerFactory = mediaViewControllerFactory, 184 sceneInteractor = kosmos.sceneInteractor, 185 ) 186 verify(configurationController).addCallback(capture(configListener)) 187 verify(mediaDataManager).addListener(capture(listener)) 188 verify(visualStabilityProvider) 189 .addPersistentReorderingAllowedListener(capture(visualStabilityCallback)) 190 verify(keyguardUpdateMonitor).registerCallback(capture(keyguardCallback)) 191 verify(mediaHostStatesManager).addCallback(capture(hostStateCallback)) 192 whenever(mediaControlPanelFactory.get()).thenReturn(panel) 193 whenever(panel.mediaViewController).thenReturn(mediaViewController) 194 whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(false) 195 MediaPlayerData.clear() 196 FakeExecutor.exhaustExecutors(bgExecutor) 197 verify(globalSettings) 198 .registerContentObserverSync( 199 eq(Settings.Global.getUriFor(Settings.Global.ANIMATOR_DURATION_SCALE)), 200 capture(settingsObserverCaptor) 201 ) 202 } 203 204 @After tearDownnull205 fun tearDown() { 206 Settings.Secure.putInt( 207 context.contentResolver, 208 Settings.Secure.MEDIA_CONTROLS_RESUME, 209 originalResumeSetting 210 ) 211 } 212 213 @Test testPlayerOrderingnull214 fun testPlayerOrdering() { 215 // Test values: key, data, last active time 216 val playingLocal = 217 Triple( 218 PLAYING_LOCAL, 219 DATA.copy( 220 active = true, 221 isPlaying = true, 222 playbackLocation = MediaData.PLAYBACK_LOCAL, 223 resumption = false 224 ), 225 4500L 226 ) 227 228 val playingCast = 229 Triple( 230 "playing cast", 231 DATA.copy( 232 active = true, 233 isPlaying = true, 234 playbackLocation = MediaData.PLAYBACK_CAST_LOCAL, 235 resumption = false 236 ), 237 5000L 238 ) 239 240 val pausedLocal = 241 Triple( 242 PAUSED_LOCAL, 243 DATA.copy( 244 active = true, 245 isPlaying = false, 246 playbackLocation = MediaData.PLAYBACK_LOCAL, 247 resumption = false 248 ), 249 1000L 250 ) 251 252 val pausedCast = 253 Triple( 254 "paused cast", 255 DATA.copy( 256 active = true, 257 isPlaying = false, 258 playbackLocation = MediaData.PLAYBACK_CAST_LOCAL, 259 resumption = false 260 ), 261 2000L 262 ) 263 264 val playingRcn = 265 Triple( 266 "playing RCN", 267 DATA.copy( 268 active = true, 269 isPlaying = true, 270 playbackLocation = MediaData.PLAYBACK_CAST_REMOTE, 271 resumption = false 272 ), 273 5000L 274 ) 275 276 val pausedRcn = 277 Triple( 278 "paused RCN", 279 DATA.copy( 280 active = true, 281 isPlaying = false, 282 playbackLocation = MediaData.PLAYBACK_CAST_REMOTE, 283 resumption = false 284 ), 285 5000L 286 ) 287 288 val active = 289 Triple( 290 "active", 291 DATA.copy( 292 active = true, 293 isPlaying = false, 294 playbackLocation = MediaData.PLAYBACK_LOCAL, 295 resumption = true 296 ), 297 250L 298 ) 299 300 val resume1 = 301 Triple( 302 "resume 1", 303 DATA.copy( 304 active = false, 305 isPlaying = false, 306 playbackLocation = MediaData.PLAYBACK_LOCAL, 307 resumption = true 308 ), 309 500L 310 ) 311 312 val resume2 = 313 Triple( 314 "resume 2", 315 DATA.copy( 316 active = false, 317 isPlaying = false, 318 playbackLocation = MediaData.PLAYBACK_LOCAL, 319 resumption = true 320 ), 321 1000L 322 ) 323 324 val activeMoreRecent = 325 Triple( 326 "active more recent", 327 DATA.copy( 328 active = false, 329 isPlaying = false, 330 playbackLocation = MediaData.PLAYBACK_LOCAL, 331 resumption = true, 332 lastActive = 2L 333 ), 334 1000L 335 ) 336 337 val activeLessRecent = 338 Triple( 339 "active less recent", 340 DATA.copy( 341 active = false, 342 isPlaying = false, 343 playbackLocation = MediaData.PLAYBACK_LOCAL, 344 resumption = true, 345 lastActive = 1L 346 ), 347 1000L 348 ) 349 // Expected ordering for media players: 350 // Actively playing local sessions 351 // Actively playing cast sessions 352 // Paused local and cast sessions, by last active 353 // RCNs 354 // Resume controls, by last active 355 356 val expected = 357 listOf( 358 playingLocal, 359 playingCast, 360 pausedCast, 361 pausedLocal, 362 playingRcn, 363 pausedRcn, 364 active, 365 resume2, 366 resume1 367 ) 368 369 expected.forEach { 370 clock.setCurrentTimeMillis(it.third) 371 MediaPlayerData.addMediaPlayer( 372 it.first, 373 it.second.copy(notificationKey = it.first), 374 panel, 375 clock, 376 isSsReactivated = false 377 ) 378 } 379 380 for ((index, key) in MediaPlayerData.playerKeys().withIndex()) { 381 assertEquals(expected.get(index).first, key.data.notificationKey) 382 } 383 384 for ((index, key) in MediaPlayerData.visiblePlayerKeys().withIndex()) { 385 assertEquals(expected.get(index).first, key.data.notificationKey) 386 } 387 } 388 389 @Test testOrderWithSmartspace_prioritizednull390 fun testOrderWithSmartspace_prioritized() { 391 testPlayerOrdering() 392 393 // If smartspace is prioritized 394 MediaPlayerData.addMediaRecommendation( 395 SMARTSPACE_KEY, 396 EMPTY_SMARTSPACE_MEDIA_DATA.copy(isActive = true), 397 panel, 398 true, 399 clock 400 ) 401 402 // Then it should be shown immediately after any actively playing controls 403 assertTrue(MediaPlayerData.playerKeys().elementAt(2).isSsMediaRec) 404 } 405 406 @Test testOrderWithSmartspace_prioritized_updatingVisibleMediaPlayersnull407 fun testOrderWithSmartspace_prioritized_updatingVisibleMediaPlayers() { 408 testPlayerOrdering() 409 410 // If smartspace is prioritized 411 listener.value.onSmartspaceMediaDataLoaded( 412 SMARTSPACE_KEY, 413 EMPTY_SMARTSPACE_MEDIA_DATA.copy(isActive = true), 414 true 415 ) 416 417 // Then it should be shown immediately after any actively playing controls 418 assertTrue(MediaPlayerData.playerKeys().elementAt(2).isSsMediaRec) 419 assertTrue(MediaPlayerData.visiblePlayerKeys().elementAt(2).isSsMediaRec) 420 } 421 422 @Test testOrderWithSmartspace_notPrioritizednull423 fun testOrderWithSmartspace_notPrioritized() { 424 testPlayerOrdering() 425 426 // If smartspace is not prioritized 427 MediaPlayerData.addMediaRecommendation( 428 SMARTSPACE_KEY, 429 EMPTY_SMARTSPACE_MEDIA_DATA.copy(isActive = true), 430 panel, 431 false, 432 clock 433 ) 434 435 // Then it should be shown at the end of the carousel's active entries 436 val idx = MediaPlayerData.playerKeys().count { it.data.active } - 1 437 assertTrue(MediaPlayerData.playerKeys().elementAt(idx).isSsMediaRec) 438 } 439 440 @Test testPlayingExistingMediaPlayerFromCarousel_visibleMediaPlayersNotUpdatednull441 fun testPlayingExistingMediaPlayerFromCarousel_visibleMediaPlayersNotUpdated() { 442 testPlayerOrdering() 443 // playing paused player 444 listener.value.onMediaDataLoaded( 445 PAUSED_LOCAL, 446 PAUSED_LOCAL, 447 DATA.copy( 448 active = true, 449 isPlaying = true, 450 playbackLocation = MediaData.PLAYBACK_LOCAL, 451 resumption = false 452 ) 453 ) 454 listener.value.onMediaDataLoaded( 455 PLAYING_LOCAL, 456 PLAYING_LOCAL, 457 DATA.copy( 458 active = true, 459 isPlaying = false, 460 playbackLocation = MediaData.PLAYBACK_LOCAL, 461 resumption = true 462 ) 463 ) 464 465 assertEquals( 466 MediaPlayerData.getMediaPlayerIndex(PAUSED_LOCAL), 467 mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex 468 ) 469 // paused player order should stays the same in visibleMediaPLayer map. 470 // paused player order should be first in mediaPlayer map. 471 assertEquals( 472 MediaPlayerData.visiblePlayerKeys().elementAt(3), 473 MediaPlayerData.playerKeys().elementAt(0) 474 ) 475 } 476 477 @Test testSwipeDismiss_loggednull478 fun testSwipeDismiss_logged() { 479 mediaCarouselController.mediaCarouselScrollHandler.dismissCallback.invoke() 480 481 verify(logger).logSwipeDismiss() 482 } 483 484 @Test testSettingsButton_loggednull485 fun testSettingsButton_logged() { 486 mediaCarouselController.settingsButton.callOnClick() 487 488 verify(logger).logCarouselSettings() 489 } 490 491 @Test testLocationChangeQs_loggednull492 fun testLocationChangeQs_logged() { 493 mediaCarouselController.onDesiredLocationChanged( 494 LOCATION_QS, 495 mediaHostState, 496 animate = false 497 ) 498 bgExecutor.runAllReady() 499 verify(logger).logCarouselPosition(LOCATION_QS) 500 } 501 502 @Test testLocationChangeQqs_loggednull503 fun testLocationChangeQqs_logged() { 504 mediaCarouselController.onDesiredLocationChanged( 505 MediaHierarchyManager.LOCATION_QQS, 506 mediaHostState, 507 animate = false 508 ) 509 bgExecutor.runAllReady() 510 verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_QQS) 511 } 512 513 @Test testLocationChangeLockscreen_loggednull514 fun testLocationChangeLockscreen_logged() { 515 mediaCarouselController.onDesiredLocationChanged( 516 MediaHierarchyManager.LOCATION_LOCKSCREEN, 517 mediaHostState, 518 animate = false 519 ) 520 bgExecutor.runAllReady() 521 verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_LOCKSCREEN) 522 } 523 524 @Test testLocationChangeDream_loggednull525 fun testLocationChangeDream_logged() { 526 mediaCarouselController.onDesiredLocationChanged( 527 MediaHierarchyManager.LOCATION_DREAM_OVERLAY, 528 mediaHostState, 529 animate = false 530 ) 531 bgExecutor.runAllReady() 532 verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_DREAM_OVERLAY) 533 } 534 535 @Test testRecommendationRemoved_loggednull536 fun testRecommendationRemoved_logged() { 537 val packageName = "smartspace package" 538 val instanceId = InstanceId.fakeInstanceId(123) 539 540 val smartspaceData = 541 EMPTY_SMARTSPACE_MEDIA_DATA.copy(packageName = packageName, instanceId = instanceId) 542 MediaPlayerData.addMediaRecommendation(SMARTSPACE_KEY, smartspaceData, panel, true, clock) 543 mediaCarouselController.removePlayer(SMARTSPACE_KEY) 544 545 verify(logger).logRecommendationRemoved(eq(packageName), eq(instanceId!!)) 546 } 547 548 @Test testMediaLoaded_ScrollToActivePlayernull549 fun testMediaLoaded_ScrollToActivePlayer() { 550 listener.value.onMediaDataLoaded( 551 PLAYING_LOCAL, 552 null, 553 DATA.copy( 554 active = true, 555 isPlaying = true, 556 playbackLocation = MediaData.PLAYBACK_LOCAL, 557 resumption = false 558 ) 559 ) 560 listener.value.onMediaDataLoaded( 561 PAUSED_LOCAL, 562 null, 563 DATA.copy( 564 active = true, 565 isPlaying = false, 566 playbackLocation = MediaData.PLAYBACK_LOCAL, 567 resumption = false 568 ) 569 ) 570 // adding a media recommendation card. 571 listener.value.onSmartspaceMediaDataLoaded( 572 SMARTSPACE_KEY, 573 EMPTY_SMARTSPACE_MEDIA_DATA, 574 false 575 ) 576 mediaCarouselController.shouldScrollToKey = true 577 // switching between media players. 578 listener.value.onMediaDataLoaded( 579 PLAYING_LOCAL, 580 PLAYING_LOCAL, 581 DATA.copy( 582 active = true, 583 isPlaying = false, 584 playbackLocation = MediaData.PLAYBACK_LOCAL, 585 resumption = true 586 ) 587 ) 588 listener.value.onMediaDataLoaded( 589 PAUSED_LOCAL, 590 PAUSED_LOCAL, 591 DATA.copy( 592 active = true, 593 isPlaying = true, 594 playbackLocation = MediaData.PLAYBACK_LOCAL, 595 resumption = false 596 ) 597 ) 598 599 assertEquals( 600 MediaPlayerData.getMediaPlayerIndex(PAUSED_LOCAL), 601 mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex 602 ) 603 } 604 605 @Test testMediaLoadedFromRecommendationCard_ScrollToActivePlayernull606 fun testMediaLoadedFromRecommendationCard_ScrollToActivePlayer() { 607 listener.value.onSmartspaceMediaDataLoaded( 608 SMARTSPACE_KEY, 609 EMPTY_SMARTSPACE_MEDIA_DATA.copy(packageName = "PACKAGE_NAME", isActive = true), 610 false 611 ) 612 listener.value.onMediaDataLoaded( 613 PLAYING_LOCAL, 614 null, 615 DATA.copy( 616 active = true, 617 isPlaying = true, 618 playbackLocation = MediaData.PLAYBACK_LOCAL, 619 resumption = false 620 ) 621 ) 622 623 var playerIndex = MediaPlayerData.getMediaPlayerIndex(PLAYING_LOCAL) 624 assertEquals( 625 playerIndex, 626 mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex 627 ) 628 assertEquals(playerIndex, 0) 629 630 // Replaying the same media player one more time. 631 // And check that the card stays in its position. 632 mediaCarouselController.shouldScrollToKey = true 633 listener.value.onMediaDataLoaded( 634 PLAYING_LOCAL, 635 null, 636 DATA.copy( 637 active = true, 638 isPlaying = true, 639 playbackLocation = MediaData.PLAYBACK_LOCAL, 640 resumption = false, 641 packageName = "PACKAGE_NAME" 642 ) 643 ) 644 playerIndex = MediaPlayerData.getMediaPlayerIndex(PLAYING_LOCAL) 645 assertEquals(playerIndex, 0) 646 } 647 648 @Test testRecommendationRemovedWhileNotVisible_updateHostVisibilitynull649 fun testRecommendationRemovedWhileNotVisible_updateHostVisibility() { 650 var result = false 651 mediaCarouselController.updateHostVisibility = { result = true } 652 653 whenever(visualStabilityProvider.isReorderingAllowed).thenReturn(true) 654 listener.value.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY, false) 655 656 assertEquals(true, result) 657 } 658 659 @Test testRecommendationRemovedWhileVisible_thenReorders_updateHostVisibilitynull660 fun testRecommendationRemovedWhileVisible_thenReorders_updateHostVisibility() { 661 var result = false 662 mediaCarouselController.updateHostVisibility = { result = true } 663 664 whenever(visualStabilityProvider.isReorderingAllowed).thenReturn(false) 665 listener.value.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY, false) 666 assertEquals(false, result) 667 668 visualStabilityCallback.value.onReorderingAllowed() 669 assertEquals(true, result) 670 } 671 672 @Test testGetCurrentVisibleMediaContentIntentnull673 fun testGetCurrentVisibleMediaContentIntent() { 674 val clickIntent1 = mock(PendingIntent::class.java) 675 val player1 = Triple("player1", DATA.copy(clickIntent = clickIntent1), 1000L) 676 clock.setCurrentTimeMillis(player1.third) 677 MediaPlayerData.addMediaPlayer( 678 player1.first, 679 player1.second.copy(notificationKey = player1.first), 680 panel, 681 clock, 682 isSsReactivated = false 683 ) 684 685 assertEquals(mediaCarouselController.getCurrentVisibleMediaContentIntent(), clickIntent1) 686 687 val clickIntent2 = mock(PendingIntent::class.java) 688 val player2 = Triple("player2", DATA.copy(clickIntent = clickIntent2), 2000L) 689 clock.setCurrentTimeMillis(player2.third) 690 MediaPlayerData.addMediaPlayer( 691 player2.first, 692 player2.second.copy(notificationKey = player2.first), 693 panel, 694 clock, 695 isSsReactivated = false 696 ) 697 698 // mediaCarouselScrollHandler.visibleMediaIndex is unchanged (= 0), and the new player is 699 // added to the front because it was active more recently. 700 assertEquals(mediaCarouselController.getCurrentVisibleMediaContentIntent(), clickIntent2) 701 702 val clickIntent3 = mock(PendingIntent::class.java) 703 val player3 = Triple("player3", DATA.copy(clickIntent = clickIntent3), 500L) 704 clock.setCurrentTimeMillis(player3.third) 705 MediaPlayerData.addMediaPlayer( 706 player3.first, 707 player3.second.copy(notificationKey = player3.first), 708 panel, 709 clock, 710 isSsReactivated = false 711 ) 712 713 // mediaCarouselScrollHandler.visibleMediaIndex is unchanged (= 0), and the new player is 714 // added to the end because it was active less recently. 715 assertEquals(mediaCarouselController.getCurrentVisibleMediaContentIntent(), clickIntent2) 716 } 717 718 @Test testSetCurrentState_UpdatePageIndicatorAlphaWhenSquishnull719 fun testSetCurrentState_UpdatePageIndicatorAlphaWhenSquish() { 720 val delta = 0.0001F 721 mediaCarouselController.mediaCarousel = mediaCarousel 722 mediaCarouselController.pageIndicator = pageIndicator 723 whenever(mediaCarousel.measuredHeight).thenReturn(100) 724 whenever(pageIndicator.translationY).thenReturn(80F) 725 whenever(pageIndicator.height).thenReturn(10) 726 whenever(mediaHostStatesManager.mediaHostStates) 727 .thenReturn(mutableMapOf(LOCATION_QS to mediaHostState)) 728 whenever(mediaHostState.visible).thenReturn(true) 729 mediaCarouselController.currentEndLocation = LOCATION_QS 730 whenever(mediaHostState.squishFraction).thenReturn(0.938F) 731 mediaCarouselController.updatePageIndicatorAlpha() 732 verify(pageIndicator).alpha = floatThat { abs(it - 0.5F) < delta } 733 734 whenever(mediaHostState.squishFraction).thenReturn(1.0F) 735 mediaCarouselController.updatePageIndicatorAlpha() 736 verify(pageIndicator).alpha = floatThat { abs(it - 1.0F) < delta } 737 } 738 739 @Test testOnConfigChanged_playersAreAddedBacknull740 fun testOnConfigChanged_playersAreAddedBack() { 741 testConfigurationChange { configListener.value.onConfigChanged(Configuration()) } 742 } 743 744 @Test testOnUiModeChanged_playersAreAddedBacknull745 fun testOnUiModeChanged_playersAreAddedBack() { 746 testConfigurationChange(configListener.value::onUiModeChanged) 747 748 verify(pageIndicator).tintList = 749 ColorStateList.valueOf(context.getColor(R.color.media_paging_indicator)) 750 verify(pageIndicator, times(2)).setNumPages(any()) 751 } 752 753 @Test testOnDensityOrFontScaleChanged_playersAreAddedBacknull754 fun testOnDensityOrFontScaleChanged_playersAreAddedBack() { 755 testConfigurationChange(configListener.value::onDensityOrFontScaleChanged) 756 757 verify(pageIndicator).tintList = 758 ColorStateList.valueOf(context.getColor(R.color.media_paging_indicator)) 759 // when recreateMedia is set to true, page indicator is updated on removal and addition. 760 verify(pageIndicator, times(4)).setNumPages(any()) 761 } 762 763 @Test testOnThemeChanged_playersAreAddedBacknull764 fun testOnThemeChanged_playersAreAddedBack() { 765 testConfigurationChange(configListener.value::onThemeChanged) 766 767 verify(pageIndicator).tintList = 768 ColorStateList.valueOf(context.getColor(R.color.media_paging_indicator)) 769 verify(pageIndicator, times(2)).setNumPages(any()) 770 } 771 772 @Test testOnLocaleListChanged_playersAreAddedBacknull773 fun testOnLocaleListChanged_playersAreAddedBack() { 774 context.resources.configuration.setLocales(LocaleList(Locale.US, Locale.UK, Locale.CANADA)) 775 testConfigurationChange(configListener.value::onLocaleListChanged) 776 777 verify(pageIndicator, never()).tintList = 778 ColorStateList.valueOf(context.getColor(R.color.media_paging_indicator)) 779 780 context.resources.configuration.setLocales(LocaleList(Locale.UK, Locale.US, Locale.CANADA)) 781 testConfigurationChange(configListener.value::onLocaleListChanged) 782 783 verify(pageIndicator).tintList = 784 ColorStateList.valueOf(context.getColor(R.color.media_paging_indicator)) 785 // When recreateMedia is set to true, page indicator is updated on removal and addition. 786 verify(pageIndicator, times(4)).setNumPages(any()) 787 } 788 789 @Test testRecommendation_persistentEnabled_newSmartspaceLoaded_updatesSortnull790 fun testRecommendation_persistentEnabled_newSmartspaceLoaded_updatesSort() { 791 testRecommendation_persistentEnabled_inactiveSmartspaceDataLoaded_isAdded() 792 793 // When an update to existing smartspace data is loaded 794 listener.value.onSmartspaceMediaDataLoaded( 795 SMARTSPACE_KEY, 796 EMPTY_SMARTSPACE_MEDIA_DATA.copy(isActive = true), 797 true 798 ) 799 800 // Then the carousel is updated 801 assertTrue(MediaPlayerData.playerKeys().elementAt(0).data.active) 802 assertTrue(MediaPlayerData.visiblePlayerKeys().elementAt(0).data.active) 803 } 804 805 @Test testRecommendation_persistentEnabled_inactiveSmartspaceDataLoaded_isAddednull806 fun testRecommendation_persistentEnabled_inactiveSmartspaceDataLoaded_isAdded() { 807 whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true) 808 809 // When inactive smartspace data is loaded 810 listener.value.onSmartspaceMediaDataLoaded( 811 SMARTSPACE_KEY, 812 EMPTY_SMARTSPACE_MEDIA_DATA, 813 false 814 ) 815 816 // Then it is added to the carousel with correct state 817 assertTrue(MediaPlayerData.playerKeys().elementAt(0).isSsMediaRec) 818 assertFalse(MediaPlayerData.playerKeys().elementAt(0).data.active) 819 820 assertTrue(MediaPlayerData.visiblePlayerKeys().elementAt(0).isSsMediaRec) 821 assertFalse(MediaPlayerData.visiblePlayerKeys().elementAt(0).data.active) 822 } 823 824 @Test testOnLockDownMode_hideMediaCarouselnull825 fun testOnLockDownMode_hideMediaCarousel() { 826 whenever(keyguardUpdateMonitor.isUserInLockdown(context.userId)).thenReturn(true) 827 mediaCarouselController.mediaCarousel = mediaCarousel 828 829 keyguardCallback.value.onStrongAuthStateChanged(context.userId) 830 831 verify(mediaCarousel).visibility = View.GONE 832 } 833 834 @Test testLockDownModeOff_showMediaCarouselnull835 fun testLockDownModeOff_showMediaCarousel() { 836 whenever(keyguardUpdateMonitor.isUserInLockdown(context.userId)).thenReturn(false) 837 whenever(keyguardUpdateMonitor.isUserUnlocked(context.userId)).thenReturn(true) 838 mediaCarouselController.mediaCarousel = mediaCarousel 839 840 keyguardCallback.value.onStrongAuthStateChanged(context.userId) 841 842 verify(mediaCarousel).visibility = View.VISIBLE 843 } 844 845 @DisableSceneContainer 846 @ExperimentalCoroutinesApi 847 @Test testKeyguardGone_showMediaCarouselnull848 fun testKeyguardGone_showMediaCarousel() = 849 kosmos.testScope.runTest { 850 kosmos.fakeFeatureFlagsClassic.set(Flags.MEDIA_RETAIN_RECOMMENDATIONS, false) 851 var updatedVisibility = false 852 mediaCarouselController.updateHostVisibility = { updatedVisibility = true } 853 mediaCarouselController.mediaCarousel = mediaCarousel 854 855 val job = mediaCarouselController.listenForAnyStateToGoneKeyguardTransition(this) 856 transitionRepository.sendTransitionSteps( 857 from = KeyguardState.LOCKSCREEN, 858 to = KeyguardState.GONE, 859 this 860 ) 861 862 verify(mediaCarousel).visibility = View.VISIBLE 863 assertEquals(true, updatedVisibility) 864 assertEquals(false, mediaCarouselController.isLockedAndHidden()) 865 866 job.cancel() 867 } 868 869 @EnableSceneContainer 870 @ExperimentalCoroutinesApi 871 @Test testKeyguardGone_showMediaCarousel_scene_containernull872 fun testKeyguardGone_showMediaCarousel_scene_container() = 873 kosmos.testScope.runTest { 874 kosmos.fakeFeatureFlagsClassic.set(Flags.MEDIA_RETAIN_RECOMMENDATIONS, false) 875 var updatedVisibility = false 876 mediaCarouselController.updateHostVisibility = { updatedVisibility = true } 877 mediaCarouselController.mediaCarousel = mediaCarousel 878 879 val job = mediaCarouselController.listenForAnyStateToGoneKeyguardTransition(this) 880 kosmos.setSceneTransition(Idle(Scenes.Gone)) 881 882 verify(mediaCarousel).visibility = View.VISIBLE 883 assertEquals(true, updatedVisibility) 884 885 job.cancel() 886 } 887 888 @ExperimentalCoroutinesApi 889 @Test keyguardShowing_notAllowedOnLockscreen_updateVisibilitynull890 fun keyguardShowing_notAllowedOnLockscreen_updateVisibility() { 891 kosmos.testScope.runTest { 892 kosmos.fakeFeatureFlagsClassic.set(Flags.MEDIA_RETAIN_RECOMMENDATIONS, false) 893 var updatedVisibility = false 894 mediaCarouselController.updateHostVisibility = { updatedVisibility = true } 895 mediaCarouselController.mediaCarousel = mediaCarousel 896 897 val settingsJob = mediaCarouselController.listenForLockscreenSettingChanges(this) 898 secureSettings.putBool(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, false) 899 900 val keyguardJob = mediaCarouselController.listenForAnyStateToLockscreenTransition(this) 901 transitionRepository.sendTransitionSteps( 902 from = KeyguardState.GONE, 903 to = KeyguardState.LOCKSCREEN, 904 this 905 ) 906 907 assertEquals(true, updatedVisibility) 908 assertEquals(true, mediaCarouselController.isLockedAndHidden()) 909 910 settingsJob.cancel() 911 keyguardJob.cancel() 912 } 913 } 914 915 @ExperimentalCoroutinesApi 916 @Test keyguardShowing_allowedOnLockscreen_updateVisibilitynull917 fun keyguardShowing_allowedOnLockscreen_updateVisibility() { 918 kosmos.testScope.runTest { 919 kosmos.fakeFeatureFlagsClassic.set(Flags.MEDIA_RETAIN_RECOMMENDATIONS, false) 920 var updatedVisibility = false 921 mediaCarouselController.updateHostVisibility = { updatedVisibility = true } 922 mediaCarouselController.mediaCarousel = mediaCarousel 923 924 val settingsJob = mediaCarouselController.listenForLockscreenSettingChanges(this) 925 secureSettings.putBool(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, true) 926 927 val keyguardJob = mediaCarouselController.listenForAnyStateToLockscreenTransition(this) 928 transitionRepository.sendTransitionSteps( 929 from = KeyguardState.GONE, 930 to = KeyguardState.LOCKSCREEN, 931 this 932 ) 933 934 assertEquals(true, updatedVisibility) 935 assertEquals(false, mediaCarouselController.isLockedAndHidden()) 936 937 settingsJob.cancel() 938 keyguardJob.cancel() 939 } 940 } 941 942 @Test testInvisibleToUserAndExpanded_playersNotListeningnull943 fun testInvisibleToUserAndExpanded_playersNotListening() { 944 // Add players to carousel. 945 testPlayerOrdering() 946 947 // Make the carousel visible to user in expanded layout. 948 mediaCarouselController.currentlyExpanded = true 949 mediaCarouselController.mediaCarouselScrollHandler.visibleToUser = true 950 951 // panel is the player for each MediaPlayerData. 952 // Verify that seekbar listening attribute in media control panel is set to true. 953 verify(panel, times(MediaPlayerData.players().size)).listening = true 954 955 // Make the carousel invisible to user. 956 mediaCarouselController.mediaCarouselScrollHandler.visibleToUser = false 957 958 // panel is the player for each MediaPlayerData. 959 // Verify that seekbar listening attribute in media control panel is set to false. 960 verify(panel, times(MediaPlayerData.players().size)).listening = false 961 } 962 963 @Test testVisibleToUserAndExpanded_playersListeningnull964 fun testVisibleToUserAndExpanded_playersListening() { 965 // Add players to carousel. 966 testPlayerOrdering() 967 968 // Make the carousel visible to user in expanded layout. 969 mediaCarouselController.currentlyExpanded = true 970 mediaCarouselController.mediaCarouselScrollHandler.visibleToUser = true 971 972 // panel is the player for each MediaPlayerData. 973 // Verify that seekbar listening attribute in media control panel is set to true. 974 verify(panel, times(MediaPlayerData.players().size)).listening = true 975 } 976 977 @Test testUMOCollapsed_playersNotListeningnull978 fun testUMOCollapsed_playersNotListening() { 979 // Add players to carousel. 980 testPlayerOrdering() 981 982 // Make the carousel in collapsed layout. 983 mediaCarouselController.currentlyExpanded = false 984 985 // panel is the player for each MediaPlayerData. 986 // Verify that seekbar listening attribute in media control panel is set to false. 987 verify(panel, times(MediaPlayerData.players().size)).listening = false 988 989 // Make the carousel visible to user. 990 reset(panel) 991 mediaCarouselController.mediaCarouselScrollHandler.visibleToUser = true 992 993 // Verify that seekbar listening attribute in media control panel is set to false. 994 verify(panel, times(MediaPlayerData.players().size)).listening = false 995 } 996 997 @Test testOnHostStateChanged_updateVisibilitynull998 fun testOnHostStateChanged_updateVisibility() { 999 var stateUpdated = false 1000 mediaCarouselController.updateUserVisibility = { stateUpdated = true } 1001 1002 // When the host state updates 1003 hostStateCallback.value!!.onHostStateChanged(LOCATION_QS, mediaHostState) 1004 1005 // Then the carousel visibility is updated 1006 assertTrue(stateUpdated) 1007 } 1008 1009 @Test testAnimationScaleChanged_mediaControlPanelsNotifiednull1010 fun testAnimationScaleChanged_mediaControlPanelsNotified() { 1011 MediaPlayerData.addMediaPlayer("key", DATA, panel, clock, isSsReactivated = false) 1012 1013 globalSettings.putFloat(Settings.Global.ANIMATOR_DURATION_SCALE, 0f) 1014 settingsObserverCaptor.value!!.onChange(false) 1015 verify(panel).updateAnimatorDurationScale() 1016 } 1017 1018 @Test swipeToDismiss_pausedAndResumeOff_userInitiatednull1019 fun swipeToDismiss_pausedAndResumeOff_userInitiated() { 1020 // When resumption is disabled, paused media should be dismissed after being swiped away 1021 Settings.Secure.putInt(context.contentResolver, Settings.Secure.MEDIA_CONTROLS_RESUME, 0) 1022 1023 val pausedMedia = DATA.copy(isPlaying = false) 1024 listener.value.onMediaDataLoaded(PAUSED_LOCAL, PAUSED_LOCAL, pausedMedia) 1025 mediaCarouselController.onSwipeToDismiss() 1026 1027 // When it can be removed immediately on update 1028 whenever(visualStabilityProvider.isReorderingAllowed).thenReturn(true) 1029 val inactiveMedia = pausedMedia.copy(active = false) 1030 listener.value.onMediaDataLoaded(PAUSED_LOCAL, PAUSED_LOCAL, inactiveMedia) 1031 1032 // This is processed as a user-initiated dismissal 1033 verify(debugLogger).logMediaRemoved(eq(PAUSED_LOCAL), eq(true)) 1034 verify(mediaDataManager).dismissMediaData(eq(PAUSED_LOCAL), anyLong(), eq(true)) 1035 } 1036 1037 @Test swipeToDismiss_pausedAndResumeOff_delayed_userInitiatednull1038 fun swipeToDismiss_pausedAndResumeOff_delayed_userInitiated() { 1039 // When resumption is disabled, paused media should be dismissed after being swiped away 1040 Settings.Secure.putInt(context.contentResolver, Settings.Secure.MEDIA_CONTROLS_RESUME, 0) 1041 mediaCarouselController.updateHostVisibility = {} 1042 1043 val pausedMedia = DATA.copy(isPlaying = false) 1044 listener.value.onMediaDataLoaded(PAUSED_LOCAL, PAUSED_LOCAL, pausedMedia) 1045 mediaCarouselController.onSwipeToDismiss() 1046 1047 // When it can't be removed immediately on update 1048 whenever(visualStabilityProvider.isReorderingAllowed).thenReturn(false) 1049 val inactiveMedia = pausedMedia.copy(active = false) 1050 listener.value.onMediaDataLoaded(PAUSED_LOCAL, PAUSED_LOCAL, inactiveMedia) 1051 visualStabilityCallback.value.onReorderingAllowed() 1052 1053 // This is processed as a user-initiated dismissal 1054 verify(mediaDataManager).dismissMediaData(eq(PAUSED_LOCAL), anyLong(), eq(true)) 1055 } 1056 1057 /** 1058 * Helper method when a configuration change occurs. 1059 * 1060 * @param function called when a certain configuration change occurs. 1061 */ testConfigurationChangenull1062 private fun testConfigurationChange(function: () -> Unit) { 1063 mediaCarouselController.pageIndicator = pageIndicator 1064 listener.value.onMediaDataLoaded( 1065 PLAYING_LOCAL, 1066 null, 1067 DATA.copy( 1068 active = true, 1069 isPlaying = true, 1070 playbackLocation = MediaData.PLAYBACK_LOCAL, 1071 resumption = false 1072 ) 1073 ) 1074 listener.value.onMediaDataLoaded( 1075 PAUSED_LOCAL, 1076 null, 1077 DATA.copy( 1078 active = true, 1079 isPlaying = false, 1080 playbackLocation = MediaData.PLAYBACK_LOCAL, 1081 resumption = false 1082 ) 1083 ) 1084 1085 val playersSize = MediaPlayerData.players().size 1086 reset(pageIndicator) 1087 function() 1088 1089 assertEquals(playersSize, MediaPlayerData.players().size) 1090 assertEquals( 1091 MediaPlayerData.getMediaPlayerIndex(PLAYING_LOCAL), 1092 mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex 1093 ) 1094 } 1095 } 1096