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.Insets
20 import android.graphics.Rect
21 import android.os.PowerManager
22 import android.platform.test.annotations.DisableFlags
23 import android.platform.test.annotations.EnableFlags
24 import android.testing.AndroidTestingRunner
25 import android.testing.TestableLooper
26 import android.testing.ViewUtils
27 import android.view.MotionEvent
28 import android.view.View
29 import android.view.WindowInsets
30 import android.widget.FrameLayout
31 import androidx.lifecycle.Lifecycle
32 import androidx.lifecycle.LifecycleOwner
33 import androidx.test.filters.SmallTest
34 import com.android.compose.animation.scene.SceneKey
35 import com.android.systemui.Flags
36 import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2
37 import com.android.systemui.Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX
38 import com.android.systemui.SysuiTestCase
39 import com.android.systemui.ambient.touch.TouchHandler
40 import com.android.systemui.ambient.touch.TouchMonitor
41 import com.android.systemui.ambient.touch.dagger.AmbientTouchComponent
42 import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository
43 import com.android.systemui.communal.data.repository.FakeCommunalSceneRepository
44 import com.android.systemui.communal.data.repository.fakeCommunalSceneRepository
45 import com.android.systemui.communal.domain.interactor.communalInteractor
46 import com.android.systemui.communal.domain.interactor.communalSettingsInteractor
47 import com.android.systemui.communal.domain.interactor.setCommunalAvailable
48 import com.android.systemui.communal.domain.interactor.setCommunalV2ConfigEnabled
49 import com.android.systemui.communal.shared.model.CommunalScenes
50 import com.android.systemui.communal.ui.compose.CommunalContent
51 import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
52 import com.android.systemui.communal.util.CommunalColors
53 import com.android.systemui.coroutines.collectLastValue
54 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
55 import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
56 import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
57 import com.android.systemui.keyguard.shared.model.KeyguardState
58 import com.android.systemui.keyguard.shared.model.TransitionState
59 import com.android.systemui.keyguard.shared.model.TransitionStep
60 import com.android.systemui.kosmos.Kosmos
61 import com.android.systemui.kosmos.testDispatcher
62 import com.android.systemui.kosmos.testScope
63 import com.android.systemui.log.logcatLogBuffer
64 import com.android.systemui.media.controls.controller.keyguardMediaController
65 import com.android.systemui.res.R
66 import com.android.systemui.scene.shared.model.sceneDataSourceDelegator
67 import com.android.systemui.shade.domain.interactor.shadeInteractor
68 import com.android.systemui.statusbar.lockscreen.lockscreenSmartspaceController
69 import com.android.systemui.statusbar.notification.stack.notificationStackScrollLayoutController
70 import com.android.systemui.testKosmos
71 import com.google.common.truth.Truth.assertThat
72 import kotlinx.coroutines.ExperimentalCoroutinesApi
73 import kotlinx.coroutines.launch
74 import kotlinx.coroutines.test.UnconfinedTestDispatcher
75 import kotlinx.coroutines.test.runTest
76 import org.junit.After
77 import org.junit.Assert.assertThrows
78 import org.junit.Before
79 import org.junit.Test
80 import org.junit.runner.RunWith
81 import org.mockito.kotlin.any
82 import org.mockito.kotlin.clearInvocations
83 import org.mockito.kotlin.doReturn
84 import org.mockito.kotlin.mock
85 import org.mockito.kotlin.never
86 import org.mockito.kotlin.spy
87 import org.mockito.kotlin.verify
88 import org.mockito.kotlin.whenever
89 
90 @ExperimentalCoroutinesApi
91 @RunWith(AndroidTestingRunner::class)
92 @TestableLooper.RunWithLooper(setAsMainLooper = true)
93 @SmallTest
94 class GlanceableHubContainerControllerTest : SysuiTestCase() {
95     private val kosmos: Kosmos =
<lambda>null96         testKosmos().apply {
97             // UnconfinedTestDispatcher makes testing simpler due to CommunalInteractor flows using
98             // SharedFlow
99             testDispatcher = UnconfinedTestDispatcher()
100         }
101 
102     private var communalViewModel = mock<CommunalViewModel>()
103     private var powerManager = mock<PowerManager>()
104     private var touchMonitor = mock<TouchMonitor>()
105     private var communalColors = mock<CommunalColors>()
106     private var communalContent = mock<CommunalContent>()
107     private lateinit var ambientTouchComponentFactory: AmbientTouchComponent.Factory
108 
109     private lateinit var parentView: FrameLayout
110     private lateinit var containerView: View
111     private lateinit var testableLooper: TestableLooper
112 
113     private lateinit var communalRepository: FakeCommunalSceneRepository
114     private lateinit var underTest: GlanceableHubContainerController
115 
116     @Before
setUpnull117     fun setUp() {
118         communalRepository = kosmos.fakeCommunalSceneRepository
119 
120         ambientTouchComponentFactory =
121             object : AmbientTouchComponent.Factory {
122                 override fun create(
123                     lifecycleOwner: LifecycleOwner,
124                     touchHandlers: Set<TouchHandler>,
125                     loggingName: String,
126                 ): AmbientTouchComponent =
127                     object : AmbientTouchComponent {
128                         override fun getTouchMonitor(): TouchMonitor = touchMonitor
129                     }
130             }
131 
132         with(kosmos) {
133             underTest =
134                 GlanceableHubContainerController(
135                     communalInteractor,
136                     communalSettingsInteractor,
137                     communalViewModel,
138                     keyguardInteractor,
139                     keyguardTransitionInteractor,
140                     shadeInteractor,
141                     powerManager,
142                     communalColors,
143                     ambientTouchComponentFactory,
144                     communalContent,
145                     sceneDataSourceDelegator,
146                     notificationStackScrollLayoutController,
147                     keyguardMediaController,
148                     lockscreenSmartspaceController,
149                     logcatLogBuffer("GlanceableHubContainerControllerTest"),
150                 )
151 
152             // Make below last notification true by default or else touches will be ignored by
153             // default when the hub is not showing.
154             whenever(notificationStackScrollLayoutController.isBelowLastNotification(any(), any()))
155                 .thenReturn(true)
156         }
157         testableLooper = TestableLooper.get(this)
158 
159         overrideResource(R.dimen.communal_right_edge_swipe_region_width, RIGHT_SWIPE_REGION_WIDTH)
160         overrideResource(R.dimen.communal_top_edge_swipe_region_height, TOP_SWIPE_REGION_WIDTH)
161         overrideResource(
162             R.dimen.communal_bottom_edge_swipe_region_height,
163             BOTTOM_SWIPE_REGION_WIDTH,
164         )
165 
166         // Make communal available so that communalInteractor.desiredScene accurately reflects
167         // scene changes instead of just returning Blank.
168         mSetFlagsRule.enableFlags(Flags.FLAG_COMMUNAL_HUB)
169         with(kosmos.testScope) {
170             launch { kosmos.setCommunalAvailable(true) }
171             testScheduler.runCurrent()
172         }
173 
174         initAndAttachContainerView()
175     }
176 
177     @After
tearDownnull178     fun tearDown() {
179         ViewUtils.detachView(parentView)
180     }
181 
182     @Test
initView_calledTwice_throwsExceptionnull183     fun initView_calledTwice_throwsException() =
184         with(kosmos) {
185             testScope.runTest {
186                 underTest =
187                     GlanceableHubContainerController(
188                         communalInteractor,
189                         kosmos.communalSettingsInteractor,
190                         communalViewModel,
191                         keyguardInteractor,
192                         kosmos.keyguardTransitionInteractor,
193                         shadeInteractor,
194                         powerManager,
195                         communalColors,
196                         ambientTouchComponentFactory,
197                         communalContent,
198                         kosmos.sceneDataSourceDelegator,
199                         kosmos.notificationStackScrollLayoutController,
200                         kosmos.keyguardMediaController,
201                         kosmos.lockscreenSmartspaceController,
202                         logcatLogBuffer("GlanceableHubContainerControllerTest"),
203                     )
204 
205                 // First call succeeds.
206                 underTest.initView(context)
207 
208                 // Second call throws.
209                 assertThrows(RuntimeException::class.java) { underTest.initView(context) }
210             }
211         }
212 
213     @Test
lifecycle_initializedAfterConstructionnull214     fun lifecycle_initializedAfterConstruction() =
215         with(kosmos) {
216             val underTest =
217                 GlanceableHubContainerController(
218                     communalInteractor,
219                     kosmos.communalSettingsInteractor,
220                     communalViewModel,
221                     keyguardInteractor,
222                     kosmos.keyguardTransitionInteractor,
223                     shadeInteractor,
224                     powerManager,
225                     communalColors,
226                     ambientTouchComponentFactory,
227                     communalContent,
228                     kosmos.sceneDataSourceDelegator,
229                     kosmos.notificationStackScrollLayoutController,
230                     kosmos.keyguardMediaController,
231                     kosmos.lockscreenSmartspaceController,
232                     logcatLogBuffer("GlanceableHubContainerControllerTest"),
233                 )
234 
235             assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.INITIALIZED)
236         }
237 
238     @Test
lifecycle_createdAfterViewCreatednull239     fun lifecycle_createdAfterViewCreated() =
240         with(kosmos) {
241             val underTest =
242                 GlanceableHubContainerController(
243                     communalInteractor,
244                     kosmos.communalSettingsInteractor,
245                     communalViewModel,
246                     keyguardInteractor,
247                     kosmos.keyguardTransitionInteractor,
248                     shadeInteractor,
249                     powerManager,
250                     communalColors,
251                     ambientTouchComponentFactory,
252                     communalContent,
253                     kosmos.sceneDataSourceDelegator,
254                     kosmos.notificationStackScrollLayoutController,
255                     kosmos.keyguardMediaController,
256                     kosmos.lockscreenSmartspaceController,
257                     logcatLogBuffer("GlanceableHubContainerControllerTest"),
258                 )
259 
260             // Only initView without attaching a view as we don't want the flows to start collecting
261             // yet.
262             underTest.initView(View(context))
263 
264             assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.CREATED)
265         }
266 
267     @Test
lifecycle_startedAfterFlowsUpdatenull268     fun lifecycle_startedAfterFlowsUpdate() {
269         // Flows start collecting due to test setup, causing the state to advance to STARTED.
270         assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
271     }
272 
273     @Test
lifecycle_resumedAfterCommunalShowsnull274     fun lifecycle_resumedAfterCommunalShows() =
275         with(kosmos) {
276             testScope.runTest {
277                 // Communal is open.
278                 goToScene(CommunalScenes.Communal)
279 
280                 assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
281             }
282         }
283 
284     @Test
lifecycle_startedAfterCommunalClosesnull285     fun lifecycle_startedAfterCommunalCloses() =
286         with(kosmos) {
287             testScope.runTest {
288                 // Communal is open.
289                 goToScene(CommunalScenes.Communal)
290 
291                 assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
292 
293                 // Communal closes.
294                 goToScene(CommunalScenes.Blank)
295 
296                 assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
297             }
298         }
299 
300     @Test
lifecycle_startedAfterPrimaryBouncerShowsnull301     fun lifecycle_startedAfterPrimaryBouncerShows() =
302         with(kosmos) {
303             testScope.runTest {
304                 // Communal is open.
305                 goToScene(CommunalScenes.Communal)
306 
307                 // Bouncer is visible.
308                 fakeKeyguardBouncerRepository.setPrimaryShow(true)
309                 testableLooper.processAllMessages()
310 
311                 assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
312             }
313         }
314 
315     @Test
lifecycle_startedAfterAlternateBouncerShowsnull316     fun lifecycle_startedAfterAlternateBouncerShows() =
317         with(kosmos) {
318             testScope.runTest {
319                 // Communal is open.
320                 goToScene(CommunalScenes.Communal)
321 
322                 // Bouncer is visible.
323                 fakeKeyguardBouncerRepository.setAlternateVisible(true)
324                 testableLooper.processAllMessages()
325 
326                 assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
327             }
328         }
329 
330     @Test
lifecycle_startedWhenEditActivityShowingnull331     fun lifecycle_startedWhenEditActivityShowing() =
332         with(kosmos) {
333             testScope.runTest {
334                 // Communal is open.
335                 goToScene(CommunalScenes.Communal)
336 
337                 // Edit activity is showing.
338                 communalInteractor.setEditActivityShowing(true)
339                 testableLooper.processAllMessages()
340 
341                 assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
342             }
343         }
344 
345     @Test
lifecycle_startedWhenEditModeTransitionStartednull346     fun lifecycle_startedWhenEditModeTransitionStarted() =
347         with(kosmos) {
348             testScope.runTest {
349                 // Communal is open.
350                 goToScene(CommunalScenes.Communal)
351 
352                 // Leaving edit mode to return to the hub.
353                 fakeKeyguardTransitionRepository.sendTransitionStep(
354                     TransitionStep(
355                         from = KeyguardState.GONE,
356                         to = KeyguardState.GLANCEABLE_HUB,
357                         value = 1.0f,
358                         transitionState = TransitionState.RUNNING,
359                     )
360                 )
361                 testableLooper.processAllMessages()
362 
363                 assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
364             }
365         }
366 
367     @Test
lifecycle_createdAfterDisposeViewnull368     fun lifecycle_createdAfterDisposeView() {
369         // Container view disposed.
370         underTest.disposeView()
371 
372         assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.CREATED)
373     }
374 
375     @Test
lifecycle_startedAfterShadeShowsnull376     fun lifecycle_startedAfterShadeShows() =
377         with(kosmos) {
378             testScope.runTest {
379                 // Communal is open.
380                 goToScene(CommunalScenes.Communal)
381 
382                 // Shade shows up.
383                 shadeTestUtil.setQsExpansion(1.0f)
384                 testableLooper.processAllMessages()
385 
386                 assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
387             }
388         }
389 
390     @Test
lifecycle_doesNotResumeOnUserInteractivityOnceExpandednull391     fun lifecycle_doesNotResumeOnUserInteractivityOnceExpanded() =
392         with(kosmos) {
393             testScope.runTest {
394                 // Communal is open.
395                 goToScene(CommunalScenes.Communal)
396 
397                 // Shade shows up.
398                 shadeTestUtil.setShadeExpansion(1.0f)
399                 testableLooper.processAllMessages()
400                 underTest.onTouchEvent(DOWN_EVENT)
401                 testableLooper.processAllMessages()
402 
403                 assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
404 
405                 // Shade starts collapsing.
406                 shadeTestUtil.setShadeExpansion(.5f)
407                 testableLooper.processAllMessages()
408                 underTest.onTouchEvent(DOWN_EVENT)
409                 testableLooper.processAllMessages()
410 
411                 assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
412 
413                 // Shade fully collpase, and then expand should with touch interaction should now
414                 // be resumed.
415                 shadeTestUtil.setShadeExpansion(0f)
416                 testableLooper.processAllMessages()
417                 shadeTestUtil.setShadeExpansion(.5f)
418                 testableLooper.processAllMessages()
419                 underTest.onTouchEvent(DOWN_EVENT)
420                 testableLooper.processAllMessages()
421 
422                 assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
423             }
424         }
425 
426     @Test
touchHandling_moveEventProcessedAfterCancelnull427     fun touchHandling_moveEventProcessedAfterCancel() =
428         with(kosmos) {
429             testScope.runTest {
430                 // Communal is open.
431                 goToScene(CommunalScenes.Communal)
432 
433                 // Touch starts and ends.
434                 assertThat(underTest.onTouchEvent(DOWN_EVENT)).isTrue()
435                 assertThat(underTest.onTouchEvent(CANCEL_EVENT)).isTrue()
436 
437                 // Up event is no longer processed
438                 assertThat(underTest.onTouchEvent(UP_EVENT)).isFalse()
439 
440                 // Move event can still be processed
441                 assertThat(underTest.onTouchEvent(MOVE_EVENT)).isTrue()
442                 assertThat(underTest.onTouchEvent(MOVE_EVENT)).isTrue()
443                 assertThat(underTest.onTouchEvent(UP_EVENT)).isTrue()
444             }
445         }
446 
447     @Test
editMode_communalAvailablenull448     fun editMode_communalAvailable() =
449         with(kosmos) {
450             testScope.runTest {
451                 val available by collectLastValue(underTest.communalAvailable())
452                 setCommunalAvailable(false)
453 
454                 assertThat(available).isFalse()
455                 communalInteractor.setEditModeOpen(true)
456                 assertThat(available).isTrue()
457             }
458         }
459 
460     @Test
461     @DisableFlags(FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX)
gestureExclusionZone_setAfterInitnull462     fun gestureExclusionZone_setAfterInit() =
463         with(kosmos) {
464             testScope.runTest {
465                 goToScene(CommunalScenes.Communal)
466 
467                 assertThat(containerView.systemGestureExclusionRects)
468                     .containsExactly(
469                         Rect(
470                             /* left= */ 0,
471                             /* top= */ TOP_SWIPE_REGION_WIDTH,
472                             /* right= */ CONTAINER_WIDTH,
473                             /* bottom= */ CONTAINER_HEIGHT - BOTTOM_SWIPE_REGION_WIDTH,
474                         )
475                     )
476             }
477         }
478 
479     @Test
480     @EnableFlags(FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX)
gestureExclusionZone_setAfterInit_fullSwipenull481     fun gestureExclusionZone_setAfterInit_fullSwipe() =
482         with(kosmos) {
483             testScope.runTest {
484                 goToScene(CommunalScenes.Communal)
485 
486                 assertThat(containerView.systemGestureExclusionRects).isEmpty()
487             }
488         }
489 
490     @Test
491     @DisableFlags(FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX)
gestureExclusionZone_unsetWhenShadeOpennull492     fun gestureExclusionZone_unsetWhenShadeOpen() =
493         with(kosmos) {
494             testScope.runTest {
495                 goToScene(CommunalScenes.Communal)
496 
497                 // Exclusion rect is set.
498                 assertThat(containerView.systemGestureExclusionRects).isNotEmpty()
499 
500                 // Shade shows up.
501                 shadeTestUtil.setQsExpansion(1.0f)
502                 testableLooper.processAllMessages()
503 
504                 // Exclusion rects are unset.
505                 assertThat(containerView.systemGestureExclusionRects).isEmpty()
506             }
507         }
508 
509     @Test
510     @DisableFlags(FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX)
gestureExclusionZone_unsetWhenBouncerOpennull511     fun gestureExclusionZone_unsetWhenBouncerOpen() =
512         with(kosmos) {
513             testScope.runTest {
514                 goToScene(CommunalScenes.Communal)
515 
516                 // Exclusion rect is set.
517                 assertThat(containerView.systemGestureExclusionRects).isNotEmpty()
518 
519                 // Bouncer is visible.
520                 fakeKeyguardBouncerRepository.setPrimaryShow(true)
521                 testableLooper.processAllMessages()
522 
523                 // Exclusion rects are unset.
524                 assertThat(containerView.systemGestureExclusionRects).isEmpty()
525             }
526         }
527 
528     @Test
529     @DisableFlags(FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX)
gestureExclusionZone_unsetWhenHubClosednull530     fun gestureExclusionZone_unsetWhenHubClosed() =
531         with(kosmos) {
532             testScope.runTest {
533                 goToScene(CommunalScenes.Communal)
534 
535                 // Exclusion rect is set.
536                 assertThat(containerView.systemGestureExclusionRects).isNotEmpty()
537 
538                 // Leave the hub.
539                 goToScene(CommunalScenes.Blank)
540 
541                 // Exclusion rect is unset.
542                 assertThat(containerView.systemGestureExclusionRects).isEmpty()
543             }
544         }
545 
546     @Test
fullScreenSwipeGesture_doNotProcessTouchesInNotificationStacknull547     fun fullScreenSwipeGesture_doNotProcessTouchesInNotificationStack() =
548         with(kosmos) {
549             testScope.runTest {
550                 // Communal is closed.
551                 goToScene(CommunalScenes.Blank)
552                 whenever(
553                         notificationStackScrollLayoutController.isBelowLastNotification(
554                             any(),
555                             any(),
556                         )
557                     )
558                     .thenReturn(false)
559                 assertThat(underTest.onTouchEvent(DOWN_EVENT)).isFalse()
560             }
561         }
562 
563     @Test
fullScreenSwipeGesture_doNotProcessTouchesInUmonull564     fun fullScreenSwipeGesture_doNotProcessTouchesInUmo() =
565         with(kosmos) {
566             testScope.runTest {
567                 // Communal is closed.
568                 goToScene(CommunalScenes.Blank)
569                 whenever(keyguardMediaController.isWithinMediaViewBounds(any(), any()))
570                     .thenReturn(true)
571                 assertThat(underTest.onTouchEvent(DOWN_EVENT)).isFalse()
572             }
573         }
574 
575     @Test
fullScreenSwipeGesture_doNotProcessTouchesInSmartspacenull576     fun fullScreenSwipeGesture_doNotProcessTouchesInSmartspace() =
577         with(kosmos) {
578             testScope.runTest {
579                 // Communal is closed.
580                 goToScene(CommunalScenes.Blank)
581                 whenever(lockscreenSmartspaceController.isWithinSmartspaceBounds(any(), any()))
582                     .thenReturn(true)
583                 assertThat(underTest.onTouchEvent(DOWN_EVENT)).isFalse()
584             }
585         }
586 
587     @Test
onTouchEvent_hubOpen_touchesDispatchednull588     fun onTouchEvent_hubOpen_touchesDispatched() =
589         with(kosmos) {
590             testScope.runTest {
591                 // Communal is open.
592                 goToScene(CommunalScenes.Communal)
593 
594                 // Touch event is sent to the container view.
595                 assertThat(underTest.onTouchEvent(DOWN_EVENT)).isTrue()
596                 verify(containerView).onTouchEvent(DOWN_EVENT)
597                 assertThat(underTest.onTouchEvent(UP_EVENT)).isTrue()
598                 verify(containerView).onTouchEvent(UP_EVENT)
599             }
600         }
601 
602     @Test
onTouchEvent_editActivityShowing_touchesConsumedButNotDispatchednull603     fun onTouchEvent_editActivityShowing_touchesConsumedButNotDispatched() =
604         with(kosmos) {
605             testScope.runTest {
606                 // Communal is open.
607                 goToScene(CommunalScenes.Communal)
608 
609                 // Transitioning to or from edit mode.
610                 communalInteractor.setEditActivityShowing(true)
611                 testableLooper.processAllMessages()
612 
613                 // onTouchEvent returns true to consume the touch, but it is not sent to the
614                 // container view.
615                 assertThat(underTest.onTouchEvent(DOWN_EVENT)).isTrue()
616                 verify(containerView, never()).onTouchEvent(any())
617             }
618         }
619 
620     @Test
onTouchEvent_editModeTransitionStarted_touchesConsumedButNotDispatchednull621     fun onTouchEvent_editModeTransitionStarted_touchesConsumedButNotDispatched() =
622         with(kosmos) {
623             testScope.runTest {
624                 // Communal is open.
625                 goToScene(CommunalScenes.Communal)
626 
627                 // Leaving edit mode to return to the hub.
628                 fakeKeyguardTransitionRepository.sendTransitionStep(
629                     TransitionStep(
630                         from = KeyguardState.GONE,
631                         to = KeyguardState.GLANCEABLE_HUB,
632                         value = 1.0f,
633                         transitionState = TransitionState.RUNNING,
634                     )
635                 )
636                 testableLooper.processAllMessages()
637 
638                 // onTouchEvent returns true to consume the touch, but it is not sent to the
639                 // container view.
640                 assertThat(underTest.onTouchEvent(DOWN_EVENT)).isTrue()
641                 verify(containerView, never()).onTouchEvent(any())
642             }
643         }
644 
645     @DisableFlags(FLAG_GLANCEABLE_HUB_V2)
646     @Test
onTouchEvent_shadeInteracting_movesNotDispatchednull647     fun onTouchEvent_shadeInteracting_movesNotDispatched() =
648         with(kosmos) {
649             testScope.runTest {
650                 // On lockscreen.
651                 goToScene(CommunalScenes.Blank)
652                 whenever(
653                         notificationStackScrollLayoutController.isBelowLastNotification(
654                             any(),
655                             any(),
656                         )
657                     )
658                     .thenReturn(true)
659 
660                 // Touches not consumed by default but are received by containerView.
661                 assertThat(underTest.onTouchEvent(DOWN_EVENT)).isFalse()
662                 verify(containerView).onTouchEvent(DOWN_EVENT)
663 
664                 // User is interacting with shade on lockscreen.
665                 shadeTestUtil.setLockscreenShadeTracking(true)
666                 testableLooper.processAllMessages()
667 
668                 // A move event is ignored while the user is already interacting.
669                 assertThat(underTest.onTouchEvent(MOVE_EVENT)).isFalse()
670                 verify(containerView, never()).onTouchEvent(MOVE_EVENT)
671 
672                 // An up event is still delivered.
673                 assertThat(underTest.onTouchEvent(UP_EVENT)).isFalse()
674                 verify(containerView).onTouchEvent(UP_EVENT)
675             }
676         }
677 
678     @Test
onTouchEvent_shadeExpanding_touchesNotDispatchednull679     fun onTouchEvent_shadeExpanding_touchesNotDispatched() =
680         with(kosmos) {
681             testScope.runTest {
682                 // On lockscreen.
683                 goToScene(CommunalScenes.Blank)
684                 whenever(
685                         notificationStackScrollLayoutController.isBelowLastNotification(
686                             any(),
687                             any(),
688                         )
689                     )
690                     .thenReturn(true)
691 
692                 // Shade is open slightly.
693                 shadeTestUtil.setShadeExpansion(0.01f)
694                 testableLooper.processAllMessages()
695 
696                 // Touches are not consumed.
697                 assertThat(underTest.onTouchEvent(DOWN_EVENT)).isFalse()
698                 verify(containerView, never()).onTouchEvent(DOWN_EVENT)
699             }
700         }
701 
702     @DisableFlags(FLAG_GLANCEABLE_HUB_V2)
703     @Test
onTouchEvent_bouncerInteracting_movesNotDispatchednull704     fun onTouchEvent_bouncerInteracting_movesNotDispatched() =
705         with(kosmos) {
706             testScope.runTest {
707                 // On lockscreen.
708                 goToScene(CommunalScenes.Blank)
709                 whenever(
710                         notificationStackScrollLayoutController.isBelowLastNotification(
711                             any(),
712                             any(),
713                         )
714                     )
715                     .thenReturn(true)
716 
717                 // Touches not consumed by default but are received by containerView.
718                 assertThat(underTest.onTouchEvent(DOWN_EVENT)).isFalse()
719                 verify(containerView).onTouchEvent(DOWN_EVENT)
720 
721                 // User is interacting with bouncer on lockscreen.
722                 fakeKeyguardBouncerRepository.setPrimaryShow(true)
723                 testableLooper.processAllMessages()
724 
725                 // A move event is ignored while the user is already interacting.
726                 assertThat(underTest.onTouchEvent(MOVE_EVENT)).isFalse()
727                 verify(containerView, never()).onTouchEvent(MOVE_EVENT)
728 
729                 // An up event is still delivered.
730                 assertThat(underTest.onTouchEvent(UP_EVENT)).isFalse()
731                 verify(containerView).onTouchEvent(UP_EVENT)
732             }
733         }
734 
735     @EnableFlags(FLAG_GLANCEABLE_HUB_V2)
736     @Test
onTouchEvent_onLockscreenAndGlanceableHubV2_touchIgnorednull737     fun onTouchEvent_onLockscreenAndGlanceableHubV2_touchIgnored() =
738         with(kosmos) {
739             testScope.runTest {
740                 kosmos.setCommunalV2ConfigEnabled(true)
741 
742                 // On lockscreen.
743                 goToScene(CommunalScenes.Blank)
744 
745                 assertThat(underTest.onTouchEvent(DOWN_EVENT)).isFalse()
746                 verify(containerView, never()).onTouchEvent(DOWN_EVENT)
747             }
748         }
749 
750     @Test
disposeView_destroysTouchMonitornull751     fun disposeView_destroysTouchMonitor() {
752         clearInvocations(touchMonitor)
753 
754         underTest.disposeView()
755 
756         verify(touchMonitor).destroy()
757     }
758 
initAndAttachContainerViewnull759     private fun initAndAttachContainerView() {
760         val mockInsets =
761             mock<WindowInsets> {
762                 on { getInsets(WindowInsets.Type.systemGestures()) } doReturn FAKE_INSETS
763             }
764 
765         containerView =
766             spy(View(context)) {
767                 on { rootWindowInsets } doReturn mockInsets
768                 // Return true to handle touch events or else further events in the gesture will not
769                 // be received as we are using real View objects.
770                 onGeneric { onTouchEvent(any()) } doReturn true
771             }
772 
773         parentView = FrameLayout(context)
774 
775         parentView.addView(underTest.initView(containerView))
776 
777         // Attach the view so that flows start collecting.
778         ViewUtils.attachView(parentView, CONTAINER_WIDTH, CONTAINER_HEIGHT)
779         // Attaching is async so processAllMessages is required for view.repeatWhenAttached to run.
780         testableLooper.processAllMessages()
781     }
782 
goToScenenull783     private suspend fun goToScene(scene: SceneKey) {
784         communalRepository.changeScene(scene)
785         if (scene == CommunalScenes.Communal) {
786             kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps(
787                 from = KeyguardState.LOCKSCREEN,
788                 to = KeyguardState.GLANCEABLE_HUB,
789                 kosmos.testScope,
790             )
791         } else {
792             kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps(
793                 from = KeyguardState.GLANCEABLE_HUB,
794                 to = KeyguardState.LOCKSCREEN,
795                 kosmos.testScope,
796             )
797         }
798         testableLooper.processAllMessages()
799     }
800 
801     companion object {
802         private const val CONTAINER_WIDTH = 100
803         private const val CONTAINER_HEIGHT = 100
804         private const val RIGHT_SWIPE_REGION_WIDTH = 20
805         private const val TOP_SWIPE_REGION_WIDTH = 12
806         private const val BOTTOM_SWIPE_REGION_WIDTH = 14
807         private val FAKE_INSETS = Insets.of(10, 20, 30, 50)
808 
809         /**
810          * A touch down event right in the middle of the screen, to avoid being in any of the swipe
811          * regions.
812          */
813         private val DOWN_EVENT =
814             MotionEvent.obtain(
815                 0L,
816                 0L,
817                 MotionEvent.ACTION_DOWN,
818                 CONTAINER_WIDTH.toFloat() / 2,
819                 CONTAINER_HEIGHT.toFloat() / 2,
820                 0,
821             )
822 
823         private val CANCEL_EVENT =
824             MotionEvent.obtain(
825                 0L,
826                 0L,
827                 MotionEvent.ACTION_CANCEL,
828                 CONTAINER_WIDTH.toFloat() / 2,
829                 CONTAINER_HEIGHT.toFloat() / 2,
830                 0,
831             )
832 
833         private val MOVE_EVENT =
834             MotionEvent.obtain(
835                 0L,
836                 0L,
837                 MotionEvent.ACTION_MOVE,
838                 CONTAINER_WIDTH.toFloat() / 2,
839                 CONTAINER_HEIGHT.toFloat() / 2,
840                 0,
841             )
842 
843         private val UP_EVENT =
844             MotionEvent.obtain(
845                 0L,
846                 0L,
847                 MotionEvent.ACTION_UP,
848                 CONTAINER_WIDTH.toFloat() / 2,
849                 CONTAINER_HEIGHT.toFloat() / 2,
850                 0,
851             )
852     }
853 }
854