Loading packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt +34 −3 Original line number Diff line number Diff line Loading @@ -181,6 +181,16 @@ constructor( */ private var shadeShowingAndConsumingTouches = false /** * True anytime the shade is processing user touches, regardless of expansion state. * * Based on [ShadeInteractor.isUserInteracting]. */ private var shadeConsumingTouches = false /** True if the keyguard transition state is finished on [KeyguardState.LOCKSCREEN]. */ private var onLockscreen = false /** * True if the shade ever fully expands and the user isn't interacting with it (aka finger on * screen dragging). In this case, the shade should handle all touch events until it has fully Loading Loading @@ -336,6 +346,11 @@ constructor( updateTouchHandlingState() } ) collectFlow( containerView, keyguardTransitionInteractor.isFinishedIn(KeyguardState.LOCKSCREEN), { onLockscreen = it } ) collectFlow( containerView, communalInteractor.isCommunalVisible, Loading Loading @@ -369,6 +384,7 @@ constructor( ::Triple ), { (isFullyExpanded, isUserInteracting, isShadeFullyCollapsed) -> shadeConsumingTouches = isUserInteracting val expandedAndNotInteractive = isFullyExpanded && !isUserInteracting // If we ever are fully expanded and not interacting, capture this state as we Loading Loading @@ -497,12 +513,27 @@ constructor( return true } try { // On the lock screen, our touch handlers are not active and we rely on the NSWVC's // touch handling for gestures on blank areas, which can go up to show the bouncer or // down to show the notification shade. We see the touches first and they are not // consumed and cancelled like on the dream or hub so we have to gracefully ignore them // if the shade or bouncer are handling them. This issue only applies to touches on the // keyguard itself, once the bouncer or shade are fully open, our logic stops us from // taking touches. val touchTaken = onLockscreen && (shadeConsumingTouches || anyBouncerShowing) // Only dispatch touches to communal if not already handled or the touch is ending, // meaning the event is an up or cancel. This is necessary as the hub always receives at // least the initial down even if the shade or bouncer end up handling the touch. val dispatchToCommunal = !touchTaken || !isTrackingHubTouch var handled = false if (dispatchToCommunal) { communalContainerWrapper?.dispatchTouchEvent(ev) { if (it) { handled = true } } } return handled || hubShowing } finally { powerManager.userActivity( Loading packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt +75 −2 Original line number Diff line number Diff line Loading @@ -61,6 +61,7 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.media.controls.controller.keyguardMediaController import com.android.systemui.res.R import com.android.systemui.scene.shared.model.sceneDataSourceDelegator import com.android.systemui.shade.data.repository.fakeShadeRepository import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.statusbar.lockscreen.lockscreenSmartspaceController import com.android.systemui.statusbar.notification.stack.notificationStackScrollLayoutController Loading Loading @@ -727,7 +728,9 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { // Touch event is sent to the container view. assertThat(underTest.onTouchEvent(DOWN_EVENT)).isTrue() verify(containerView).onTouchEvent(any()) verify(containerView).onTouchEvent(DOWN_EVENT) assertThat(underTest.onTouchEvent(UP_EVENT)).isTrue() verify(containerView).onTouchEvent(UP_EVENT) } } Loading Loading @@ -774,13 +777,83 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { } } @Test fun onTouchEvent_shadeInteracting_movesNotDispatched() = with(kosmos) { testScope.runTest { // On lockscreen. goToScene(CommunalScenes.Blank) whenever( notificationStackScrollLayoutController.isBelowLastNotification( any(), any() ) ) .thenReturn(true) // Touches not consumed by default but are received by containerView. assertThat(underTest.onTouchEvent(DOWN_EVENT)).isFalse() verify(containerView).onTouchEvent(DOWN_EVENT) // User is interacting with shade on lockscreen. fakeShadeRepository.setLegacyLockscreenShadeTracking(true) testableLooper.processAllMessages() // A move event is ignored while the user is already interacting. assertThat(underTest.onTouchEvent(MOVE_EVENT)).isFalse() verify(containerView, never()).onTouchEvent(MOVE_EVENT) // An up event is still delivered. assertThat(underTest.onTouchEvent(UP_EVENT)).isFalse() verify(containerView).onTouchEvent(UP_EVENT) } } @Test fun onTouchEvent_bouncerInteracting_movesNotDispatched() = with(kosmos) { testScope.runTest { // On lockscreen. goToScene(CommunalScenes.Blank) whenever( notificationStackScrollLayoutController.isBelowLastNotification( any(), any() ) ) .thenReturn(true) // Touches not consumed by default but are received by containerView. assertThat(underTest.onTouchEvent(DOWN_EVENT)).isFalse() verify(containerView).onTouchEvent(DOWN_EVENT) // User is interacting with bouncer on lockscreen. fakeKeyguardBouncerRepository.setPrimaryShow(true) testableLooper.processAllMessages() // A move event is ignored while the user is already interacting. assertThat(underTest.onTouchEvent(MOVE_EVENT)).isFalse() verify(containerView, never()).onTouchEvent(MOVE_EVENT) // An up event is still delivered. assertThat(underTest.onTouchEvent(UP_EVENT)).isFalse() verify(containerView).onTouchEvent(UP_EVENT) } } private fun initAndAttachContainerView() { val mockInsets = mock<WindowInsets> { on { getInsets(WindowInsets.Type.systemGestures()) } doReturn FAKE_INSETS } containerView = spy(View(context)) { on { rootWindowInsets } doReturn mockInsets } containerView = spy(View(context)) { on { rootWindowInsets } doReturn mockInsets // Return true to handle touch events or else further events in the gesture will not // be received as we are using real View objects. onGeneric { onTouchEvent(any()) } doReturn true } parentView = FrameLayout(context) Loading Loading
packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt +34 −3 Original line number Diff line number Diff line Loading @@ -181,6 +181,16 @@ constructor( */ private var shadeShowingAndConsumingTouches = false /** * True anytime the shade is processing user touches, regardless of expansion state. * * Based on [ShadeInteractor.isUserInteracting]. */ private var shadeConsumingTouches = false /** True if the keyguard transition state is finished on [KeyguardState.LOCKSCREEN]. */ private var onLockscreen = false /** * True if the shade ever fully expands and the user isn't interacting with it (aka finger on * screen dragging). In this case, the shade should handle all touch events until it has fully Loading Loading @@ -336,6 +346,11 @@ constructor( updateTouchHandlingState() } ) collectFlow( containerView, keyguardTransitionInteractor.isFinishedIn(KeyguardState.LOCKSCREEN), { onLockscreen = it } ) collectFlow( containerView, communalInteractor.isCommunalVisible, Loading Loading @@ -369,6 +384,7 @@ constructor( ::Triple ), { (isFullyExpanded, isUserInteracting, isShadeFullyCollapsed) -> shadeConsumingTouches = isUserInteracting val expandedAndNotInteractive = isFullyExpanded && !isUserInteracting // If we ever are fully expanded and not interacting, capture this state as we Loading Loading @@ -497,12 +513,27 @@ constructor( return true } try { // On the lock screen, our touch handlers are not active and we rely on the NSWVC's // touch handling for gestures on blank areas, which can go up to show the bouncer or // down to show the notification shade. We see the touches first and they are not // consumed and cancelled like on the dream or hub so we have to gracefully ignore them // if the shade or bouncer are handling them. This issue only applies to touches on the // keyguard itself, once the bouncer or shade are fully open, our logic stops us from // taking touches. val touchTaken = onLockscreen && (shadeConsumingTouches || anyBouncerShowing) // Only dispatch touches to communal if not already handled or the touch is ending, // meaning the event is an up or cancel. This is necessary as the hub always receives at // least the initial down even if the shade or bouncer end up handling the touch. val dispatchToCommunal = !touchTaken || !isTrackingHubTouch var handled = false if (dispatchToCommunal) { communalContainerWrapper?.dispatchTouchEvent(ev) { if (it) { handled = true } } } return handled || hubShowing } finally { powerManager.userActivity( Loading
packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt +75 −2 Original line number Diff line number Diff line Loading @@ -61,6 +61,7 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.media.controls.controller.keyguardMediaController import com.android.systemui.res.R import com.android.systemui.scene.shared.model.sceneDataSourceDelegator import com.android.systemui.shade.data.repository.fakeShadeRepository import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.statusbar.lockscreen.lockscreenSmartspaceController import com.android.systemui.statusbar.notification.stack.notificationStackScrollLayoutController Loading Loading @@ -727,7 +728,9 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { // Touch event is sent to the container view. assertThat(underTest.onTouchEvent(DOWN_EVENT)).isTrue() verify(containerView).onTouchEvent(any()) verify(containerView).onTouchEvent(DOWN_EVENT) assertThat(underTest.onTouchEvent(UP_EVENT)).isTrue() verify(containerView).onTouchEvent(UP_EVENT) } } Loading Loading @@ -774,13 +777,83 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { } } @Test fun onTouchEvent_shadeInteracting_movesNotDispatched() = with(kosmos) { testScope.runTest { // On lockscreen. goToScene(CommunalScenes.Blank) whenever( notificationStackScrollLayoutController.isBelowLastNotification( any(), any() ) ) .thenReturn(true) // Touches not consumed by default but are received by containerView. assertThat(underTest.onTouchEvent(DOWN_EVENT)).isFalse() verify(containerView).onTouchEvent(DOWN_EVENT) // User is interacting with shade on lockscreen. fakeShadeRepository.setLegacyLockscreenShadeTracking(true) testableLooper.processAllMessages() // A move event is ignored while the user is already interacting. assertThat(underTest.onTouchEvent(MOVE_EVENT)).isFalse() verify(containerView, never()).onTouchEvent(MOVE_EVENT) // An up event is still delivered. assertThat(underTest.onTouchEvent(UP_EVENT)).isFalse() verify(containerView).onTouchEvent(UP_EVENT) } } @Test fun onTouchEvent_bouncerInteracting_movesNotDispatched() = with(kosmos) { testScope.runTest { // On lockscreen. goToScene(CommunalScenes.Blank) whenever( notificationStackScrollLayoutController.isBelowLastNotification( any(), any() ) ) .thenReturn(true) // Touches not consumed by default but are received by containerView. assertThat(underTest.onTouchEvent(DOWN_EVENT)).isFalse() verify(containerView).onTouchEvent(DOWN_EVENT) // User is interacting with bouncer on lockscreen. fakeKeyguardBouncerRepository.setPrimaryShow(true) testableLooper.processAllMessages() // A move event is ignored while the user is already interacting. assertThat(underTest.onTouchEvent(MOVE_EVENT)).isFalse() verify(containerView, never()).onTouchEvent(MOVE_EVENT) // An up event is still delivered. assertThat(underTest.onTouchEvent(UP_EVENT)).isFalse() verify(containerView).onTouchEvent(UP_EVENT) } } private fun initAndAttachContainerView() { val mockInsets = mock<WindowInsets> { on { getInsets(WindowInsets.Type.systemGestures()) } doReturn FAKE_INSETS } containerView = spy(View(context)) { on { rootWindowInsets } doReturn mockInsets } containerView = spy(View(context)) { on { rootWindowInsets } doReturn mockInsets // Return true to handle touch events or else further events in the gesture will not // be received as we are using real View objects. onGeneric { onTouchEvent(any()) } doReturn true } parentView = FrameLayout(context) Loading