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