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

Commit 86fa77ab authored by William Xiao's avatar William Xiao
Browse files

Fix ShadeTouchHandler over the lock screen

Shade expansion while on the lock screen is handled by the
NotificationShadeWindowViewController's DragDownHelper. In order for
the ShadeTouchHandler to work when opening the shade from the hub over
the lock screen, we need to simulate the full sequence of events for a
touch, since NSWVC has logic in both the dispatch and intercept phases.

This change also limits the ShadeTouchHandler to only accept downward
swipes. Upward swipes were not an issue over the dream, but it causes
some visual issues over the lock screen.

Fixed: 328838259
Bug: 335505186
Test: atest ShadeTouchHandlerTest CommunalTouchHandlerTest NotificationShadeWindowViewControllerTest
      also verified manually, see bug for video
Flag: ACONFIG com.android.systemui.communal_hub TEAMFOOD
Change-Id: I92b9c220b52b0c0677712fcda6ace27dff143ffa
parent c84e0559
Loading
Loading
Loading
Loading
+63 −38
Original line number Diff line number Diff line
@@ -18,8 +18,10 @@ package com.android.systemui.ambient.touch;

import static com.google.common.truth.Truth.assertThat;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.view.GestureDetector;
import android.view.MotionEvent;
@@ -28,7 +30,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;

import com.android.systemui.SysuiTestCase;
import com.android.systemui.shade.ShadeViewController;
import com.android.systemui.shared.system.InputChannelCompat;
import com.android.systemui.statusbar.phone.CentralSurfaces;

@@ -36,6 +37,7 @@ import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
@@ -48,67 +50,90 @@ public class ShadeTouchHandlerTest extends SysuiTestCase {
    @Mock
    CentralSurfaces mCentralSurfaces;

    @Mock
    ShadeViewController mShadeViewController;

    @Mock
    TouchHandler.TouchSession mTouchSession;

    ShadeTouchHandler mTouchHandler;

    @Captor
    ArgumentCaptor<GestureDetector.OnGestureListener> mGestureListenerCaptor;
    @Captor
    ArgumentCaptor<InputChannelCompat.InputEventListener> mInputListenerCaptor;

    private static final int TOUCH_HEIGHT = 20;

    @Before
    public void setup() {
        MockitoAnnotations.initMocks(this);
        mTouchHandler = new ShadeTouchHandler(Optional.of(mCentralSurfaces), mShadeViewController,
                TOUCH_HEIGHT);

        mTouchHandler = new ShadeTouchHandler(Optional.of(mCentralSurfaces), TOUCH_HEIGHT);
    }

    /**
     * Verify that touches aren't handled when the bouncer is showing.
     */
    // Verifies that a swipe down in the gesture region is captured by the shade touch handler.
    @Test
    public void testInactiveOnBouncer() {
        when(mCentralSurfaces.isBouncerShowing()).thenReturn(true);
        mTouchHandler.onSessionStart(mTouchSession);
        verify(mTouchSession).pop();
    public void testSwipeDown_captured() {
        final boolean captured = swipe(Direction.DOWN);

        assertThat(captured).isTrue();
    }

    /**
     * Make sure {@link ShadeTouchHandler}
     */
    // Verifies that a swipe in the upward direction is not catpured.
    @Test
    public void testTouchPilferingOnScroll() {
        final MotionEvent motionEvent1 = Mockito.mock(MotionEvent.class);
        final MotionEvent motionEvent2 = Mockito.mock(MotionEvent.class);
    public void testSwipeUp_notCaptured() {
        final boolean captured = swipe(Direction.UP);

        final ArgumentCaptor<GestureDetector.OnGestureListener> gestureListenerArgumentCaptor =
                ArgumentCaptor.forClass(GestureDetector.OnGestureListener.class);
        // Motion events not captured as the swipe is going in the wrong direction.
        assertThat(captured).isFalse();
    }

        mTouchHandler.onSessionStart(mTouchSession);
        verify(mTouchSession).registerGestureListener(gestureListenerArgumentCaptor.capture());
    // Verifies that a swipe down forwards captured touches to the shade window for handling.
    @Test
    public void testSwipeDown_sentToShadeWindow() {
        swipe(Direction.DOWN);

        assertThat(gestureListenerArgumentCaptor.getValue()
                .onScroll(motionEvent1, motionEvent2, 1, 1))
                .isTrue();
        // Both motion events are sent for the shade window to process.
        verify(mCentralSurfaces, times(2)).handleExternalShadeWindowTouch(any());
    }

    /**
     * Ensure touches are propagated to the {@link ShadeViewController}.
     */
    // Verifies that a swipe down is not forwarded to the shade window.
    @Test
    public void testEventPropagation() {
        final MotionEvent motionEvent = Mockito.mock(MotionEvent.class);
    public void testSwipeUp_touchesNotSent() {
        swipe(Direction.UP);

        final ArgumentCaptor<InputChannelCompat.InputEventListener>
                inputEventListenerArgumentCaptor =
                    ArgumentCaptor.forClass(InputChannelCompat.InputEventListener.class);
        // Motion events are not sent for the shade window to process as the swipe is going in the
        // wrong direction.
        verify(mCentralSurfaces, never()).handleExternalShadeWindowTouch(any());
    }

    /**
     * Simulates a swipe in the given direction and returns true if the touch was intercepted by the
     * touch handler's gesture listener.
     * <p>
     * Swipe down starts from a Y coordinate of 0 and goes downward. Swipe up starts from the edge
     * of the gesture region, {@link #TOUCH_HEIGHT}, and goes upward to 0.
     */
    private boolean swipe(Direction direction) {
        Mockito.clearInvocations(mTouchSession);
        mTouchHandler.onSessionStart(mTouchSession);
        verify(mTouchSession).registerInputListener(inputEventListenerArgumentCaptor.capture());
        inputEventListenerArgumentCaptor.getValue().onInputEvent(motionEvent);
        verify(mShadeViewController).handleExternalTouch(motionEvent);

        verify(mTouchSession).registerGestureListener(mGestureListenerCaptor.capture());
        verify(mTouchSession).registerInputListener(mInputListenerCaptor.capture());

        final float startY = direction == Direction.UP ? TOUCH_HEIGHT : 0;
        final float endY = direction == Direction.UP ? 0 : TOUCH_HEIGHT;

        // Send touches to the input and gesture listener.
        final MotionEvent event1 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, 0, startY, 0);
        final MotionEvent event2 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, 0, endY, 0);
        mInputListenerCaptor.getValue().onInputEvent(event1);
        mInputListenerCaptor.getValue().onInputEvent(event2);
        final boolean captured = mGestureListenerCaptor.getValue().onScroll(event1, event2, 0,
                startY - endY);

        return captured;
    }

    private enum Direction {
        DOWN, UP,
    }
}
+1 −1
Original line number Diff line number Diff line
@@ -108,7 +108,7 @@ public class CommunalTouchHandlerTest extends SysuiTestCase {
        mTouchHandler.onSessionStart(mTouchSession);
        verify(mTouchSession).registerInputListener(inputEventListenerArgumentCaptor.capture());
        inputEventListenerArgumentCaptor.getValue().onInputEvent(motionEvent);
        verify(mCentralSurfaces).handleDreamTouch(motionEvent);
        verify(mCentralSurfaces).handleExternalShadeWindowTouch(motionEvent);
    }

    @Test
+27 −11
Original line number Diff line number Diff line
@@ -23,7 +23,8 @@ import android.graphics.Region;
import android.view.GestureDetector;
import android.view.MotionEvent;

import com.android.systemui.shade.ShadeViewController;
import androidx.annotation.NonNull;

import com.android.systemui.statusbar.phone.CentralSurfaces;

import java.util.Optional;
@@ -37,29 +38,34 @@ import javax.inject.Named;
 */
public class ShadeTouchHandler implements TouchHandler {
    private final Optional<CentralSurfaces> mSurfaces;
    private final ShadeViewController mShadeViewController;
    private final int mInitiationHeight;

    /**
     * Tracks whether or not we are capturing a given touch. Will be null before and after a touch.
     */
    private Boolean mCapture;

    @Inject
    ShadeTouchHandler(Optional<CentralSurfaces> centralSurfaces,
            ShadeViewController shadeViewController,
            @Named(NOTIFICATION_SHADE_GESTURE_INITIATION_HEIGHT) int initiationHeight) {
        mSurfaces = centralSurfaces;
        mShadeViewController = shadeViewController;
        mInitiationHeight = initiationHeight;
    }

    @Override
    public void onSessionStart(TouchSession session) {
        if (mSurfaces.map(CentralSurfaces::isBouncerShowing).orElse(false)) {
        if (mSurfaces.isEmpty()) {
            session.pop();
            return;
        }

        session.registerInputListener(ev -> {
            mShadeViewController.handleExternalTouch((MotionEvent) ev);
        session.registerCallback(() -> mCapture = null);

        session.registerInputListener(ev -> {
            if (ev instanceof MotionEvent) {
                if (mCapture != null && mCapture) {
                    mSurfaces.get().handleExternalShadeWindowTouch((MotionEvent) ev);
                }
                if (((MotionEvent) ev).getAction() == MotionEvent.ACTION_UP) {
                    session.pop();
                }
@@ -68,15 +74,25 @@ public class ShadeTouchHandler implements TouchHandler {

        session.registerGestureListener(new GestureDetector.SimpleOnGestureListener() {
            @Override
            public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
            public boolean onScroll(MotionEvent e1, @NonNull MotionEvent e2, float distanceX,
                    float distanceY) {
                return true;
                if (mCapture == null) {
                    // Only capture swipes that are going downwards.
                    mCapture = Math.abs(distanceY) > Math.abs(distanceX) && distanceY < 0;
                    if (mCapture) {
                        // Send the initial touches over, as the input listener has already
                        // processed these touches.
                        mSurfaces.get().handleExternalShadeWindowTouch(e1);
                        mSurfaces.get().handleExternalShadeWindowTouch(e2);
                    }
                }
                return mCapture;
            }

            @Override
            public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
            public boolean onFling(MotionEvent e1, @NonNull MotionEvent e2, float velocityX,
                    float velocityY) {
                return true;
                return mCapture;
            }
        });
    }
+1 −1
Original line number Diff line number Diff line
@@ -98,7 +98,7 @@ public class CommunalTouchHandler implements TouchHandler {
        // Notification shade window has its own logic to be visible if the hub is open, no need to
        // do anything here other than send touch events over.
        session.registerInputListener(ev -> {
            surfaces.handleDreamTouch((MotionEvent) ev);
            surfaces.handleExternalShadeWindowTouch((MotionEvent) ev);
            if (ev != null && ((MotionEvent) ev).getAction() == MotionEvent.ACTION_UP) {
                var unused = session.pop();
            }
+11 −3
Original line number Diff line number Diff line
@@ -49,6 +49,8 @@ import com.android.systemui.res.R
import com.android.systemui.scene.shared.model.SceneDataSourceDelegator
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.statusbar.phone.SystemUIDialogFactory
import com.android.systemui.util.kotlin.BooleanFlowOperators.and
import com.android.systemui.util.kotlin.BooleanFlowOperators.not
import com.android.systemui.util.kotlin.BooleanFlowOperators.or
import com.android.systemui.util.kotlin.collectFlow
import javax.inject.Inject
@@ -123,9 +125,15 @@ constructor(
    private var anyBouncerShowing = false

    /**
     * True if the shade is fully expanded, meaning the hub should not receive any touch input.
     * True if the shade is fully expanded and the user is not interacting with it anymore, meaning
     * the hub should not receive any touch input.
     *
     * Tracks [ShadeInteractor.isAnyFullyExpanded].
     * We need to not pause the touch handling lifecycle as soon as the shade opens because if the
     * user swipes down, then back up without lifting their finger, the lifecycle will be paused
     * then resumed, and resuming force-stops all active touch sessions. This means the shade will
     * not receive the end of the gesture and will be stuck open.
     *
     * Based on [ShadeInteractor.isAnyFullyExpanded] and [ShadeInteractor.isUserInteracting].
     */
    private var shadeShowing = false

@@ -225,7 +233,7 @@ constructor(
        )
        collectFlow(
            containerView,
            shadeInteractor.isAnyFullyExpanded,
            and(shadeInteractor.isAnyFullyExpanded, not(shadeInteractor.isUserInteracting)),
            {
                shadeShowing = it
                updateLifecycleState()
Loading