1 /*
2  * Copyright (C) 2023 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.shade
18 
19 import android.graphics.Rect
20 import android.os.PowerManager
21 import android.platform.test.annotations.DisableFlags
22 import android.platform.test.annotations.EnableFlags
23 import android.testing.AndroidTestingRunner
24 import android.testing.TestableLooper
25 import android.testing.ViewUtils
26 import android.view.MotionEvent
27 import android.view.View
28 import android.widget.FrameLayout
29 import androidx.lifecycle.Lifecycle
30 import androidx.lifecycle.LifecycleOwner
31 import androidx.test.filters.SmallTest
32 import com.android.compose.animation.scene.ObservableTransitionState
33 import com.android.compose.animation.scene.SceneKey
34 import com.android.systemui.Flags
35 import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_FULLSCREEN_SWIPE
36 import com.android.systemui.SysuiTestCase
37 import com.android.systemui.ambient.touch.TouchHandler
38 import com.android.systemui.ambient.touch.TouchMonitor
39 import com.android.systemui.ambient.touch.dagger.AmbientTouchComponent
40 import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository
41 import com.android.systemui.communal.data.repository.FakeCommunalSceneRepository
42 import com.android.systemui.communal.data.repository.fakeCommunalSceneRepository
43 import com.android.systemui.communal.domain.interactor.communalInteractor
44 import com.android.systemui.communal.domain.interactor.setCommunalAvailable
45 import com.android.systemui.communal.shared.model.CommunalScenes
46 import com.android.systemui.communal.ui.compose.CommunalContent
47 import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
48 import com.android.systemui.communal.util.CommunalColors
49 import com.android.systemui.coroutines.collectLastValue
50 import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
51 import com.android.systemui.kosmos.Kosmos
52 import com.android.systemui.kosmos.testDispatcher
53 import com.android.systemui.kosmos.testScope
54 import com.android.systemui.res.R
55 import com.android.systemui.scene.shared.model.sceneDataSourceDelegator
56 import com.android.systemui.shade.domain.interactor.shadeInteractor
57 import com.android.systemui.statusbar.notification.stack.notificationStackScrollLayoutController
58 import com.android.systemui.testKosmos
59 import com.android.systemui.util.mockito.any
60 import com.google.common.truth.Truth.assertThat
61 import kotlinx.coroutines.ExperimentalCoroutinesApi
62 import kotlinx.coroutines.flow.flowOf
63 import kotlinx.coroutines.launch
64 import kotlinx.coroutines.test.UnconfinedTestDispatcher
65 import kotlinx.coroutines.test.runTest
66 import org.junit.After
67 import org.junit.Assert.assertThrows
68 import org.junit.Before
69 import org.junit.Test
70 import org.junit.runner.RunWith
71 import org.mockito.ArgumentMatchers.anyFloat
72 import org.mockito.Mock
73 import org.mockito.Mockito.times
74 import org.mockito.Mockito.verify
75 import org.mockito.Mockito.`when`
76 import org.mockito.MockitoAnnotations
77 
78 @ExperimentalCoroutinesApi
79 @RunWith(AndroidTestingRunner::class)
80 @TestableLooper.RunWithLooper(setAsMainLooper = true)
81 @SmallTest
82 class GlanceableHubContainerControllerTest : SysuiTestCase() {
83     private val kosmos: Kosmos =
<lambda>null84         testKosmos().apply {
85             // UnconfinedTestDispatcher makes testing simpler due to CommunalInteractor flows using
86             // SharedFlow
87             testDispatcher = UnconfinedTestDispatcher()
88         }
89 
90     @Mock private lateinit var communalViewModel: CommunalViewModel
91     @Mock private lateinit var powerManager: PowerManager
92     @Mock private lateinit var touchMonitor: TouchMonitor
93     @Mock private lateinit var communalColors: CommunalColors
94     @Mock private lateinit var communalContent: CommunalContent
95     private lateinit var ambientTouchComponentFactory: AmbientTouchComponent.Factory
96 
97     private lateinit var parentView: FrameLayout
98     private lateinit var containerView: View
99     private lateinit var testableLooper: TestableLooper
100 
101     private lateinit var communalRepository: FakeCommunalSceneRepository
102     private lateinit var underTest: GlanceableHubContainerController
103 
104     @Before
setUpnull105     fun setUp() {
106         MockitoAnnotations.initMocks(this)
107 
108         communalRepository = kosmos.fakeCommunalSceneRepository
109 
110         ambientTouchComponentFactory =
111             object : AmbientTouchComponent.Factory {
112                 override fun create(
113                     lifecycleOwner: LifecycleOwner,
114                     touchHandlers: Set<TouchHandler>
115                 ): AmbientTouchComponent =
116                     object : AmbientTouchComponent {
117                         override fun getTouchMonitor(): TouchMonitor = touchMonitor
118                     }
119             }
120 
121         with(kosmos) {
122             underTest =
123                 GlanceableHubContainerController(
124                     communalInteractor,
125                     communalViewModel,
126                     keyguardInteractor,
127                     shadeInteractor,
128                     powerManager,
129                     communalColors,
130                     ambientTouchComponentFactory,
131                     communalContent,
132                     kosmos.sceneDataSourceDelegator,
133                     kosmos.notificationStackScrollLayoutController
134                 )
135         }
136         testableLooper = TestableLooper.get(this)
137 
138         overrideResource(R.dimen.communal_right_edge_swipe_region_width, RIGHT_SWIPE_REGION_WIDTH)
139         overrideResource(R.dimen.communal_top_edge_swipe_region_height, TOP_SWIPE_REGION_WIDTH)
140         overrideResource(
141             R.dimen.communal_bottom_edge_swipe_region_height,
142             BOTTOM_SWIPE_REGION_WIDTH
143         )
144 
145         // Make communal available so that communalInteractor.desiredScene accurately reflects
146         // scene changes instead of just returning Blank.
147         mSetFlagsRule.enableFlags(Flags.FLAG_COMMUNAL_HUB)
148         with(kosmos.testScope) {
149             launch { kosmos.setCommunalAvailable(true) }
150             testScheduler.runCurrent()
151         }
152 
153         initAndAttachContainerView()
154     }
155 
156     @After
tearDownnull157     fun tearDown() {
158         ViewUtils.detachView(parentView)
159     }
160 
161     @Test
initView_calledTwice_throwsExceptionnull162     fun initView_calledTwice_throwsException() =
163         with(kosmos) {
164             testScope.runTest {
165                 underTest =
166                     GlanceableHubContainerController(
167                         communalInteractor,
168                         communalViewModel,
169                         keyguardInteractor,
170                         shadeInteractor,
171                         powerManager,
172                         communalColors,
173                         ambientTouchComponentFactory,
174                         communalContent,
175                         kosmos.sceneDataSourceDelegator,
176                         kosmos.notificationStackScrollLayoutController
177                     )
178 
179                 // First call succeeds.
180                 underTest.initView(context)
181 
182                 // Second call throws.
183                 assertThrows(RuntimeException::class.java) { underTest.initView(context) }
184             }
185         }
186 
187     @DisableFlags(FLAG_GLANCEABLE_HUB_FULLSCREEN_SWIPE)
188     @Test
onTouchEvent_communalClosed_doesNotInterceptnull189     fun onTouchEvent_communalClosed_doesNotIntercept() =
190         with(kosmos) {
191             testScope.runTest {
192                 // Communal is closed.
193                 goToScene(CommunalScenes.Blank)
194 
195                 assertThat(underTest.onTouchEvent(DOWN_EVENT)).isFalse()
196             }
197         }
198 
199     @DisableFlags(FLAG_GLANCEABLE_HUB_FULLSCREEN_SWIPE)
200     @Test
onTouchEvent_openGesture_interceptsTouchesnull201     fun onTouchEvent_openGesture_interceptsTouches() =
202         with(kosmos) {
203             testScope.runTest {
204                 // Communal is closed.
205                 goToScene(CommunalScenes.Blank)
206 
207                 // Initial touch down is intercepted, and so are touches outside of the region,
208                 // until an
209                 // up event is received.
210                 assertThat(underTest.onTouchEvent(DOWN_IN_RIGHT_SWIPE_REGION_EVENT)).isTrue()
211                 assertThat(underTest.onTouchEvent(MOVE_EVENT)).isTrue()
212                 assertThat(underTest.onTouchEvent(UP_EVENT)).isTrue()
213                 assertThat(underTest.onTouchEvent(MOVE_EVENT)).isFalse()
214             }
215         }
216 
217     @DisableFlags(FLAG_GLANCEABLE_HUB_FULLSCREEN_SWIPE)
218     @Test
onTouchEvent_communalTransitioning_interceptsTouchesnull219     fun onTouchEvent_communalTransitioning_interceptsTouches() =
220         with(kosmos) {
221             testScope.runTest {
222                 // Communal is opening.
223                 communalRepository.setTransitionState(
224                     flowOf(
225                         ObservableTransitionState.Transition(
226                             fromScene = CommunalScenes.Blank,
227                             toScene = CommunalScenes.Communal,
228                             currentScene = flowOf(CommunalScenes.Blank),
229                             progress = flowOf(0.5f),
230                             isInitiatedByUserInput = true,
231                             isUserInputOngoing = flowOf(true)
232                         )
233                     )
234                 )
235                 testableLooper.processAllMessages()
236 
237                 // Touch events are intercepted.
238                 assertThat(underTest.onTouchEvent(DOWN_EVENT)).isTrue()
239                 // User activity sent to PowerManager.
240                 verify(powerManager).userActivity(any(), any(), any())
241             }
242         }
243 
244     @DisableFlags(FLAG_GLANCEABLE_HUB_FULLSCREEN_SWIPE)
245     @Test
onTouchEvent_communalOpen_interceptsTouchesnull246     fun onTouchEvent_communalOpen_interceptsTouches() =
247         with(kosmos) {
248             testScope.runTest {
249                 // Communal is open.
250                 goToScene(CommunalScenes.Communal)
251 
252                 // Touch events are intercepted.
253                 assertThat(underTest.onTouchEvent(DOWN_EVENT)).isTrue()
254                 // User activity sent to PowerManager.
255                 verify(powerManager).userActivity(any(), any(), any())
256             }
257         }
258 
259     @DisableFlags(FLAG_GLANCEABLE_HUB_FULLSCREEN_SWIPE)
260     @Test
onTouchEvent_communalAndBouncerShowing_doesNotInterceptnull261     fun onTouchEvent_communalAndBouncerShowing_doesNotIntercept() =
262         with(kosmos) {
263             testScope.runTest {
264                 // Communal is open.
265                 goToScene(CommunalScenes.Communal)
266 
267                 // Bouncer is visible.
268                 fakeKeyguardBouncerRepository.setPrimaryShow(true)
269                 testableLooper.processAllMessages()
270 
271                 // Touch events are not intercepted.
272                 assertThat(underTest.onTouchEvent(DOWN_EVENT)).isFalse()
273                 // User activity is not sent to PowerManager.
274                 verify(powerManager, times(0)).userActivity(any(), any(), any())
275             }
276         }
277 
278     @DisableFlags(FLAG_GLANCEABLE_HUB_FULLSCREEN_SWIPE)
279     @Test
onTouchEvent_communalAndShadeShowing_doesNotInterceptnull280     fun onTouchEvent_communalAndShadeShowing_doesNotIntercept() =
281         with(kosmos) {
282             testScope.runTest {
283                 // Communal is open.
284                 goToScene(CommunalScenes.Communal)
285 
286                 // Shade shows up.
287                 shadeTestUtil.setQsExpansion(1.0f)
288                 testableLooper.processAllMessages()
289 
290                 // Touch events are not intercepted.
291                 assertThat(underTest.onTouchEvent(DOWN_EVENT)).isFalse()
292             }
293         }
294 
295     @DisableFlags(FLAG_GLANCEABLE_HUB_FULLSCREEN_SWIPE)
296     @Test
onTouchEvent_containerViewDisposed_doesNotInterceptnull297     fun onTouchEvent_containerViewDisposed_doesNotIntercept() =
298         with(kosmos) {
299             testScope.runTest {
300                 // Communal is open.
301                 goToScene(CommunalScenes.Communal)
302 
303                 // Touch events are intercepted.
304                 assertThat(underTest.onTouchEvent(DOWN_EVENT)).isTrue()
305 
306                 // Container view disposed.
307                 underTest.disposeView()
308 
309                 // Touch events are not intercepted.
310                 assertThat(underTest.onTouchEvent(DOWN_EVENT)).isFalse()
311             }
312         }
313 
314     @Test
lifecycle_initializedAfterConstructionnull315     fun lifecycle_initializedAfterConstruction() =
316         with(kosmos) {
317             val underTest =
318                 GlanceableHubContainerController(
319                     communalInteractor,
320                     communalViewModel,
321                     keyguardInteractor,
322                     shadeInteractor,
323                     powerManager,
324                     communalColors,
325                     ambientTouchComponentFactory,
326                     communalContent,
327                     kosmos.sceneDataSourceDelegator,
328                     kosmos.notificationStackScrollLayoutController,
329                 )
330 
331             assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.INITIALIZED)
332         }
333 
334     @Test
lifecycle_createdAfterViewCreatednull335     fun lifecycle_createdAfterViewCreated() =
336         with(kosmos) {
337             val underTest =
338                 GlanceableHubContainerController(
339                     communalInteractor,
340                     communalViewModel,
341                     keyguardInteractor,
342                     shadeInteractor,
343                     powerManager,
344                     communalColors,
345                     ambientTouchComponentFactory,
346                     communalContent,
347                     kosmos.sceneDataSourceDelegator,
348                     kosmos.notificationStackScrollLayoutController,
349                 )
350 
351             // Only initView without attaching a view as we don't want the flows to start collecting
352             // yet.
353             underTest.initView(View(context))
354 
355             assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.CREATED)
356         }
357 
358     @Test
lifecycle_startedAfterFlowsUpdatenull359     fun lifecycle_startedAfterFlowsUpdate() {
360         // Flows start collecting due to test setup, causing the state to advance to STARTED.
361         assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
362     }
363 
364     @Test
lifecycle_resumedAfterCommunalShowsnull365     fun lifecycle_resumedAfterCommunalShows() {
366         // Communal is open.
367         goToScene(CommunalScenes.Communal)
368 
369         assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
370     }
371 
372     @Test
lifecycle_startedAfterCommunalClosesnull373     fun lifecycle_startedAfterCommunalCloses() =
374         with(kosmos) {
375             testScope.runTest {
376                 // Communal is open.
377                 goToScene(CommunalScenes.Communal)
378 
379                 assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
380 
381                 // Communal closes.
382                 goToScene(CommunalScenes.Blank)
383 
384                 assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
385             }
386         }
387 
388     @Test
lifecycle_startedAfterPrimaryBouncerShowsnull389     fun lifecycle_startedAfterPrimaryBouncerShows() =
390         with(kosmos) {
391             testScope.runTest {
392                 // Communal is open.
393                 goToScene(CommunalScenes.Communal)
394 
395                 // Bouncer is visible.
396                 fakeKeyguardBouncerRepository.setPrimaryShow(true)
397                 testableLooper.processAllMessages()
398 
399                 assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
400             }
401         }
402 
403     @Test
lifecycle_startedAfterAlternateBouncerShowsnull404     fun lifecycle_startedAfterAlternateBouncerShows() =
405         with(kosmos) {
406             testScope.runTest {
407                 // Communal is open.
408                 goToScene(CommunalScenes.Communal)
409 
410                 // Bouncer is visible.
411                 fakeKeyguardBouncerRepository.setAlternateVisible(true)
412                 testableLooper.processAllMessages()
413 
414                 assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
415             }
416         }
417 
418     @Test
lifecycle_createdAfterDisposeViewnull419     fun lifecycle_createdAfterDisposeView() {
420         // Container view disposed.
421         underTest.disposeView()
422 
423         assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.CREATED)
424     }
425 
426     @Test
lifecycle_startedAfterShadeShowsnull427     fun lifecycle_startedAfterShadeShows() =
428         with(kosmos) {
429             testScope.runTest {
430                 // Communal is open.
431                 goToScene(CommunalScenes.Communal)
432 
433                 // Shade shows up.
434                 shadeTestUtil.setQsExpansion(1.0f)
435                 testableLooper.processAllMessages()
436 
437                 assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
438             }
439         }
440 
441     @Test
editMode_communalAvailablenull442     fun editMode_communalAvailable() =
443         with(kosmos) {
444             testScope.runTest {
445                 val available by collectLastValue(underTest.communalAvailable())
446                 setCommunalAvailable(false)
447 
448                 assertThat(available).isFalse()
449                 communalInteractor.setEditModeOpen(true)
450                 assertThat(available).isTrue()
451             }
452         }
453 
454     @Test
gestureExclusionZone_setAfterInitnull455     fun gestureExclusionZone_setAfterInit() =
456         with(kosmos) {
457             testScope.runTest {
458                 goToScene(CommunalScenes.Communal)
459 
460                 assertThat(containerView.systemGestureExclusionRects)
461                     .containsExactly(
462                         Rect(
463                             /* left */ 0,
464                             /* top */ TOP_SWIPE_REGION_WIDTH,
465                             /* right */ CONTAINER_WIDTH,
466                             /* bottom */ CONTAINER_HEIGHT - BOTTOM_SWIPE_REGION_WIDTH
467                         )
468                     )
469             }
470         }
471 
472     @Test
gestureExclusionZone_unsetWhenShadeOpennull473     fun gestureExclusionZone_unsetWhenShadeOpen() =
474         with(kosmos) {
475             testScope.runTest {
476                 goToScene(CommunalScenes.Communal)
477 
478                 // Shade shows up.
479                 shadeTestUtil.setQsExpansion(1.0f)
480                 testableLooper.processAllMessages()
481 
482                 // Exclusion rects are unset.
483                 assertThat(containerView.systemGestureExclusionRects).isEmpty()
484             }
485         }
486 
487     @Test
gestureExclusionZone_unsetWhenBouncerOpennull488     fun gestureExclusionZone_unsetWhenBouncerOpen() =
489         with(kosmos) {
490             testScope.runTest {
491                 goToScene(CommunalScenes.Communal)
492 
493                 // Bouncer is visible.
494                 fakeKeyguardBouncerRepository.setPrimaryShow(true)
495                 testableLooper.processAllMessages()
496 
497                 // Exclusion rects are unset.
498                 assertThat(containerView.systemGestureExclusionRects).isEmpty()
499             }
500         }
501 
502     @Test
gestureExclusionZone_unsetWhenHubClosednull503     fun gestureExclusionZone_unsetWhenHubClosed() =
504         with(kosmos) {
505             testScope.runTest {
506                 goToScene(CommunalScenes.Communal)
507 
508                 // Exclusion rect is set.
509                 assertThat(containerView.systemGestureExclusionRects).hasSize(1)
510 
511                 // Leave the hub.
512                 goToScene(CommunalScenes.Blank)
513 
514                 // Exclusion rect is unset.
515                 assertThat(containerView.systemGestureExclusionRects).isEmpty()
516             }
517         }
518 
519     @Test
520     @EnableFlags(FLAG_GLANCEABLE_HUB_FULLSCREEN_SWIPE)
fullScreenSwipeGesture_doNotProcessTouchesInNotificationStacknull521     fun fullScreenSwipeGesture_doNotProcessTouchesInNotificationStack() =
522         with(kosmos) {
523             testScope.runTest {
524                 // Communal is closed.
525                 goToScene(CommunalScenes.Blank)
526                 `when`(
527                         notificationStackScrollLayoutController.isBelowLastNotification(
528                             anyFloat(),
529                             anyFloat()
530                         )
531                     )
532                     .thenReturn(false)
533                 assertThat(underTest.onTouchEvent(DOWN_EVENT)).isFalse()
534             }
535         }
536 
initAndAttachContainerViewnull537     private fun initAndAttachContainerView() {
538         containerView = View(context)
539 
540         parentView = FrameLayout(context)
541 
542         parentView.addView(underTest.initView(containerView))
543 
544         // Attach the view so that flows start collecting.
545         ViewUtils.attachView(parentView, CONTAINER_WIDTH, CONTAINER_HEIGHT)
546         // Attaching is async so processAllMessages is required for view.repeatWhenAttached to run.
547         testableLooper.processAllMessages()
548     }
549 
goToScenenull550     private fun goToScene(scene: SceneKey) {
551         communalRepository.changeScene(scene)
552         testableLooper.processAllMessages()
553     }
554 
555     companion object {
556         private const val CONTAINER_WIDTH = 100
557         private const val CONTAINER_HEIGHT = 100
558         private const val RIGHT_SWIPE_REGION_WIDTH = 20
559         private const val TOP_SWIPE_REGION_WIDTH = 12
560         private const val BOTTOM_SWIPE_REGION_WIDTH = 14
561 
562         /**
563          * A touch down event right in the middle of the screen, to avoid being in any of the swipe
564          * regions.
565          */
566         private val DOWN_EVENT =
567             MotionEvent.obtain(
568                 0L,
569                 0L,
570                 MotionEvent.ACTION_DOWN,
571                 CONTAINER_WIDTH.toFloat() / 2,
572                 CONTAINER_HEIGHT.toFloat() / 2,
573                 0
574             )
575         private val DOWN_IN_RIGHT_SWIPE_REGION_EVENT =
576             MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, CONTAINER_WIDTH.toFloat(), 0f, 0)
577         private val MOVE_EVENT = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_MOVE, 0f, 0f, 0)
578         private val UP_EVENT = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_UP, 0f, 0f, 0)
579     }
580 }
581