1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.media.controls.ui.controller
18 
19 import android.graphics.Rect
20 import android.provider.Settings
21 import android.testing.TestableLooper
22 import android.view.ViewGroup
23 import android.widget.FrameLayout
24 import androidx.test.ext.junit.runners.AndroidJUnit4
25 import androidx.test.filters.SmallTest
26 import com.android.keyguard.KeyguardViewController
27 import com.android.systemui.SysuiTestCase
28 import com.android.systemui.bouncer.data.repository.keyguardBouncerRepository
29 import com.android.systemui.communal.data.repository.fakeCommunalSceneRepository
30 import com.android.systemui.communal.shared.model.CommunalScenes
31 import com.android.systemui.communal.ui.viewmodel.communalTransitionViewModel
32 import com.android.systemui.controls.controller.ControlsControllerImplTest.Companion.eq
33 import com.android.systemui.dreams.DreamOverlayStateController
34 import com.android.systemui.flags.DisableSceneContainer
35 import com.android.systemui.keyguard.WakefulnessLifecycle
36 import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
37 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
38 import com.android.systemui.keyguard.data.repository.keyguardRepository
39 import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
40 import com.android.systemui.keyguard.shared.model.KeyguardState
41 import com.android.systemui.kosmos.testScope
42 import com.android.systemui.media.controls.domain.pipeline.MediaDataManager
43 import com.android.systemui.media.controls.ui.view.MediaCarouselScrollHandler
44 import com.android.systemui.media.controls.ui.view.MediaHost
45 import com.android.systemui.media.controls.ui.view.MediaHostState
46 import com.android.systemui.media.dream.MediaDreamComplication
47 import com.android.systemui.plugins.statusbar.StatusBarStateController
48 import com.android.systemui.res.R
49 import com.android.systemui.shade.domain.interactor.ShadeInteractor
50 import com.android.systemui.statusbar.StatusBarState
51 import com.android.systemui.statusbar.SysuiStatusBarStateController
52 import com.android.systemui.statusbar.phone.KeyguardBypassController
53 import com.android.systemui.statusbar.policy.FakeConfigurationController
54 import com.android.systemui.statusbar.policy.KeyguardStateController
55 import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController
56 import com.android.systemui.testKosmos
57 import com.android.systemui.util.animation.UniqueObjectHostView
58 import com.android.systemui.util.mockito.mock
59 import com.android.systemui.util.mockito.nullable
60 import com.android.systemui.util.settings.FakeSettings
61 import com.android.systemui.utils.os.FakeHandler
62 import com.google.common.truth.Truth.assertThat
63 import kotlinx.coroutines.ExperimentalCoroutinesApi
64 import kotlinx.coroutines.flow.MutableStateFlow
65 import kotlinx.coroutines.test.runCurrent
66 import kotlinx.coroutines.test.runTest
67 import org.junit.Assert.assertNotNull
68 import org.junit.Before
69 import org.junit.Rule
70 import org.junit.Test
71 import org.junit.runner.RunWith
72 import org.mockito.ArgumentCaptor
73 import org.mockito.ArgumentMatchers
74 import org.mockito.ArgumentMatchers.anyBoolean
75 import org.mockito.ArgumentMatchers.anyLong
76 import org.mockito.Captor
77 import org.mockito.Mock
78 import org.mockito.Mockito.clearInvocations
79 import org.mockito.Mockito.times
80 import org.mockito.Mockito.verify
81 import org.mockito.Mockito.`when` as whenever
82 import org.mockito.junit.MockitoJUnit
83 import org.mockito.kotlin.any
84 import org.mockito.kotlin.anyOrNull
85 import org.mockito.kotlin.atLeastOnce
86 import org.mockito.kotlin.lastValue
87 
88 @OptIn(ExperimentalCoroutinesApi::class)
89 @SmallTest
90 @RunWith(AndroidJUnit4::class)
91 @TestableLooper.RunWithLooper(setAsMainLooper = true)
92 @DisableSceneContainer
93 class MediaHierarchyManagerTest : SysuiTestCase() {
94 
95     private val kosmos = testKosmos()
96 
97     @Mock private lateinit var lockHost: MediaHost
98     @Mock private lateinit var qsHost: MediaHost
99     @Mock private lateinit var qqsHost: MediaHost
100     @Mock private lateinit var hubModeHost: MediaHost
101     @Mock private lateinit var bypassController: KeyguardBypassController
102     @Mock private lateinit var keyguardStateController: KeyguardStateController
103     @Mock private lateinit var statusBarStateController: SysuiStatusBarStateController
104     @Mock private lateinit var mediaCarouselController: MediaCarouselController
105     @Mock private lateinit var mediaCarouselScrollHandler: MediaCarouselScrollHandler
106     @Mock private lateinit var wakefulnessLifecycle: WakefulnessLifecycle
107     @Mock private lateinit var keyguardViewController: KeyguardViewController
108     @Mock private lateinit var mediaDataManager: MediaDataManager
109     @Mock private lateinit var uniqueObjectHostView: UniqueObjectHostView
110     @Mock private lateinit var dreamOverlayStateController: DreamOverlayStateController
111     @Mock private lateinit var shadeInteractor: ShadeInteractor
112     @Mock lateinit var logger: MediaViewLogger
113     @Captor
114     private lateinit var wakefullnessObserver: ArgumentCaptor<(WakefulnessLifecycle.Observer)>
115     @Captor
116     private lateinit var statusBarCallback: ArgumentCaptor<(StatusBarStateController.StateListener)>
117     @Captor
118     private lateinit var dreamOverlayCallback:
119         ArgumentCaptor<(DreamOverlayStateController.Callback)>
120     @JvmField @Rule val mockito = MockitoJUnit.rule()
121     private val testScope = kosmos.testScope
122     private lateinit var mediaHierarchyManager: MediaHierarchyManager
123     private lateinit var isQsBypassingShade: MutableStateFlow<Boolean>
124     private lateinit var shadeExpansion: MutableStateFlow<Float>
125     private lateinit var anyShadeExpanded: MutableStateFlow<Boolean>
126     private lateinit var mediaFrame: ViewGroup
127     private val configurationController = FakeConfigurationController()
128     private val settings = FakeSettings()
129     private lateinit var testableLooper: TestableLooper
130     private lateinit var fakeHandler: FakeHandler
131     private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
132     private val keyguardRepository = kosmos.fakeKeyguardRepository
133 
134     @Before
setupnull135     fun setup() {
136         context
137             .getOrCreateTestableResources()
138             .addOverride(R.bool.config_use_split_notification_shade, false)
139         mediaFrame = FrameLayout(context)
140         testableLooper = TestableLooper.get(this)
141         fakeHandler = FakeHandler(testableLooper.looper)
142         whenever(mediaCarouselController.mediaFrame).thenReturn(mediaFrame)
143         isQsBypassingShade = MutableStateFlow(false)
144         shadeExpansion = MutableStateFlow(0f)
145         anyShadeExpanded = MutableStateFlow(false)
146         whenever(shadeInteractor.isQsBypassingShade).thenReturn(isQsBypassingShade)
147         whenever(shadeInteractor.shadeExpansion).thenReturn(shadeExpansion)
148         whenever(shadeInteractor.isAnyFullyExpanded).thenReturn(anyShadeExpanded)
149         mediaHierarchyManager =
150             MediaHierarchyManager(
151                 context,
152                 statusBarStateController,
153                 keyguardStateController,
154                 bypassController,
155                 mediaCarouselController,
156                 mediaDataManager,
157                 keyguardViewController,
158                 dreamOverlayStateController,
159                 kosmos.keyguardInteractor,
160                 kosmos.communalTransitionViewModel,
161                 configurationController,
162                 wakefulnessLifecycle,
163                 shadeInteractor,
164                 settings,
165                 fakeHandler,
166                 testScope.backgroundScope,
167                 ResourcesSplitShadeStateController(),
168                 logger,
169             )
170         verify(wakefulnessLifecycle).addObserver(wakefullnessObserver.capture())
171         verify(statusBarStateController).addCallback(statusBarCallback.capture())
172         verify(dreamOverlayStateController).addCallback(dreamOverlayCallback.capture())
173         setupHost(lockHost, MediaHierarchyManager.LOCATION_LOCKSCREEN, LOCKSCREEN_TOP)
174         setupHost(qsHost, MediaHierarchyManager.LOCATION_QS, QS_TOP)
175         setupHost(qqsHost, MediaHierarchyManager.LOCATION_QQS, QQS_TOP)
176         setupHost(hubModeHost, MediaHierarchyManager.LOCATION_COMMUNAL_HUB, COMMUNAL_TOP)
177         whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE)
178         whenever(mediaDataManager.hasActiveMedia()).thenReturn(true)
179         whenever(mediaCarouselController.mediaCarouselScrollHandler)
180             .thenReturn(mediaCarouselScrollHandler)
181         val observer = wakefullnessObserver.value
182         assertNotNull("lifecycle observer wasn't registered", observer)
183         observer.onFinishedWakingUp()
184         // We'll use the viewmanager to verify a few calls below, let's reset this.
185         clearInvocations(mediaCarouselController)
186     }
187 
setupHostnull188     private fun setupHost(host: MediaHost, location: Int, top: Int) {
189         whenever(host.location).thenReturn(location)
190         whenever(host.currentBounds).thenReturn(Rect(0, top, 0, top))
191         whenever(host.hostView).thenReturn(uniqueObjectHostView)
192         whenever(host.visible).thenReturn(true)
193         mediaHierarchyManager.register(host)
194     }
195 
196     @Test
testHostViewSetOnRegisternull197     fun testHostViewSetOnRegister() {
198         val host = mediaHierarchyManager.register(lockHost)
199         verify(lockHost).hostView = eq(host)
200     }
201 
202     @Test
testBlockedWhenScreenTurningOffnull203     fun testBlockedWhenScreenTurningOff() {
204         // Let's set it onto QS:
205         mediaHierarchyManager.qsExpansion = 1.0f
206         verify(mediaCarouselController)
207             .onDesiredLocationChanged(
208                 ArgumentMatchers.anyInt(),
209                 any<MediaHostState>(),
210                 anyBoolean(),
211                 anyLong(),
212                 anyLong()
213             )
214         val observer = wakefullnessObserver.value
215         assertNotNull("lifecycle observer wasn't registered", observer)
216         observer.onStartedGoingToSleep()
217         clearInvocations(mediaCarouselController)
218         mediaHierarchyManager.qsExpansion = 0.0f
219         verify(mediaCarouselController, times(0))
220             .onDesiredLocationChanged(
221                 ArgumentMatchers.anyInt(),
222                 any<MediaHostState>(),
223                 anyBoolean(),
224                 anyLong(),
225                 anyLong()
226             )
227     }
228 
229     @Test
testBlockedWhenConfigurationChangesAndScreenOffnull230     fun testBlockedWhenConfigurationChangesAndScreenOff() {
231         // Let's set it onto QS:
232         mediaHierarchyManager.qsExpansion = 1.0f
233         verify(mediaCarouselController)
234             .onDesiredLocationChanged(
235                 ArgumentMatchers.anyInt(),
236                 any<MediaHostState>(),
237                 anyBoolean(),
238                 anyLong(),
239                 anyLong()
240             )
241         val observer = wakefullnessObserver.value
242         assertNotNull("lifecycle observer wasn't registered", observer)
243         observer.onStartedGoingToSleep()
244         clearInvocations(mediaCarouselController)
245         configurationController.notifyConfigurationChanged()
246         verify(mediaCarouselController, times(0))
247             .onDesiredLocationChanged(
248                 ArgumentMatchers.anyInt(),
249                 any<MediaHostState>(),
250                 anyBoolean(),
251                 anyLong(),
252                 anyLong()
253             )
254     }
255 
256     @Test
testAllowedWhenConfigurationChangesnull257     fun testAllowedWhenConfigurationChanges() {
258         // Let's set it onto QS:
259         mediaHierarchyManager.qsExpansion = 1.0f
260         verify(mediaCarouselController)
261             .onDesiredLocationChanged(
262                 ArgumentMatchers.anyInt(),
263                 any<MediaHostState>(),
264                 anyBoolean(),
265                 anyLong(),
266                 anyLong()
267             )
268         clearInvocations(mediaCarouselController)
269         configurationController.notifyConfigurationChanged()
270         verify(mediaCarouselController)
271             .onDesiredLocationChanged(
272                 ArgumentMatchers.anyInt(),
273                 any<MediaHostState>(),
274                 anyBoolean(),
275                 anyLong(),
276                 anyLong()
277             )
278     }
279 
280     @Test
testAllowedWhenNotTurningOffnull281     fun testAllowedWhenNotTurningOff() {
282         // Let's set it onto QS:
283         mediaHierarchyManager.qsExpansion = 1.0f
284         verify(mediaCarouselController)
285             .onDesiredLocationChanged(
286                 ArgumentMatchers.anyInt(),
287                 any<MediaHostState>(),
288                 anyBoolean(),
289                 anyLong(),
290                 anyLong()
291             )
292         val observer = wakefullnessObserver.value
293         assertNotNull("lifecycle observer wasn't registered", observer)
294         clearInvocations(mediaCarouselController)
295         mediaHierarchyManager.qsExpansion = 0.0f
296         verify(mediaCarouselController)
297             .onDesiredLocationChanged(
298                 ArgumentMatchers.anyInt(),
299                 any<MediaHostState>(),
300                 anyBoolean(),
301                 anyLong(),
302                 anyLong()
303             )
304     }
305 
306     @Test
testGoingToFullShadenull307     fun testGoingToFullShade() {
308         goToLockscreen()
309 
310         // Let's transition all the way to full shade
311         mediaHierarchyManager.setTransitionToFullShadeAmount(100000f)
312         verify(mediaCarouselController)
313             .onDesiredLocationChanged(
314                 eq(MediaHierarchyManager.LOCATION_QQS),
315                 any<MediaHostState>(),
316                 eq(false),
317                 anyLong(),
318                 anyLong()
319             )
320         clearInvocations(mediaCarouselController)
321 
322         // Let's go back to the lock screen
323         mediaHierarchyManager.setTransitionToFullShadeAmount(0.0f)
324         verify(mediaCarouselController)
325             .onDesiredLocationChanged(
326                 eq(MediaHierarchyManager.LOCATION_LOCKSCREEN),
327                 any<MediaHostState>(),
328                 eq(false),
329                 anyLong(),
330                 anyLong()
331             )
332 
333         // Let's make sure alpha is set
334         mediaHierarchyManager.setTransitionToFullShadeAmount(2.0f)
335         assertThat(mediaFrame.alpha).isNotEqualTo(1.0f)
336     }
337 
338     @Test
testTransformationOnLockScreenIsFadingnull339     fun testTransformationOnLockScreenIsFading() {
340         goToLockscreen()
341         expandQS()
342 
343         val transformType = mediaHierarchyManager.calculateTransformationType()
344         assertThat(transformType).isEqualTo(MediaHierarchyManager.TRANSFORMATION_TYPE_FADE)
345     }
346 
347     @Test
calculateTransformationType_notOnLockscreen_returnsTransitionnull348     fun calculateTransformationType_notOnLockscreen_returnsTransition() {
349         expandQS()
350 
351         val transformType = mediaHierarchyManager.calculateTransformationType()
352 
353         assertThat(transformType).isEqualTo(MediaHierarchyManager.TRANSFORMATION_TYPE_TRANSITION)
354     }
355 
356     @Test
calculateTransformationType_onLockscreen_returnsTransitionnull357     fun calculateTransformationType_onLockscreen_returnsTransition() {
358         goToLockscreen()
359         expandQS()
360 
361         val transformType = mediaHierarchyManager.calculateTransformationType()
362 
363         assertThat(transformType).isEqualTo(MediaHierarchyManager.TRANSFORMATION_TYPE_FADE)
364     }
365 
366     @Test
calculateTransformationType_onLockShade_inSplitShade_goingToFullShade_returnsTransitionnull367     fun calculateTransformationType_onLockShade_inSplitShade_goingToFullShade_returnsTransition() {
368         enableSplitShade()
369         goToLockscreen()
370         expandQS()
371         mediaHierarchyManager.setTransitionToFullShadeAmount(10000f)
372 
373         val transformType = mediaHierarchyManager.calculateTransformationType()
374         assertThat(transformType).isEqualTo(MediaHierarchyManager.TRANSFORMATION_TYPE_TRANSITION)
375     }
376 
377     @Test
calculateTransformationType_onLockSplitShade_goingToFullShade_mediaInvisible_returnsFadenull378     fun calculateTransformationType_onLockSplitShade_goingToFullShade_mediaInvisible_returnsFade() {
379         enableSplitShade()
380         goToLockscreen()
381         expandQS()
382         whenever(lockHost.visible).thenReturn(false)
383         mediaHierarchyManager.setTransitionToFullShadeAmount(10000f)
384 
385         val transformType = mediaHierarchyManager.calculateTransformationType()
386         assertThat(transformType).isEqualTo(MediaHierarchyManager.TRANSFORMATION_TYPE_FADE)
387     }
388 
389     @Test
calculateTransformationType_onLockShade_inSplitShade_notExpanding_returnsFadenull390     fun calculateTransformationType_onLockShade_inSplitShade_notExpanding_returnsFade() {
391         enableSplitShade()
392         goToLockscreen()
393         goToLockedShade()
394         expandQS()
395         mediaHierarchyManager.setTransitionToFullShadeAmount(0f)
396 
397         val transformType = mediaHierarchyManager.calculateTransformationType()
398         assertThat(transformType).isEqualTo(MediaHierarchyManager.TRANSFORMATION_TYPE_FADE)
399     }
400 
401     @Test
testTransformationOnLockScreenToQQSisFadingnull402     fun testTransformationOnLockScreenToQQSisFading() {
403         goToLockscreen()
404         goToLockedShade()
405 
406         val transformType = mediaHierarchyManager.calculateTransformationType()
407         assertThat(transformType).isEqualTo(MediaHierarchyManager.TRANSFORMATION_TYPE_FADE)
408     }
409 
410     @Test
testCloseGutsRelayToCarouselnull411     fun testCloseGutsRelayToCarousel() {
412         mediaHierarchyManager.closeGuts()
413 
414         verify(mediaCarouselController).closeGuts()
415     }
416 
417     @Test
testCloseGutsWhenDozenull418     fun testCloseGutsWhenDoze() {
419         statusBarCallback.value.onDozingChanged(true)
420 
421         verify(mediaCarouselController).closeGuts()
422     }
423 
424     @Test
getGuidedTransformationTranslationY_notInGuidedTransformation_returnsNegativeNumbernull425     fun getGuidedTransformationTranslationY_notInGuidedTransformation_returnsNegativeNumber() {
426         assertThat(mediaHierarchyManager.getGuidedTransformationTranslationY()).isLessThan(0)
427     }
428 
429     @Test
getGuidedTransformationTranslationY_inGuidedTransformation_returnsCurrentTranslationnull430     fun getGuidedTransformationTranslationY_inGuidedTransformation_returnsCurrentTranslation() {
431         enterGuidedTransformation()
432 
433         val expectedTranslation = LOCKSCREEN_TOP - QS_TOP
434         assertThat(mediaHierarchyManager.getGuidedTransformationTranslationY())
435             .isEqualTo(expectedTranslation)
436     }
437 
438     @Test
getGuidedTransformationTranslationY_previousHostInvisible_returnsZeronull439     fun getGuidedTransformationTranslationY_previousHostInvisible_returnsZero() {
440         goToLockscreen()
441         enterGuidedTransformation()
442         whenever(lockHost.visible).thenReturn(false)
443 
444         assertThat(mediaHierarchyManager.getGuidedTransformationTranslationY()).isEqualTo(0)
445     }
446 
447     @Test
isCurrentlyInGuidedTransformation_hostsVisible_returnsTruenull448     fun isCurrentlyInGuidedTransformation_hostsVisible_returnsTrue() {
449         goToLockscreen()
450         enterGuidedTransformation()
451         whenever(lockHost.visible).thenReturn(true)
452         whenever(qsHost.visible).thenReturn(true)
453         whenever(qqsHost.visible).thenReturn(true)
454 
455         assertThat(mediaHierarchyManager.isCurrentlyInGuidedTransformation()).isTrue()
456     }
457 
458     @OptIn(ExperimentalCoroutinesApi::class)
459     @Test
isCurrentlyInGuidedTransformation_hostsVisible_expandImmediateEnabled_returnsFalsenull460     fun isCurrentlyInGuidedTransformation_hostsVisible_expandImmediateEnabled_returnsFalse() =
461         testScope.runTest {
462             runCurrent()
463             isQsBypassingShade.value = true
464             runCurrent()
465             goToLockscreen()
466             enterGuidedTransformation()
467             whenever(lockHost.visible).thenReturn(true)
468             whenever(qsHost.visible).thenReturn(true)
469             whenever(qqsHost.visible).thenReturn(true)
470 
471             assertThat(mediaHierarchyManager.isCurrentlyInGuidedTransformation()).isFalse()
472         }
473 
474     @Test
isCurrentlyInGuidedTransformation_hostNotVisible_returnsFalse_with_activenull475     fun isCurrentlyInGuidedTransformation_hostNotVisible_returnsFalse_with_active() {
476         goToLockscreen()
477         enterGuidedTransformation()
478         whenever(lockHost.visible).thenReturn(false)
479         whenever(qsHost.visible).thenReturn(true)
480         whenever(qqsHost.visible).thenReturn(true)
481         whenever(mediaDataManager.hasActiveMediaOrRecommendation()).thenReturn(true)
482 
483         assertThat(mediaHierarchyManager.isCurrentlyInGuidedTransformation()).isFalse()
484     }
485 
486     @Test
isCurrentlyInGuidedTransformation_hostNotVisible_returnsTrue_without_activenull487     fun isCurrentlyInGuidedTransformation_hostNotVisible_returnsTrue_without_active() {
488         // To keep the appearing behavior, we need to be in a guided transition
489         goToLockscreen()
490         enterGuidedTransformation()
491         whenever(lockHost.visible).thenReturn(false)
492         whenever(qsHost.visible).thenReturn(true)
493         whenever(qqsHost.visible).thenReturn(true)
494         whenever(mediaDataManager.hasActiveMediaOrRecommendation()).thenReturn(false)
495 
496         assertThat(mediaHierarchyManager.isCurrentlyInGuidedTransformation()).isTrue()
497     }
498 
499     @Test
isCurrentlyInGuidedTransformation_desiredLocationIsHub_returnsFalsenull500     fun isCurrentlyInGuidedTransformation_desiredLocationIsHub_returnsFalse() =
501         testScope.runTest {
502             goToLockscreen()
503             keyguardTransitionRepository.sendTransitionSteps(
504                 from = KeyguardState.LOCKSCREEN,
505                 to = KeyguardState.GLANCEABLE_HUB,
506                 testScope = testScope,
507             )
508             kosmos.fakeCommunalSceneRepository.changeScene(CommunalScenes.Communal)
509             runCurrent()
510             mediaHierarchyManager.qsExpansion = 0f
511             mediaHierarchyManager.setTransitionToFullShadeAmount(123f)
512 
513             whenever(lockHost.visible).thenReturn(true)
514             whenever(qsHost.visible).thenReturn(true)
515             whenever(qqsHost.visible).thenReturn(true)
516             whenever(hubModeHost.visible).thenReturn(true)
517 
518             assertThat(mediaHierarchyManager.isCurrentlyInGuidedTransformation()).isFalse()
519         }
520 
521     @Test
testDreamnull522     fun testDream() {
523         goToDream()
524         setMediaDreamComplicationEnabled(true)
525         verify(mediaCarouselController)
526             .onDesiredLocationChanged(
527                 eq(MediaHierarchyManager.LOCATION_DREAM_OVERLAY),
528                 nullable(),
529                 eq(false),
530                 anyLong(),
531                 anyLong()
532             )
533         clearInvocations(mediaCarouselController)
534 
535         setMediaDreamComplicationEnabled(false)
536         verify(mediaCarouselController)
537             .onDesiredLocationChanged(
538                 eq(MediaHierarchyManager.LOCATION_QQS),
539                 any<MediaHostState>(),
540                 eq(false),
541                 anyLong(),
542                 anyLong()
543             )
544     }
545 
546     @Test
testCommunalLocationnull547     fun testCommunalLocation() =
548         testScope.runTest {
549             keyguardTransitionRepository.sendTransitionSteps(
550                 from = KeyguardState.LOCKSCREEN,
551                 to = KeyguardState.GLANCEABLE_HUB,
552                 testScope = testScope,
553             )
554             kosmos.fakeCommunalSceneRepository.changeScene(CommunalScenes.Communal)
555             runCurrent()
556             verify(mediaCarouselController)
557                 .onDesiredLocationChanged(
558                     eq(MediaHierarchyManager.LOCATION_COMMUNAL_HUB),
559                     nullable(),
560                     eq(false),
561                     anyLong(),
562                     anyLong()
563                 )
564             clearInvocations(mediaCarouselController)
565 
566             keyguardTransitionRepository.sendTransitionSteps(
567                 from = KeyguardState.GLANCEABLE_HUB,
568                 to = KeyguardState.LOCKSCREEN,
569                 testScope = testScope,
570             )
571             kosmos.fakeCommunalSceneRepository.changeScene(CommunalScenes.Blank)
572             runCurrent()
573             verify(mediaCarouselController)
574                 .onDesiredLocationChanged(
575                     eq(MediaHierarchyManager.LOCATION_QQS),
576                     any<MediaHostState>(),
577                     eq(false),
578                     anyLong(),
579                     anyLong()
580                 )
581         }
582 
583     @Test
testCommunalLocationVisibilityWithShadeShowingnull584     fun testCommunalLocationVisibilityWithShadeShowing() =
585         testScope.runTest {
586             whenever(mediaDataManager.hasActiveMediaOrRecommendation()).thenReturn(true)
587             keyguardTransitionRepository.sendTransitionSteps(
588                 from = KeyguardState.LOCKSCREEN,
589                 to = KeyguardState.GLANCEABLE_HUB,
590                 testScope = testScope,
591             )
592             kosmos.fakeCommunalSceneRepository.changeScene(CommunalScenes.Communal)
593             runCurrent()
594             verify(mediaCarouselController)
595                 .onDesiredLocationChanged(
596                     eq(MediaHierarchyManager.LOCATION_COMMUNAL_HUB),
597                     nullable(),
598                     eq(false),
599                     anyLong(),
600                     anyLong()
601                 )
602 
603             val captor = ArgumentCaptor.forClass(Boolean::class.java)
604             verify(mediaCarouselScrollHandler, atLeastOnce()).visibleToUser = captor.capture()
605 
606             assertThat(captor.lastValue).isTrue()
607 
608             clearInvocations(mediaCarouselScrollHandler)
609             anyShadeExpanded.value = true
610             runCurrent()
611             verify(mediaCarouselScrollHandler, atLeastOnce()).visibleToUser = captor.capture()
612 
613             assertThat(captor.lastValue).isFalse()
614         }
615 
616     @Test
testCommunalLocationVisibilityWithPrimaryBouncerShowingnull617     fun testCommunalLocationVisibilityWithPrimaryBouncerShowing() =
618         testScope.runTest {
619             whenever(mediaDataManager.hasActiveMediaOrRecommendation()).thenReturn(true)
620             keyguardTransitionRepository.sendTransitionSteps(
621                 from = KeyguardState.LOCKSCREEN,
622                 to = KeyguardState.GLANCEABLE_HUB,
623                 testScope = testScope,
624             )
625             kosmos.fakeCommunalSceneRepository.changeScene(CommunalScenes.Communal)
626             runCurrent()
627             verify(mediaCarouselController)
628                 .onDesiredLocationChanged(
629                     eq(MediaHierarchyManager.LOCATION_COMMUNAL_HUB),
630                     nullable(),
631                     eq(false),
632                     anyLong(),
633                     anyLong()
634                 )
635 
636             val captor = ArgumentCaptor.forClass(Boolean::class.java)
637             verify(mediaCarouselScrollHandler, atLeastOnce()).visibleToUser = captor.capture()
638 
639             assertThat(captor.lastValue).isTrue()
640 
641             clearInvocations(mediaCarouselScrollHandler)
642             kosmos.keyguardBouncerRepository.setPrimaryShow(true)
643             runCurrent()
644             verify(mediaCarouselScrollHandler, atLeastOnce()).visibleToUser = captor.capture()
645 
646             assertThat(captor.lastValue).isFalse()
647         }
648 
649     @Test
testCommunalLocation_showsOverLockscreennull650     fun testCommunalLocation_showsOverLockscreen() =
651         testScope.runTest {
652             // Device is on lock screen.
653             whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
654 
655             // UMO goes to communal from the lock screen.
656             keyguardTransitionRepository.sendTransitionSteps(
657                 from = KeyguardState.LOCKSCREEN,
658                 to = KeyguardState.GLANCEABLE_HUB,
659                 testScope = testScope,
660             )
661             kosmos.fakeCommunalSceneRepository.changeScene(CommunalScenes.Communal)
662             runCurrent()
663             verify(mediaCarouselController)
664                 .onDesiredLocationChanged(
665                     eq(MediaHierarchyManager.LOCATION_COMMUNAL_HUB),
666                     nullable(),
667                     eq(false),
668                     anyLong(),
669                     anyLong()
670                 )
671         }
672 
673     @Test
testCommunalLocation_showsUntilQsExpandsnull674     fun testCommunalLocation_showsUntilQsExpands() =
675         testScope.runTest {
676             // Device is on lock screen.
677             whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
678 
679             keyguardTransitionRepository.sendTransitionSteps(
680                 from = KeyguardState.LOCKSCREEN,
681                 to = KeyguardState.GLANCEABLE_HUB,
682                 testScope = testScope,
683             )
684             kosmos.fakeCommunalSceneRepository.changeScene(CommunalScenes.Communal)
685             runCurrent()
686             verify(mediaCarouselController)
687                 .onDesiredLocationChanged(
688                     eq(MediaHierarchyManager.LOCATION_COMMUNAL_HUB),
689                     nullable(),
690                     eq(false),
691                     anyLong(),
692                     anyLong()
693                 )
694             clearInvocations(mediaCarouselController)
695 
696             // Start opening the shade.
697             mediaHierarchyManager.qsExpansion = 0.1f
698             runCurrent()
699 
700             // UMO goes to the shade instead.
701             verify(mediaCarouselController)
702                 .onDesiredLocationChanged(
703                     eq(MediaHierarchyManager.LOCATION_QS),
704                     any<MediaHostState>(),
705                     eq(false),
706                     anyLong(),
707                     anyLong()
708                 )
709         }
710 
711     @Test
testCommunalLocation_whenDreamingAndShadeExpandingnull712     fun testCommunalLocation_whenDreamingAndShadeExpanding() =
713         testScope.runTest {
714             keyguardRepository.setDreaming(true)
715             runCurrent()
716             keyguardTransitionRepository.sendTransitionSteps(
717                 from = KeyguardState.DREAMING,
718                 to = KeyguardState.GLANCEABLE_HUB,
719                 testScope = testScope,
720             )
721             kosmos.fakeCommunalSceneRepository.changeScene(CommunalScenes.Communal)
722             runCurrent()
723             // Mock the behavior for dreaming that pulling down shade will immediately set QS as
724             // expanded
725             expandQS()
726             // Starts opening the shade
727             shadeExpansion.value = 0.1f
728             runCurrent()
729 
730             // UMO shows on hub
731             verify(mediaCarouselController)
732                 .onDesiredLocationChanged(
733                     eq(MediaHierarchyManager.LOCATION_COMMUNAL_HUB),
734                     anyOrNull(),
735                     eq(false),
736                     anyLong(),
737                     anyLong()
738                 )
739             clearInvocations(mediaCarouselController)
740 
741             // The shade is opened enough to make QS elements visible
742             shadeExpansion.value = 0.5f
743             runCurrent()
744 
745             // UMO shows on QS
746             verify(mediaCarouselController)
747                 .onDesiredLocationChanged(
748                     eq(MediaHierarchyManager.LOCATION_QS),
749                     any<MediaHostState>(),
750                     eq(false),
751                     anyLong(),
752                     anyLong()
753                 )
754         }
755 
756     @Test
testQsExpandedChanged_noQqsMedianull757     fun testQsExpandedChanged_noQqsMedia() {
758         // When we are looking at QQS with active media
759         whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE)
760         whenever(statusBarStateController.isExpanded).thenReturn(true)
761 
762         // When there is no longer any active media
763         whenever(mediaDataManager.hasActiveMediaOrRecommendation()).thenReturn(false)
764         mediaHierarchyManager.qsExpanded = false
765 
766         // Then the carousel is set to not visible
767         verify(mediaCarouselScrollHandler).visibleToUser = false
768         assertThat(mediaCarouselScrollHandler.visibleToUser).isFalse()
769     }
770 
enableSplitShadenull771     private fun enableSplitShade() {
772         context
773             .getOrCreateTestableResources()
774             .addOverride(R.bool.config_use_split_notification_shade, true)
775         configurationController.notifyConfigurationChanged()
776     }
777 
goToLockscreennull778     private fun goToLockscreen() {
779         whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
780         settings.putInt(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, 1)
781         statusBarCallback.value.onStatePreChange(StatusBarState.SHADE, StatusBarState.KEYGUARD)
782         whenever(dreamOverlayStateController.isOverlayActive).thenReturn(false)
783         dreamOverlayCallback.value.onStateChanged()
784         clearInvocations(mediaCarouselController)
785     }
786 
goToLockedShadenull787     private fun goToLockedShade() {
788         whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE_LOCKED)
789         statusBarCallback.value.onStatePreChange(
790             StatusBarState.KEYGUARD,
791             StatusBarState.SHADE_LOCKED
792         )
793     }
794 
goToDreamnull795     private fun goToDream() {
796         whenever(dreamOverlayStateController.isOverlayActive).thenReturn(true)
797         dreamOverlayCallback.value.onStateChanged()
798     }
799 
setMediaDreamComplicationEnablednull800     private fun setMediaDreamComplicationEnabled(enabled: Boolean) {
801         val complications = if (enabled) listOf(mock<MediaDreamComplication>()) else emptyList()
802         whenever(dreamOverlayStateController.complications).thenReturn(complications)
803         dreamOverlayCallback.value.onComplicationsChanged()
804     }
805 
expandQSnull806     private fun expandQS() {
807         mediaHierarchyManager.qsExpansion = 1.0f
808     }
809 
enterGuidedTransformationnull810     private fun enterGuidedTransformation() {
811         mediaHierarchyManager.qsExpansion = 1.0f
812         goToLockscreen()
813         mediaHierarchyManager.setTransitionToFullShadeAmount(123f)
814     }
815 
816     companion object {
817         private const val QQS_TOP = 123
818         private const val QS_TOP = 456
819         private const val LOCKSCREEN_TOP = 789
820         private const val COMMUNAL_TOP = 111
821     }
822 }
823