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

Commit 6376fc1c authored by Bryce Lee's avatar Bryce Lee
Browse files

Fix dragging down notification shade handling over Glanceable Hub.

This changelist addresses a number of issues with notification shade
touch handling in glanceable hub:
- Properly handle cancel event to ShadeTouchHandler to end the touch
  session. not popping the touch session leads to a stale active
  session.
- Recreating the touch input session on the last popped session. This
  resets any pilfering logic that was tied to the original session.
- Only allow interactivity once the shade is expanded if the user is
  still interacting at full expansion.
- Begin tracking touches on move as well as done in the
  GlancealbeHubContainerController, as the original stream might be
  canceled but replaced with another source.

Test: atest GlanceableHubContainerControllerTest#lifecycle_doesNotResumeOnUserInteractivityOnceExpanded
Test: atest TouchMonitorTest#testLastSessionPop_createsNewInputSession
Test: atest ShadeTouchHandlerTest#testCancelMotionEvent_popsTouchSession
Flag: EXEMPT bugfix
Fixes: 353342159
Change-Id: Ide3f903cd44b1b4854adb3b32782e19946cecb20
parent 59f4204a
Loading
Loading
Loading
Loading
+8 −0
Original line number Diff line number Diff line
@@ -146,6 +146,14 @@ class ShadeTouchHandlerTest : SysuiTestCase() {
        verify(mShadeViewController, never()).handleExternalTouch(any())
    }

    @Test
    fun testCancelMotionEvent_popsTouchSession() {
        swipe(Direction.DOWN)
        val event = MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0)
        mInputListenerCaptor.lastValue.onInputEvent(event)
        verify(mTouchSession).pop()
    }

    /**
     * Simulates a swipe in the given direction and returns true if the touch was intercepted by the
     * touch handler's gesture listener.
+2 −1
Original line number Diff line number Diff line
@@ -79,7 +79,8 @@ public class ShadeTouchHandler implements TouchHandler {
                if (mCapture != null && mCapture) {
                    sendTouchEvent((MotionEvent) ev);
                }
                if (((MotionEvent) ev).getAction() == MotionEvent.ACTION_UP) {
                if (((MotionEvent) ev).getAction() == MotionEvent.ACTION_UP
                        || ((MotionEvent) ev).getAction() == MotionEvent.ACTION_CANCEL) {
                    session.pop();
                }
            }
+7 −2
Original line number Diff line number Diff line
@@ -128,8 +128,13 @@ public class TouchMonitor {
                    completer.set(predecessor);
                }

                if (mActiveTouchSessions.isEmpty() && mStopMonitoringPending) {
                if (mActiveTouchSessions.isEmpty()) {
                    if (mStopMonitoringPending) {
                        stopMonitoring(false);
                    } else {
                        // restart monitoring to reset any destructive state on the input session
                        startMonitoring();
                    }
                }
            });

+33 −9
Original line number Diff line number Diff line
@@ -56,13 +56,12 @@ import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.scene.shared.model.SceneDataSourceDelegator
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf
import com.android.systemui.util.kotlin.BooleanFlowOperators.anyOf
import com.android.systemui.util.kotlin.BooleanFlowOperators.not
import com.android.systemui.util.kotlin.collectFlow
import java.util.function.Consumer
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch

/**
@@ -165,7 +164,14 @@ constructor(
     *
     * Based on [ShadeInteractor.isAnyFullyExpanded] and [ShadeInteractor.isUserInteracting].
     */
    private var shadeShowing = false
    private var shadeShowingAndConsumingTouches = 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
     * collapsed.
     */
    private var userNotInteractiveAtShadeFullyExpanded = false

    /**
     * True if the device is dreaming, in which case we shouldn't do anything for top/bottom swipes
@@ -317,9 +323,25 @@ constructor(
        )
        collectFlow(
            containerView,
            allOf(shadeInteractor.isAnyFullyExpanded, not(shadeInteractor.isUserInteracting)),
            {
                shadeShowing = it
            combine(
                shadeInteractor.isAnyFullyExpanded,
                shadeInteractor.isUserInteracting,
                shadeInteractor.isShadeFullyCollapsed,
                ::Triple
            ),
            { (isFullyExpanded, isUserInteracting, isShadeFullyCollapsed) ->
                val expandedAndNotInteractive = isFullyExpanded && !isUserInteracting

                // If we ever are fully expanded and not interacting, capture this state as we
                // should not handle touches until we fully collapse again
                userNotInteractiveAtShadeFullyExpanded =
                    !isShadeFullyCollapsed &&
                        (userNotInteractiveAtShadeFullyExpanded || expandedAndNotInteractive)

                // If the shade reaches full expansion without interaction, then we should allow it
                // to consume touches rather than handling it here until it disappears.
                shadeShowingAndConsumingTouches =
                    userNotInteractiveAtShadeFullyExpanded || expandedAndNotInteractive
                updateTouchHandlingState()
            }
        )
@@ -337,7 +359,8 @@ constructor(
     * Also clears gesture exclusion zones when the hub is occluded or gone.
     */
    private fun updateTouchHandlingState() {
        val shouldInterceptGestures = hubShowing && !(shadeShowing || anyBouncerShowing)
        val shouldInterceptGestures =
            hubShowing && !(shadeShowingAndConsumingTouches || anyBouncerShowing)
        if (shouldInterceptGestures) {
            lifecycleRegistry.currentState = Lifecycle.State.RESUMED
        } else {
@@ -395,11 +418,12 @@ constructor(
    private fun handleTouchEventOnCommunalView(view: View, ev: MotionEvent): Boolean {
        val isDown = ev.actionMasked == MotionEvent.ACTION_DOWN
        val isUp = ev.actionMasked == MotionEvent.ACTION_UP
        val isMove = ev.actionMasked == MotionEvent.ACTION_MOVE
        val isCancel = ev.actionMasked == MotionEvent.ACTION_CANCEL

        val hubOccluded = anyBouncerShowing || shadeShowing
        val hubOccluded = anyBouncerShowing || shadeShowingAndConsumingTouches

        if (isDown && !hubOccluded) {
        if ((isDown || isMove) && !hubOccluded) {
            isTrackingHubTouch = true
        }

+29 −0
Original line number Diff line number Diff line
@@ -710,6 +710,35 @@ public class TouchMonitorTest extends SysuiTestCase {
        environment.verifyLifecycleObserversUnregistered();
    }

    @Test
    public void testLastSessionPop_createsNewInputSession() {
        final TouchHandler touchHandler = createTouchHandler();

        final TouchHandler.TouchSession.Callback callback =
                Mockito.mock(TouchHandler.TouchSession.Callback.class);

        final Environment environment = new Environment(Stream.of(touchHandler)
                .collect(Collectors.toCollection(HashSet::new)), mKosmos);

        final InputEvent initialEvent = Mockito.mock(InputEvent.class);
        environment.publishInputEvent(initialEvent);

        final TouchHandler.TouchSession session = captureSession(touchHandler);
        session.registerCallback(callback);

        // Clear invocations on input session and factory.
        clearInvocations(environment.mInputFactory);
        clearInvocations(environment.mInputSession);

        // Pop only active touch session.
        session.pop();
        environment.executeAll();

        // Verify that input session disposed and new session requested from factory.
        verify(environment.mInputSession).dispose();
        verify(environment.mInputFactory).create(any(), any(), any(), anyBoolean());
    }

    private GestureDetector.OnGestureListener registerGestureListener(TouchHandler handler) {
        final GestureDetector.OnGestureListener gestureListener = Mockito.mock(
                GestureDetector.OnGestureListener.class);
Loading