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