Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 8b4b3c7d authored by William Xiao's avatar William Xiao
Browse files

Fix hub also reacting to lockscreen shade/bouncer gestures

The hub's TouchMonitor is not active on keyguard and shade/bouncer
swipes are handled by the existing logic in
NotificationShadeWindowViewController. Since these touches don't go
through the typical view touch-handling path, the hub sees the touches
before the shade/bouncer and continues to receive move events even if
the shade or bouncer are reacting to them.

This change prevents the hub from receiving and handling touches once
the bouncer or shade are opening on keyguard, with the exception of up
or cancel events, as the hub always receives at least a few initial
events before the gesture's intention is determined.

Manually tested by swiping from the top/bottom a small amount then
turning gesture to left and right on keyguard, hub, and dream.

Bug: 356999013
Fix: 356999013
Test: atest GlanceableHubContainerControllerTest
Flag: com.android.systemui.communal_hub
Change-Id: I427cd436698a360cac59dc591d906bda36d45d69
parent 74d031c5
Loading
Loading
Loading
Loading
+34 −3
Original line number Diff line number Diff line
@@ -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
@@ -336,6 +346,11 @@ constructor(
                updateTouchHandlingState()
            }
        )
        collectFlow(
            containerView,
            keyguardTransitionInteractor.isFinishedIn(KeyguardState.LOCKSCREEN),
            { onLockscreen = it }
        )
        collectFlow(
            containerView,
            communalInteractor.isCommunalVisible,
@@ -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
@@ -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(
+75 −2
Original line number Diff line number Diff line
@@ -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
@@ -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)
            }
        }

@@ -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)