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.platform.test.flag.junit.FlagsParameterization
25 import android.provider.Settings
26 import android.testing.TestableLooper
27 import android.util.MathUtils.abs
28 import android.view.View
29 import androidx.test.filters.SmallTest
30 import com.android.compose.animation.scene.ObservableTransitionState
31 import com.android.compose.animation.scene.SceneKey
32 import com.android.internal.logging.InstanceId
33 import com.android.keyguard.KeyguardUpdateMonitor
34 import com.android.keyguard.KeyguardUpdateMonitorCallback
35 import com.android.systemui.Flags.mediaControlsUmoInflationInBackground
36 import com.android.systemui.SysuiTestCase
37 import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
38 import com.android.systemui.dump.DumpManager
39 import com.android.systemui.flags.DisableSceneContainer
40 import com.android.systemui.flags.EnableSceneContainer
41 import com.android.systemui.flags.Flags
42 import com.android.systemui.flags.fakeFeatureFlagsClassic
43 import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
44 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
45 import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
46 import com.android.systemui.keyguard.shared.model.KeyguardState
47 import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
48 import com.android.systemui.kosmos.applicationCoroutineScope
49 import com.android.systemui.kosmos.testDispatcher
50 import com.android.systemui.kosmos.testScope
51 import com.android.systemui.kosmos.useUnconfinedTestDispatcher
52 import com.android.systemui.media.controls.MediaTestUtils
53 import com.android.systemui.media.controls.domain.pipeline.EMPTY_SMARTSPACE_MEDIA_DATA
54 import com.android.systemui.media.controls.domain.pipeline.MediaDataManager
55 import com.android.systemui.media.controls.shared.model.MediaData
56 import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager.Companion.LOCATION_QS
57 import com.android.systemui.media.controls.ui.view.MediaHostState
58 import com.android.systemui.media.controls.ui.view.MediaScrollView
59 import com.android.systemui.media.controls.ui.viewmodel.mediaCarouselViewModel
60 import com.android.systemui.media.controls.util.MediaFlags
61 import com.android.systemui.media.controls.util.MediaUiEventLogger
62 import com.android.systemui.plugins.ActivityStarter
63 import com.android.systemui.plugins.FalsingManager
64 import com.android.systemui.qs.PageIndicator
65 import com.android.systemui.res.R
66 import com.android.systemui.scene.data.repository.Idle
67 import com.android.systemui.scene.data.repository.setSceneTransition
68 import com.android.systemui.scene.domain.interactor.sceneInteractor
69 import com.android.systemui.scene.shared.model.Scenes
70 import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener
71 import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider
72 import com.android.systemui.statusbar.policy.ConfigurationController
73 import com.android.systemui.testKosmos
74 import com.android.systemui.util.concurrency.FakeExecutor
75 import com.android.systemui.util.settings.GlobalSettings
76 import com.android.systemui.util.settings.fakeSettings
77 import com.android.systemui.util.time.FakeSystemClock
78 import com.google.common.truth.Truth.assertThat
79 import java.util.Locale
80 import javax.inject.Provider
81 import junit.framework.Assert.assertEquals
82 import junit.framework.Assert.assertFalse
83 import junit.framework.Assert.assertTrue
84 import kotlinx.coroutines.ExperimentalCoroutinesApi
85 import kotlinx.coroutines.flow.MutableStateFlow
86 import kotlinx.coroutines.test.TestScope
87 import kotlinx.coroutines.test.runCurrent
88 import kotlinx.coroutines.test.runTest
89 import org.junit.After
90 import org.junit.Before
91 import org.junit.Test
92 import org.junit.runner.RunWith
93 import org.mockito.ArgumentCaptor
94 import org.mockito.Captor
95 import org.mockito.Mock
96 import org.mockito.Mockito.anyLong
97 import org.mockito.Mockito.floatThat
98 import org.mockito.Mockito.mock
99 import org.mockito.Mockito.never
100 import org.mockito.Mockito.reset
101 import org.mockito.Mockito.times
102 import org.mockito.Mockito.verify
103 import org.mockito.Mockito.`when` as whenever
104 import org.mockito.MockitoAnnotations
105 import org.mockito.kotlin.any
106 import org.mockito.kotlin.capture
107 import org.mockito.kotlin.eq
108 import platform.test.runner.parameterized.ParameterizedAndroidJunit4
109 import platform.test.runner.parameterized.Parameters
110 
111 private val DATA = MediaTestUtils.emptyMediaData
112 
113 private val SMARTSPACE_KEY = "smartspace"
114 private const val PAUSED_LOCAL = "paused local"
115 private const val PLAYING_LOCAL = "playing local"
116 
117 @ExperimentalCoroutinesApi
118 @SmallTest
119 @TestableLooper.RunWithLooper(setAsMainLooper = true)
120 @RunWith(ParameterizedAndroidJunit4::class)
121 class MediaCarouselControllerTest(flags: FlagsParameterization) : SysuiTestCase() {
122     private val kosmos = testKosmos().useUnconfinedTestDispatcher()
123     private val testDispatcher = kosmos.testDispatcher
124     private val secureSettings = kosmos.fakeSettings
125 
126     @Mock lateinit var mediaControlPanelFactory: Provider<MediaControlPanel>
127     @Mock lateinit var mediaViewControllerFactory: Provider<MediaViewController>
128     @Mock lateinit var panel: MediaControlPanel
129     @Mock lateinit var visualStabilityProvider: VisualStabilityProvider
130     @Mock lateinit var mediaHostStatesManager: MediaHostStatesManager
131     @Mock lateinit var mediaHostState: MediaHostState
132     @Mock lateinit var activityStarter: ActivityStarter
133     @Mock lateinit var mediaDataManager: MediaDataManager
134     @Mock lateinit var configurationController: ConfigurationController
135     @Mock lateinit var falsingManager: FalsingManager
136     @Mock lateinit var dumpManager: DumpManager
137     @Mock lateinit var logger: MediaUiEventLogger
138     @Mock lateinit var debugLogger: MediaCarouselControllerLogger
139     @Mock lateinit var mediaViewController: MediaViewController
140     @Mock lateinit var mediaCarousel: MediaScrollView
141     @Mock lateinit var pageIndicator: PageIndicator
142     @Mock lateinit var mediaFlags: MediaFlags
143     @Mock lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
144     @Mock lateinit var globalSettings: GlobalSettings
145     private val transitionRepository = kosmos.fakeKeyguardTransitionRepository
146     @Captor lateinit var listener: ArgumentCaptor<MediaDataManager.Listener>
147     @Captor
148     lateinit var configListener: ArgumentCaptor<ConfigurationController.ConfigurationListener>
149     @Captor lateinit var visualStabilityCallback: ArgumentCaptor<OnReorderingAllowedListener>
150     @Captor lateinit var keyguardCallback: ArgumentCaptor<KeyguardUpdateMonitorCallback>
151     @Captor lateinit var hostStateCallback: ArgumentCaptor<MediaHostStatesManager.Callback>
152     @Captor lateinit var settingsObserverCaptor: ArgumentCaptor<ContentObserver>
153 
154     private val clock = FakeSystemClock()
155     private lateinit var bgExecutor: FakeExecutor
156     private lateinit var uiExecutor: FakeExecutor
157     private lateinit var mediaCarouselController: MediaCarouselController
158 
159     private var originalResumeSetting =
160         Settings.Secure.getInt(context.contentResolver, Settings.Secure.MEDIA_CONTROLS_RESUME, 1)
161 
162     companion object {
163         @JvmStatic
164         @Parameters(name = "{0}")
getParamsnull165         fun getParams(): List<FlagsParameterization> {
166             return FlagsParameterization.progressionOf(
167                 com.android.systemui.Flags.FLAG_MEDIA_CONTROLS_UMO_INFLATION_IN_BACKGROUND
168             )
169         }
170     }
171 
172     init {
173         mSetFlagsRule.setFlagsParameterization(flags)
174     }
175 
176     @Before
setupnull177     fun setup() {
178         MockitoAnnotations.initMocks(this)
179         context.resources.configuration.setLocales(LocaleList(Locale.US, Locale.UK))
180         bgExecutor = FakeExecutor(clock)
181         uiExecutor = FakeExecutor(clock)
182 
183         mediaCarouselController =
184             MediaCarouselController(
185                 applicationScope = kosmos.applicationCoroutineScope,
186                 context = context,
187                 mediaControlPanelFactory = mediaControlPanelFactory,
188                 visualStabilityProvider = visualStabilityProvider,
189                 mediaHostStatesManager = mediaHostStatesManager,
190                 activityStarter = activityStarter,
191                 systemClock = clock,
192                 mainDispatcher = kosmos.testDispatcher,
193                 uiExecutor = uiExecutor,
194                 bgExecutor = bgExecutor,
195                 backgroundDispatcher = testDispatcher,
196                 mediaManager = mediaDataManager,
197                 configurationController = configurationController,
198                 falsingManager = falsingManager,
199                 dumpManager = dumpManager,
200                 logger = logger,
201                 debugLogger = debugLogger,
202                 mediaFlags = mediaFlags,
203                 keyguardUpdateMonitor = keyguardUpdateMonitor,
204                 keyguardTransitionInteractor = kosmos.keyguardTransitionInteractor,
205                 globalSettings = globalSettings,
206                 secureSettings = secureSettings,
207                 mediaCarouselViewModel = kosmos.mediaCarouselViewModel,
208                 mediaViewControllerFactory = mediaViewControllerFactory,
209                 deviceEntryInteractor = kosmos.deviceEntryInteractor,
210             )
211         verify(configurationController).addCallback(capture(configListener))
212         verify(visualStabilityProvider)
213             .addPersistentReorderingAllowedListener(capture(visualStabilityCallback))
214         verify(keyguardUpdateMonitor).registerCallback(capture(keyguardCallback))
215         verify(mediaHostStatesManager).addCallback(capture(hostStateCallback))
216         whenever(mediaControlPanelFactory.get()).thenReturn(panel)
217         whenever(panel.mediaViewController).thenReturn(mediaViewController)
218         whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(false)
219         MediaPlayerData.clear()
220         FakeExecutor.exhaustExecutors(bgExecutor)
221         FakeExecutor.exhaustExecutors(uiExecutor)
222         verify(globalSettings)
223             .registerContentObserverSync(
224                 eq(Settings.Global.getUriFor(Settings.Global.ANIMATOR_DURATION_SCALE)),
225                 capture(settingsObserverCaptor),
226             )
227     }
228 
229     @After
tearDownnull230     fun tearDown() {
231         Settings.Secure.putInt(
232             context.contentResolver,
233             Settings.Secure.MEDIA_CONTROLS_RESUME,
234             originalResumeSetting,
235         )
236     }
237 
238     @Test
testPlayerOrderingnull239     fun testPlayerOrdering() {
240         // Test values: key, data, last active time
241         val playingLocal =
242             Triple(
243                 PLAYING_LOCAL,
244                 DATA.copy(
245                     active = true,
246                     isPlaying = true,
247                     playbackLocation = MediaData.PLAYBACK_LOCAL,
248                     resumption = false,
249                 ),
250                 4500L,
251             )
252 
253         val playingCast =
254             Triple(
255                 "playing cast",
256                 DATA.copy(
257                     active = true,
258                     isPlaying = true,
259                     playbackLocation = MediaData.PLAYBACK_CAST_LOCAL,
260                     resumption = false,
261                 ),
262                 5000L,
263             )
264 
265         val pausedLocal =
266             Triple(
267                 PAUSED_LOCAL,
268                 DATA.copy(
269                     active = true,
270                     isPlaying = false,
271                     playbackLocation = MediaData.PLAYBACK_LOCAL,
272                     resumption = false,
273                 ),
274                 1000L,
275             )
276 
277         val pausedCast =
278             Triple(
279                 "paused cast",
280                 DATA.copy(
281                     active = true,
282                     isPlaying = false,
283                     playbackLocation = MediaData.PLAYBACK_CAST_LOCAL,
284                     resumption = false,
285                 ),
286                 2000L,
287             )
288 
289         val playingRcn =
290             Triple(
291                 "playing RCN",
292                 DATA.copy(
293                     active = true,
294                     isPlaying = true,
295                     playbackLocation = MediaData.PLAYBACK_CAST_REMOTE,
296                     resumption = false,
297                 ),
298                 5000L,
299             )
300 
301         val pausedRcn =
302             Triple(
303                 "paused RCN",
304                 DATA.copy(
305                     active = true,
306                     isPlaying = false,
307                     playbackLocation = MediaData.PLAYBACK_CAST_REMOTE,
308                     resumption = false,
309                 ),
310                 5000L,
311             )
312 
313         val active =
314             Triple(
315                 "active",
316                 DATA.copy(
317                     active = true,
318                     isPlaying = false,
319                     playbackLocation = MediaData.PLAYBACK_LOCAL,
320                     resumption = true,
321                 ),
322                 250L,
323             )
324 
325         val resume1 =
326             Triple(
327                 "resume 1",
328                 DATA.copy(
329                     active = false,
330                     isPlaying = false,
331                     playbackLocation = MediaData.PLAYBACK_LOCAL,
332                     resumption = true,
333                 ),
334                 500L,
335             )
336 
337         val resume2 =
338             Triple(
339                 "resume 2",
340                 DATA.copy(
341                     active = false,
342                     isPlaying = false,
343                     playbackLocation = MediaData.PLAYBACK_LOCAL,
344                     resumption = true,
345                 ),
346                 1000L,
347             )
348 
349         val activeMoreRecent =
350             Triple(
351                 "active more recent",
352                 DATA.copy(
353                     active = false,
354                     isPlaying = false,
355                     playbackLocation = MediaData.PLAYBACK_LOCAL,
356                     resumption = true,
357                     lastActive = 2L,
358                 ),
359                 1000L,
360             )
361 
362         val activeLessRecent =
363             Triple(
364                 "active less recent",
365                 DATA.copy(
366                     active = false,
367                     isPlaying = false,
368                     playbackLocation = MediaData.PLAYBACK_LOCAL,
369                     resumption = true,
370                     lastActive = 1L,
371                 ),
372                 1000L,
373             )
374         // Expected ordering for media players:
375         // Actively playing local sessions
376         // Actively playing cast sessions
377         // Paused local and cast sessions, by last active
378         // RCNs
379         // Resume controls, by last active
380 
381         val expected =
382             listOf(
383                 playingLocal,
384                 playingCast,
385                 pausedCast,
386                 pausedLocal,
387                 playingRcn,
388                 pausedRcn,
389                 active,
390                 resume2,
391                 resume1,
392             )
393 
394         expected.forEach {
395             clock.setCurrentTimeMillis(it.third)
396             MediaPlayerData.addMediaPlayer(
397                 it.first,
398                 it.second.copy(notificationKey = it.first),
399                 panel,
400                 clock,
401                 isSsReactivated = false,
402             )
403         }
404 
405         for ((index, key) in MediaPlayerData.playerKeys().withIndex()) {
406             assertEquals(expected.get(index).first, key.data.notificationKey)
407         }
408 
409         for ((index, key) in MediaPlayerData.visiblePlayerKeys().withIndex()) {
410             assertEquals(expected.get(index).first, key.data.notificationKey)
411         }
412     }
413 
414     @Test
testOrderWithSmartspace_prioritizednull415     fun testOrderWithSmartspace_prioritized() {
416         testPlayerOrdering()
417 
418         // If smartspace is prioritized
419         MediaPlayerData.addMediaRecommendation(
420             SMARTSPACE_KEY,
421             EMPTY_SMARTSPACE_MEDIA_DATA.copy(isActive = true),
422             panel,
423             true,
424             clock,
425         )
426 
427         // Then it should be shown immediately after any actively playing controls
428         assertTrue(MediaPlayerData.playerKeys().elementAt(2).isSsMediaRec)
429     }
430 
431     @DisableSceneContainer
432     @Test
testOrderWithSmartspace_prioritized_updatingVisibleMediaPlayersnull433     fun testOrderWithSmartspace_prioritized_updatingVisibleMediaPlayers() {
434         verify(mediaDataManager).addListener(capture(listener))
435 
436         testPlayerOrdering()
437 
438         // If smartspace is prioritized
439         listener.value.onSmartspaceMediaDataLoaded(
440             SMARTSPACE_KEY,
441             EMPTY_SMARTSPACE_MEDIA_DATA.copy(isActive = true),
442             true,
443         )
444 
445         // Then it should be shown immediately after any actively playing controls
446         assertTrue(MediaPlayerData.playerKeys().elementAt(2).isSsMediaRec)
447         assertTrue(MediaPlayerData.visiblePlayerKeys().elementAt(2).isSsMediaRec)
448     }
449 
450     @Test
testOrderWithSmartspace_notPrioritizednull451     fun testOrderWithSmartspace_notPrioritized() {
452         testPlayerOrdering()
453 
454         // If smartspace is not prioritized
455         MediaPlayerData.addMediaRecommendation(
456             SMARTSPACE_KEY,
457             EMPTY_SMARTSPACE_MEDIA_DATA.copy(isActive = true),
458             panel,
459             false,
460             clock,
461         )
462 
463         // Then it should be shown at the end of the carousel's active entries
464         val idx = MediaPlayerData.playerKeys().count { it.data.active } - 1
465         assertTrue(MediaPlayerData.playerKeys().elementAt(idx).isSsMediaRec)
466     }
467 
468     @DisableSceneContainer
469     @Test
testPlayingExistingMediaPlayerFromCarousel_visibleMediaPlayersNotUpdatednull470     fun testPlayingExistingMediaPlayerFromCarousel_visibleMediaPlayersNotUpdated() {
471         verify(mediaDataManager).addListener(capture(listener))
472 
473         testPlayerOrdering()
474         // playing paused player
475         listener.value.onMediaDataLoaded(
476             PAUSED_LOCAL,
477             PAUSED_LOCAL,
478             DATA.copy(
479                 active = true,
480                 isPlaying = true,
481                 playbackLocation = MediaData.PLAYBACK_LOCAL,
482                 resumption = false,
483             ),
484         )
485         listener.value.onMediaDataLoaded(
486             PLAYING_LOCAL,
487             PLAYING_LOCAL,
488             DATA.copy(
489                 active = true,
490                 isPlaying = false,
491                 playbackLocation = MediaData.PLAYBACK_LOCAL,
492                 resumption = true,
493             ),
494         )
495         runAllReady()
496 
497         assertEquals(
498             MediaPlayerData.getMediaPlayerIndex(PAUSED_LOCAL),
499             mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex,
500         )
501         // paused player order should stays the same in visibleMediaPLayer map.
502         // paused player order should be first in mediaPlayer map.
503         assertEquals(
504             MediaPlayerData.visiblePlayerKeys().elementAt(3),
505             MediaPlayerData.playerKeys().elementAt(0),
506         )
507     }
508 
509     @Test
testSwipeDismiss_loggednull510     fun testSwipeDismiss_logged() {
511         mediaCarouselController.mediaCarouselScrollHandler.dismissCallback.invoke()
512 
513         verify(logger).logSwipeDismiss()
514     }
515 
516     @Test
testSettingsButton_loggednull517     fun testSettingsButton_logged() {
518         mediaCarouselController.settingsButton.callOnClick()
519 
520         verify(logger).logCarouselSettings()
521     }
522 
523     @Test
testLocationChangeQs_loggednull524     fun testLocationChangeQs_logged() {
525         mediaCarouselController.onDesiredLocationChanged(
526             LOCATION_QS,
527             mediaHostState,
528             animate = false,
529         )
530         bgExecutor.runAllReady()
531         verify(logger).logCarouselPosition(LOCATION_QS)
532     }
533 
534     @Test
testLocationChangeQqs_loggednull535     fun testLocationChangeQqs_logged() {
536         mediaCarouselController.onDesiredLocationChanged(
537             MediaHierarchyManager.LOCATION_QQS,
538             mediaHostState,
539             animate = false,
540         )
541         bgExecutor.runAllReady()
542         verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_QQS)
543     }
544 
545     @Test
testLocationChangeLockscreen_loggednull546     fun testLocationChangeLockscreen_logged() {
547         mediaCarouselController.onDesiredLocationChanged(
548             MediaHierarchyManager.LOCATION_LOCKSCREEN,
549             mediaHostState,
550             animate = false,
551         )
552         bgExecutor.runAllReady()
553         verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_LOCKSCREEN)
554     }
555 
556     @Test
testLocationChangeDream_loggednull557     fun testLocationChangeDream_logged() {
558         mediaCarouselController.onDesiredLocationChanged(
559             MediaHierarchyManager.LOCATION_DREAM_OVERLAY,
560             mediaHostState,
561             animate = false,
562         )
563         bgExecutor.runAllReady()
564         verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_DREAM_OVERLAY)
565     }
566 
567     @Test
testRecommendationRemoved_loggednull568     fun testRecommendationRemoved_logged() {
569         val packageName = "smartspace package"
570         val instanceId = InstanceId.fakeInstanceId(123)
571 
572         val smartspaceData =
573             EMPTY_SMARTSPACE_MEDIA_DATA.copy(packageName = packageName, instanceId = instanceId)
574         MediaPlayerData.addMediaRecommendation(SMARTSPACE_KEY, smartspaceData, panel, true, clock)
575         mediaCarouselController.removePlayer(SMARTSPACE_KEY)
576 
577         verify(logger).logRecommendationRemoved(eq(packageName), eq(instanceId!!))
578     }
579 
580     @DisableSceneContainer
581     @Test
testMediaLoaded_ScrollToActivePlayernull582     fun testMediaLoaded_ScrollToActivePlayer() {
583         verify(mediaDataManager).addListener(capture(listener))
584 
585         listener.value.onMediaDataLoaded(
586             PLAYING_LOCAL,
587             null,
588             DATA.copy(
589                 active = true,
590                 isPlaying = true,
591                 playbackLocation = MediaData.PLAYBACK_LOCAL,
592                 resumption = false,
593             ),
594         )
595         listener.value.onMediaDataLoaded(
596             PAUSED_LOCAL,
597             null,
598             DATA.copy(
599                 active = true,
600                 isPlaying = false,
601                 playbackLocation = MediaData.PLAYBACK_LOCAL,
602                 resumption = false,
603             ),
604         )
605         runAllReady()
606         // adding a media recommendation card.
607         listener.value.onSmartspaceMediaDataLoaded(
608             SMARTSPACE_KEY,
609             EMPTY_SMARTSPACE_MEDIA_DATA,
610             false,
611         )
612         mediaCarouselController.shouldScrollToKey = true
613         // switching between media players.
614         listener.value.onMediaDataLoaded(
615             PLAYING_LOCAL,
616             PLAYING_LOCAL,
617             DATA.copy(
618                 active = true,
619                 isPlaying = false,
620                 playbackLocation = MediaData.PLAYBACK_LOCAL,
621                 resumption = true,
622             ),
623         )
624         listener.value.onMediaDataLoaded(
625             PAUSED_LOCAL,
626             PAUSED_LOCAL,
627             DATA.copy(
628                 active = true,
629                 isPlaying = true,
630                 playbackLocation = MediaData.PLAYBACK_LOCAL,
631                 resumption = false,
632             ),
633         )
634         runAllReady()
635 
636         assertEquals(
637             MediaPlayerData.getMediaPlayerIndex(PAUSED_LOCAL),
638             mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex,
639         )
640     }
641 
642     @DisableSceneContainer
643     @Test
testMediaLoadedFromRecommendationCard_ScrollToActivePlayernull644     fun testMediaLoadedFromRecommendationCard_ScrollToActivePlayer() {
645         verify(mediaDataManager).addListener(capture(listener))
646 
647         listener.value.onSmartspaceMediaDataLoaded(
648             SMARTSPACE_KEY,
649             EMPTY_SMARTSPACE_MEDIA_DATA.copy(packageName = "PACKAGE_NAME", isActive = true),
650             false,
651         )
652         listener.value.onMediaDataLoaded(
653             PLAYING_LOCAL,
654             null,
655             DATA.copy(
656                 active = true,
657                 isPlaying = true,
658                 playbackLocation = MediaData.PLAYBACK_LOCAL,
659                 resumption = false,
660             ),
661         )
662         runAllReady()
663 
664         var playerIndex = MediaPlayerData.getMediaPlayerIndex(PLAYING_LOCAL)
665         assertEquals(
666             playerIndex,
667             mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex,
668         )
669         assertEquals(playerIndex, 0)
670 
671         // Replaying the same media player one more time.
672         // And check that the card stays in its position.
673         mediaCarouselController.shouldScrollToKey = true
674         listener.value.onMediaDataLoaded(
675             PLAYING_LOCAL,
676             null,
677             DATA.copy(
678                 active = true,
679                 isPlaying = true,
680                 playbackLocation = MediaData.PLAYBACK_LOCAL,
681                 resumption = false,
682                 packageName = "PACKAGE_NAME",
683             ),
684         )
685         runAllReady()
686         playerIndex = MediaPlayerData.getMediaPlayerIndex(PLAYING_LOCAL)
687         assertEquals(playerIndex, 0)
688     }
689 
690     @DisableSceneContainer
691     @Test
testRecommendationRemovedWhileNotVisible_updateHostVisibilitynull692     fun testRecommendationRemovedWhileNotVisible_updateHostVisibility() {
693         verify(mediaDataManager).addListener(capture(listener))
694 
695         var result = false
696         mediaCarouselController.updateHostVisibility = { result = true }
697 
698         whenever(visualStabilityProvider.isReorderingAllowed).thenReturn(true)
699         listener.value.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY, false)
700 
701         assertEquals(true, result)
702     }
703 
704     @DisableSceneContainer
705     @Test
testRecommendationRemovedWhileVisible_thenReorders_updateHostVisibilitynull706     fun testRecommendationRemovedWhileVisible_thenReorders_updateHostVisibility() {
707         verify(mediaDataManager).addListener(capture(listener))
708 
709         var result = false
710         mediaCarouselController.updateHostVisibility = { result = true }
711 
712         whenever(visualStabilityProvider.isReorderingAllowed).thenReturn(false)
713         listener.value.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY, false)
714         assertEquals(false, result)
715 
716         visualStabilityCallback.value.onReorderingAllowed()
717         assertEquals(true, result)
718     }
719 
720     @Test
testGetCurrentVisibleMediaContentIntentnull721     fun testGetCurrentVisibleMediaContentIntent() {
722         val clickIntent1 = mock(PendingIntent::class.java)
723         val player1 = Triple("player1", DATA.copy(clickIntent = clickIntent1), 1000L)
724         clock.setCurrentTimeMillis(player1.third)
725         MediaPlayerData.addMediaPlayer(
726             player1.first,
727             player1.second.copy(notificationKey = player1.first),
728             panel,
729             clock,
730             isSsReactivated = false,
731         )
732 
733         assertEquals(mediaCarouselController.getCurrentVisibleMediaContentIntent(), clickIntent1)
734 
735         val clickIntent2 = mock(PendingIntent::class.java)
736         val player2 = Triple("player2", DATA.copy(clickIntent = clickIntent2), 2000L)
737         clock.setCurrentTimeMillis(player2.third)
738         MediaPlayerData.addMediaPlayer(
739             player2.first,
740             player2.second.copy(notificationKey = player2.first),
741             panel,
742             clock,
743             isSsReactivated = false,
744         )
745 
746         // mediaCarouselScrollHandler.visibleMediaIndex is unchanged (= 0), and the new player is
747         // added to the front because it was active more recently.
748         assertEquals(mediaCarouselController.getCurrentVisibleMediaContentIntent(), clickIntent2)
749 
750         val clickIntent3 = mock(PendingIntent::class.java)
751         val player3 = Triple("player3", DATA.copy(clickIntent = clickIntent3), 500L)
752         clock.setCurrentTimeMillis(player3.third)
753         MediaPlayerData.addMediaPlayer(
754             player3.first,
755             player3.second.copy(notificationKey = player3.first),
756             panel,
757             clock,
758             isSsReactivated = false,
759         )
760 
761         // mediaCarouselScrollHandler.visibleMediaIndex is unchanged (= 0), and the new player is
762         // added to the end because it was active less recently.
763         assertEquals(mediaCarouselController.getCurrentVisibleMediaContentIntent(), clickIntent2)
764     }
765 
766     @Test
testSetCurrentState_UpdatePageIndicatorAlphaWhenSquishnull767     fun testSetCurrentState_UpdatePageIndicatorAlphaWhenSquish() {
768         val delta = 0.0001F
769         mediaCarouselController.mediaCarousel = mediaCarousel
770         mediaCarouselController.pageIndicator = pageIndicator
771         whenever(mediaCarousel.measuredHeight).thenReturn(100)
772         whenever(pageIndicator.translationY).thenReturn(80F)
773         whenever(pageIndicator.height).thenReturn(10)
774         whenever(mediaHostStatesManager.mediaHostStates)
775             .thenReturn(mutableMapOf(LOCATION_QS to mediaHostState))
776         whenever(mediaHostState.visible).thenReturn(true)
777         mediaCarouselController.currentEndLocation = LOCATION_QS
778         whenever(mediaHostState.squishFraction).thenReturn(0.938F)
779         mediaCarouselController.updatePageIndicatorAlpha()
780         verify(pageIndicator).alpha = floatThat { abs(it - 0.5F) < delta }
781 
782         whenever(mediaHostState.squishFraction).thenReturn(1.0F)
783         mediaCarouselController.updatePageIndicatorAlpha()
784         verify(pageIndicator).alpha = floatThat { abs(it - 1.0F) < delta }
785     }
786 
787     @Test
testOnConfigChanged_playersAreAddedBacknull788     fun testOnConfigChanged_playersAreAddedBack() {
789         testConfigurationChange { configListener.value.onConfigChanged(Configuration()) }
790     }
791 
792     @Test
testOnUiModeChanged_playersAreAddedBacknull793     fun testOnUiModeChanged_playersAreAddedBack() {
794         testConfigurationChange(configListener.value::onUiModeChanged)
795 
796         verify(pageIndicator).tintList =
797             ColorStateList.valueOf(context.getColor(R.color.media_paging_indicator))
798         verify(pageIndicator, times(2)).setNumPages(any())
799     }
800 
801     @Test
testOnDensityOrFontScaleChanged_playersAreAddedBacknull802     fun testOnDensityOrFontScaleChanged_playersAreAddedBack() {
803         testConfigurationChange(configListener.value::onDensityOrFontScaleChanged)
804 
805         verify(pageIndicator).tintList =
806             ColorStateList.valueOf(context.getColor(R.color.media_paging_indicator))
807         // when recreateMedia is set to true, page indicator is updated on removal and addition.
808         verify(pageIndicator, times(4)).setNumPages(any())
809     }
810 
811     @Test
testOnThemeChanged_playersAreAddedBacknull812     fun testOnThemeChanged_playersAreAddedBack() {
813         testConfigurationChange(configListener.value::onThemeChanged)
814 
815         verify(pageIndicator).tintList =
816             ColorStateList.valueOf(context.getColor(R.color.media_paging_indicator))
817         verify(pageIndicator, times(2)).setNumPages(any())
818     }
819 
820     @Test
testOnLocaleListChanged_playersAreAddedBacknull821     fun testOnLocaleListChanged_playersAreAddedBack() {
822         context.resources.configuration.setLocales(LocaleList(Locale.US, Locale.UK, Locale.CANADA))
823         testConfigurationChange(configListener.value::onLocaleListChanged)
824 
825         verify(pageIndicator, never()).tintList =
826             ColorStateList.valueOf(context.getColor(R.color.media_paging_indicator))
827 
828         context.resources.configuration.setLocales(LocaleList(Locale.UK, Locale.US, Locale.CANADA))
829         testConfigurationChange(configListener.value::onLocaleListChanged)
830 
831         verify(pageIndicator).tintList =
832             ColorStateList.valueOf(context.getColor(R.color.media_paging_indicator))
833         // When recreateMedia is set to true, page indicator is updated on removal and addition.
834         verify(pageIndicator, times(4)).setNumPages(any())
835     }
836 
837     @DisableSceneContainer
838     @Test
testRecommendation_persistentEnabled_newSmartspaceLoaded_updatesSortnull839     fun testRecommendation_persistentEnabled_newSmartspaceLoaded_updatesSort() {
840         verify(mediaDataManager).addListener(capture(listener))
841 
842         testRecommendation_persistentEnabled_inactiveSmartspaceDataLoaded_isAdded()
843 
844         // When an update to existing smartspace data is loaded
845         listener.value.onSmartspaceMediaDataLoaded(
846             SMARTSPACE_KEY,
847             EMPTY_SMARTSPACE_MEDIA_DATA.copy(isActive = true),
848             true,
849         )
850 
851         // Then the carousel is updated
852         assertTrue(MediaPlayerData.playerKeys().elementAt(0).data.active)
853         assertTrue(MediaPlayerData.visiblePlayerKeys().elementAt(0).data.active)
854     }
855 
856     @DisableSceneContainer
857     @Test
testRecommendation_persistentEnabled_inactiveSmartspaceDataLoaded_isAddednull858     fun testRecommendation_persistentEnabled_inactiveSmartspaceDataLoaded_isAdded() {
859         verify(mediaDataManager).addListener(capture(listener))
860 
861         whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
862 
863         // When inactive smartspace data is loaded
864         listener.value.onSmartspaceMediaDataLoaded(
865             SMARTSPACE_KEY,
866             EMPTY_SMARTSPACE_MEDIA_DATA,
867             false,
868         )
869 
870         // Then it is added to the carousel with correct state
871         assertTrue(MediaPlayerData.playerKeys().elementAt(0).isSsMediaRec)
872         assertFalse(MediaPlayerData.playerKeys().elementAt(0).data.active)
873 
874         assertTrue(MediaPlayerData.visiblePlayerKeys().elementAt(0).isSsMediaRec)
875         assertFalse(MediaPlayerData.visiblePlayerKeys().elementAt(0).data.active)
876     }
877 
878     @Test
testOnLockDownMode_hideMediaCarouselnull879     fun testOnLockDownMode_hideMediaCarousel() {
880         whenever(keyguardUpdateMonitor.isUserInLockdown(context.userId)).thenReturn(true)
881         mediaCarouselController.mediaCarousel = mediaCarousel
882 
883         keyguardCallback.value.onStrongAuthStateChanged(context.userId)
884 
885         verify(mediaCarousel).visibility = View.GONE
886     }
887 
888     @Test
testLockDownModeOff_showMediaCarouselnull889     fun testLockDownModeOff_showMediaCarousel() {
890         whenever(keyguardUpdateMonitor.isUserInLockdown(context.userId)).thenReturn(false)
891         whenever(keyguardUpdateMonitor.isUserUnlocked(context.userId)).thenReturn(true)
892         mediaCarouselController.mediaCarousel = mediaCarousel
893 
894         keyguardCallback.value.onStrongAuthStateChanged(context.userId)
895 
896         verify(mediaCarousel).visibility = View.VISIBLE
897     }
898 
899     @DisableSceneContainer
900     @Test
testKeyguardGone_showMediaCarouselnull901     fun testKeyguardGone_showMediaCarousel() =
902         kosmos.testScope.runTest {
903             kosmos.fakeFeatureFlagsClassic.set(Flags.MEDIA_RETAIN_RECOMMENDATIONS, false)
904             var updatedVisibility = false
905             mediaCarouselController.updateHostVisibility = { updatedVisibility = true }
906             mediaCarouselController.mediaCarousel = mediaCarousel
907 
908             val job = mediaCarouselController.listenForAnyStateToGoneKeyguardTransition(this)
909             transitionRepository.sendTransitionSteps(
910                 from = KeyguardState.LOCKSCREEN,
911                 to = KeyguardState.GONE,
912                 this,
913             )
914 
915             verify(mediaCarousel).visibility = View.VISIBLE
916             assertEquals(true, updatedVisibility)
917             assertEquals(false, mediaCarouselController.isLockedAndHidden())
918 
919             job.cancel()
920         }
921 
922     @EnableSceneContainer
923     @Test
testKeyguardGone_showMediaCarousel_scene_containernull924     fun testKeyguardGone_showMediaCarousel_scene_container() =
925         kosmos.testScope.runTest {
926             kosmos.fakeFeatureFlagsClassic.set(Flags.MEDIA_RETAIN_RECOMMENDATIONS, false)
927             var updatedVisibility = false
928             mediaCarouselController.updateHostVisibility = { updatedVisibility = true }
929             mediaCarouselController.mediaCarousel = mediaCarousel
930 
931             val job = mediaCarouselController.listenForAnyStateToGoneKeyguardTransition(this)
932             kosmos.setSceneTransition(Idle(Scenes.Gone))
933 
934             verify(mediaCarousel).visibility = View.VISIBLE
935             assertEquals(true, updatedVisibility)
936 
937             job.cancel()
938         }
939 
940     @Test
keyguardShowing_notAllowedOnLockscreen_updateVisibilitynull941     fun keyguardShowing_notAllowedOnLockscreen_updateVisibility() {
942         kosmos.testScope.runTest {
943             kosmos.fakeFeatureFlagsClassic.set(Flags.MEDIA_RETAIN_RECOMMENDATIONS, false)
944             var updatedVisibility = false
945             mediaCarouselController.updateHostVisibility = { updatedVisibility = true }
946             mediaCarouselController.mediaCarousel = mediaCarousel
947 
948             val settingsJob =
949                 mediaCarouselController.listenForLockscreenSettingChanges(
950                     kosmos.applicationCoroutineScope
951                 )
952             secureSettings.putBool(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, false)
953 
954             val keyguardJob = mediaCarouselController.listenForAnyStateToLockscreenTransition(this)
955             transitionRepository.sendTransitionSteps(
956                 from = KeyguardState.GONE,
957                 to = KeyguardState.LOCKSCREEN,
958                 this,
959             )
960 
961             assertEquals(true, updatedVisibility)
962             assertEquals(true, mediaCarouselController.isLockedAndHidden())
963 
964             settingsJob.cancel()
965             keyguardJob.cancel()
966         }
967     }
968 
969     @Test
keyguardShowing_allowedOnLockscreen_updateVisibilitynull970     fun keyguardShowing_allowedOnLockscreen_updateVisibility() {
971         kosmos.testScope.runTest {
972             kosmos.fakeFeatureFlagsClassic.set(Flags.MEDIA_RETAIN_RECOMMENDATIONS, false)
973             var updatedVisibility = false
974             mediaCarouselController.updateHostVisibility = { updatedVisibility = true }
975             mediaCarouselController.mediaCarousel = mediaCarousel
976 
977             val settingsJob =
978                 mediaCarouselController.listenForLockscreenSettingChanges(
979                     kosmos.applicationCoroutineScope
980                 )
981             secureSettings.putBool(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, true)
982 
983             val keyguardJob = mediaCarouselController.listenForAnyStateToLockscreenTransition(this)
984             transitionRepository.sendTransitionSteps(
985                 from = KeyguardState.GONE,
986                 to = KeyguardState.LOCKSCREEN,
987                 this,
988             )
989 
990             assertEquals(true, updatedVisibility)
991             assertEquals(false, mediaCarouselController.isLockedAndHidden())
992 
993             settingsJob.cancel()
994             keyguardJob.cancel()
995         }
996     }
997 
998     @EnableSceneContainer
999     @Test
deviceEntered_mediaAllowed_notLockedAndHiddennull1000     fun deviceEntered_mediaAllowed_notLockedAndHidden() {
1001         kosmos.testScope.runTest {
1002             val settingsJob =
1003                 mediaCarouselController.listenForLockscreenSettingChanges(
1004                     kosmos.applicationCoroutineScope
1005                 )
1006             secureSettings.putBool(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, true)
1007             setDeviceEntered(true)
1008 
1009             assertEquals(false, mediaCarouselController.isLockedAndHidden())
1010 
1011             settingsJob.cancel()
1012         }
1013     }
1014 
1015     @EnableSceneContainer
1016     @Test
deviceEntered_mediaNotAllowed_notLockedAndHiddennull1017     fun deviceEntered_mediaNotAllowed_notLockedAndHidden() {
1018         kosmos.testScope.runTest {
1019             val settingsJob =
1020                 mediaCarouselController.listenForLockscreenSettingChanges(
1021                     kosmos.applicationCoroutineScope
1022                 )
1023             secureSettings.putBool(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, false)
1024             setDeviceEntered(true)
1025 
1026             assertEquals(false, mediaCarouselController.isLockedAndHidden())
1027 
1028             settingsJob.cancel()
1029         }
1030     }
1031 
1032     @EnableSceneContainer
1033     @Test
deviceNotEntered_mediaAllowed_notLockedAndHiddennull1034     fun deviceNotEntered_mediaAllowed_notLockedAndHidden() {
1035         kosmos.testScope.runTest {
1036             val settingsJob =
1037                 mediaCarouselController.listenForLockscreenSettingChanges(
1038                     kosmos.applicationCoroutineScope
1039                 )
1040             secureSettings.putBool(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, true)
1041             setDeviceEntered(false)
1042 
1043             assertEquals(false, mediaCarouselController.isLockedAndHidden())
1044 
1045             settingsJob.cancel()
1046         }
1047     }
1048 
1049     @EnableSceneContainer
1050     @Test
deviceNotEntered_mediaNotAllowed_lockedAndHiddennull1051     fun deviceNotEntered_mediaNotAllowed_lockedAndHidden() {
1052         kosmos.testScope.runTest {
1053             val settingsJob =
1054                 mediaCarouselController.listenForLockscreenSettingChanges(
1055                     kosmos.applicationCoroutineScope
1056                 )
1057             secureSettings.putBool(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, false)
1058             setDeviceEntered(false)
1059 
1060             assertEquals(true, mediaCarouselController.isLockedAndHidden())
1061 
1062             settingsJob.cancel()
1063         }
1064     }
1065 
1066     @Test
testInvisibleToUserAndExpanded_playersNotListeningnull1067     fun testInvisibleToUserAndExpanded_playersNotListening() {
1068         // Add players to carousel.
1069         testPlayerOrdering()
1070 
1071         // Make the carousel visible to user in expanded layout.
1072         mediaCarouselController.currentlyExpanded = true
1073         mediaCarouselController.mediaCarouselScrollHandler.visibleToUser = true
1074 
1075         // panel is the player for each MediaPlayerData.
1076         // Verify that seekbar listening attribute in media control panel is set to true.
1077         verify(panel, times(MediaPlayerData.players().size)).listening = true
1078 
1079         // Make the carousel invisible to user.
1080         mediaCarouselController.mediaCarouselScrollHandler.visibleToUser = false
1081 
1082         // panel is the player for each MediaPlayerData.
1083         // Verify that seekbar listening attribute in media control panel is set to false.
1084         verify(panel, times(MediaPlayerData.players().size)).listening = false
1085     }
1086 
1087     @Test
testVisibleToUserAndExpanded_playersListeningnull1088     fun testVisibleToUserAndExpanded_playersListening() {
1089         // Add players to carousel.
1090         testPlayerOrdering()
1091 
1092         // Make the carousel visible to user in expanded layout.
1093         mediaCarouselController.currentlyExpanded = true
1094         mediaCarouselController.mediaCarouselScrollHandler.visibleToUser = true
1095 
1096         // panel is the player for each MediaPlayerData.
1097         // Verify that seekbar listening attribute in media control panel is set to true.
1098         verify(panel, times(MediaPlayerData.players().size)).listening = true
1099     }
1100 
1101     @Test
testUMOCollapsed_playersNotListeningnull1102     fun testUMOCollapsed_playersNotListening() {
1103         // Add players to carousel.
1104         testPlayerOrdering()
1105 
1106         // Make the carousel in collapsed layout.
1107         mediaCarouselController.currentlyExpanded = false
1108 
1109         // panel is the player for each MediaPlayerData.
1110         // Verify that seekbar listening attribute in media control panel is set to false.
1111         verify(panel, times(MediaPlayerData.players().size)).listening = false
1112 
1113         // Make the carousel visible to user.
1114         reset(panel)
1115         mediaCarouselController.mediaCarouselScrollHandler.visibleToUser = true
1116 
1117         // Verify that seekbar listening attribute in media control panel is set to false.
1118         verify(panel, times(MediaPlayerData.players().size)).listening = false
1119     }
1120 
1121     @Test
testOnHostStateChanged_updateVisibilitynull1122     fun testOnHostStateChanged_updateVisibility() {
1123         var stateUpdated = false
1124         mediaCarouselController.updateUserVisibility = { stateUpdated = true }
1125 
1126         // When the host state updates
1127         hostStateCallback.value!!.onHostStateChanged(LOCATION_QS, mediaHostState)
1128 
1129         // Then the carousel visibility is updated
1130         assertTrue(stateUpdated)
1131     }
1132 
1133     @Test
testAnimationScaleChanged_mediaControlPanelsNotifiednull1134     fun testAnimationScaleChanged_mediaControlPanelsNotified() {
1135         MediaPlayerData.addMediaPlayer("key", DATA, panel, clock, isSsReactivated = false)
1136 
1137         globalSettings.putFloat(Settings.Global.ANIMATOR_DURATION_SCALE, 0f)
1138         settingsObserverCaptor.value!!.onChange(false)
1139         verify(panel).updateAnimatorDurationScale()
1140     }
1141 
1142     @DisableSceneContainer
1143     @Test
swipeToDismiss_pausedAndResumeOff_userInitiatednull1144     fun swipeToDismiss_pausedAndResumeOff_userInitiated() {
1145         verify(mediaDataManager).addListener(capture(listener))
1146 
1147         // When resumption is disabled, paused media should be dismissed after being swiped away
1148         Settings.Secure.putInt(context.contentResolver, Settings.Secure.MEDIA_CONTROLS_RESUME, 0)
1149         val pausedMedia = DATA.copy(isPlaying = false)
1150         listener.value.onMediaDataLoaded(PAUSED_LOCAL, PAUSED_LOCAL, pausedMedia)
1151         runAllReady()
1152         mediaCarouselController.onSwipeToDismiss()
1153 
1154         // When it can be removed immediately on update
1155         whenever(visualStabilityProvider.isReorderingAllowed).thenReturn(true)
1156         val inactiveMedia = pausedMedia.copy(active = false)
1157         listener.value.onMediaDataLoaded(PAUSED_LOCAL, PAUSED_LOCAL, inactiveMedia)
1158         runAllReady()
1159 
1160         // This is processed as a user-initiated dismissal
1161         verify(debugLogger).logMediaRemoved(eq(PAUSED_LOCAL), eq(true))
1162         verify(mediaDataManager).dismissMediaData(eq(PAUSED_LOCAL), anyLong(), eq(true))
1163     }
1164 
1165     @DisableSceneContainer
1166     @Test
swipeToDismiss_pausedAndResumeOff_delayed_userInitiatednull1167     fun swipeToDismiss_pausedAndResumeOff_delayed_userInitiated() {
1168         verify(mediaDataManager).addListener(capture(listener))
1169 
1170         // When resumption is disabled, paused media should be dismissed after being swiped away
1171         Settings.Secure.putInt(context.contentResolver, Settings.Secure.MEDIA_CONTROLS_RESUME, 0)
1172         mediaCarouselController.updateHostVisibility = {}
1173 
1174         val pausedMedia = DATA.copy(isPlaying = false)
1175         listener.value.onMediaDataLoaded(PAUSED_LOCAL, PAUSED_LOCAL, pausedMedia)
1176         runAllReady()
1177         mediaCarouselController.onSwipeToDismiss()
1178 
1179         // When it can't be removed immediately on update
1180         whenever(visualStabilityProvider.isReorderingAllowed).thenReturn(false)
1181         val inactiveMedia = pausedMedia.copy(active = false)
1182         listener.value.onMediaDataLoaded(PAUSED_LOCAL, PAUSED_LOCAL, inactiveMedia)
1183         runAllReady()
1184         visualStabilityCallback.value.onReorderingAllowed()
1185 
1186         // This is processed as a user-initiated dismissal
1187         verify(mediaDataManager).dismissMediaData(eq(PAUSED_LOCAL), anyLong(), eq(true))
1188     }
1189 
1190     /**
1191      * Helper method when a configuration change occurs.
1192      *
1193      * @param function called when a certain configuration change occurs.
1194      */
testConfigurationChangenull1195     private fun testConfigurationChange(function: () -> Unit) {
1196         verify(mediaDataManager).addListener(capture(listener))
1197         mediaCarouselController.pageIndicator = pageIndicator
1198         listener.value.onMediaDataLoaded(
1199             PLAYING_LOCAL,
1200             null,
1201             DATA.copy(
1202                 active = true,
1203                 isPlaying = true,
1204                 playbackLocation = MediaData.PLAYBACK_LOCAL,
1205                 resumption = false,
1206             ),
1207         )
1208         listener.value.onMediaDataLoaded(
1209             PAUSED_LOCAL,
1210             null,
1211             DATA.copy(
1212                 active = true,
1213                 isPlaying = false,
1214                 playbackLocation = MediaData.PLAYBACK_LOCAL,
1215                 resumption = false,
1216             ),
1217         )
1218         runAllReady()
1219 
1220         val playersSize = MediaPlayerData.players().size
1221         reset(pageIndicator)
1222         function()
1223         runAllReady()
1224 
1225         assertEquals(playersSize, MediaPlayerData.players().size)
1226         assertEquals(
1227             MediaPlayerData.getMediaPlayerIndex(PLAYING_LOCAL),
1228             mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex,
1229         )
1230     }
1231 
TestScopenull1232     private fun TestScope.setDeviceEntered(isEntered: Boolean) {
1233         if (isEntered) {
1234             // Unlock the device, marking the device as entered
1235             kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
1236                 SuccessFingerprintAuthenticationStatus(0, true)
1237             )
1238             runCurrent()
1239         }
1240         setScene(
1241             if (isEntered) {
1242                 Scenes.Gone
1243             } else {
1244                 Scenes.Lockscreen
1245             }
1246         )
1247         assertThat(kosmos.deviceEntryInteractor.isDeviceEntered.value).isEqualTo(isEntered)
1248     }
1249 
setScenenull1250     private fun TestScope.setScene(key: SceneKey) {
1251         kosmos.sceneInteractor.changeScene(key, "test")
1252         kosmos.sceneInteractor.setTransitionState(
1253             MutableStateFlow<ObservableTransitionState>(ObservableTransitionState.Idle(key))
1254         )
1255         runCurrent()
1256     }
1257 
runAllReadynull1258     private fun runAllReady() {
1259         if (mediaControlsUmoInflationInBackground()) {
1260             bgExecutor.runAllReady()
1261             uiExecutor.runAllReady()
1262         }
1263     }
1264 }
1265