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

Commit dc835025 authored by William Xiao's avatar William Xiao
Browse files

Reland "Fix ShadeTouchHandler over the lock screen"

The previous CL changed where ShadeTouchHandler sends touches, which
broke opening the shade over the dream on AP3A builds, which don't have
the hub or some of the keyguard refactors.

The fix is to only send touches on the new path for builds with the
hub flag enabled, and when not dreaming. The latter logic change is new
and added to prevent shade flickering when opening and closing the hub
over the dream.

To summarize, ShadeTouchHandler will continue to send touches to the
ShadeViewController when dreaming on all builds, and will send touches
to NotificationShadeWindowViewController only when on the glanceable
hub over the lock screen.

This reverts commit f3806ec8.

Bug: 328838259
Fix: 328838259
Test: atest ShadeTouchHandlerTest NotificationShadeWindowViewControllerTest
      Also verified manually that opening shade works on dream, lock
      screen, hub over either surface, and dream on AP3A build
Flag: com.android.systemui.glanceable_hub
Change-Id: I4fed54921d69606f4224505713883d1a186c7f49
parent 3c86dbb6
Loading
Loading
Loading
Loading
+78 −32
Original line number Diff line number Diff line
@@ -18,9 +18,13 @@ 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.app.DreamManager;
import android.view.GestureDetector;
import android.view.MotionEvent;

@@ -36,6 +40,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;
@@ -51,64 +56,105 @@ public class ShadeTouchHandlerTest extends SysuiTestCase {
    @Mock
    ShadeViewController mShadeViewController;

    @Mock
    DreamManager mDreamManager;

    @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);
                mDreamManager, 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 central surfaces for handling.
    @Test
    public void testSwipeDown_sentToCentralSurfaces() {
        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 forwards captured touches to central surfaces for handling.
    @Test
    public void testEventPropagation() {
        final MotionEvent motionEvent = Mockito.mock(MotionEvent.class);
    public void testSwipeDown_dreaming_sentToShadeView() {
        when(mDreamManager.isDreaming()).thenReturn(true);

        swipe(Direction.DOWN);

        // Both motion events are sent for the shade window to process.
        verify(mShadeViewController, times(2)).handleExternalTouch(any());
    }

        final ArgumentCaptor<InputChannelCompat.InputEventListener>
                inputEventListenerArgumentCaptor =
                    ArgumentCaptor.forClass(InputChannelCompat.InputEventListener.class);
    // Verifies that a swipe down is not forwarded to the shade window.
    @Test
    public void testSwipeUp_touchesNotSent() {
        swipe(Direction.UP);

        // 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,
    }
}
+44 −7
Original line number Diff line number Diff line
@@ -18,11 +18,15 @@ package com.android.systemui.ambient.touch;

import static com.android.systemui.ambient.touch.dagger.ShadeModule.NOTIFICATION_SHADE_GESTURE_INITIATION_HEIGHT;

import android.app.DreamManager;
import android.graphics.Rect;
import android.graphics.Region;
import android.view.GestureDetector;
import android.view.MotionEvent;

import androidx.annotation.NonNull;

import com.android.systemui.Flags;
import com.android.systemui.shade.ShadeViewController;
import com.android.systemui.statusbar.phone.CentralSurfaces;

@@ -38,28 +42,39 @@ import javax.inject.Named;
public class ShadeTouchHandler implements TouchHandler {
    private final Optional<CentralSurfaces> mSurfaces;
    private final ShadeViewController mShadeViewController;
    private final DreamManager mDreamManager;
    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,
            DreamManager dreamManager,
            @Named(NOTIFICATION_SHADE_GESTURE_INITIATION_HEIGHT) int initiationHeight) {
        mSurfaces = centralSurfaces;
        mShadeViewController = shadeViewController;
        mDreamManager = dreamManager;
        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) {
                    sendTouchEvent((MotionEvent) ev);
                }
                if (((MotionEvent) ev).getAction() == MotionEvent.ACTION_UP) {
                    session.pop();
                }
@@ -68,19 +83,41 @@ 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.
                        sendTouchEvent(e1);
                        sendTouchEvent(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;
            }
        });
    }

    private void sendTouchEvent(MotionEvent event) {
        if (Flags.communalHub() && !mDreamManager.isDreaming()) {
            // Send touches to central surfaces only when on the glanceable hub while not dreaming.
            // While sending touches where while dreaming will open the shade, the shade
            // while closing if opened then closed in the same gesture.
            mSurfaces.get().handleExternalShadeWindowTouch(event);
        } else {
            // Send touches to the shade view when dreaming.
            mShadeViewController.handleExternalTouch(event);
        }
    }

    @Override
    public void getTouchInitiationRegion(Rect bounds, Region region, Rect exclusionRect) {
        final Rect outBounds = new Rect(bounds);
+8 −2
Original line number Diff line number Diff line
@@ -125,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

+25 −3
Original line number Diff line number Diff line
@@ -138,6 +138,11 @@ public class NotificationShadeWindowViewController implements Dumpable {
    private final PanelExpansionInteractor mPanelExpansionInteractor;
    private final ShadeExpansionStateManager mShadeExpansionStateManager;

    /**
     * If {@code true}, an external touch sent in {@link #handleExternalTouch(MotionEvent)} has been
     * intercepted and all future touch events for the gesture should be processed by this view.
     */
    private boolean mExternalTouchIntercepted = false;
    private boolean mIsTrackingBarGesture = false;
    private boolean mIsOcclusionTransitionRunning = false;
    private DisableSubpixelTextTransitionListener mDisableSubpixelTextTransitionListener;
@@ -255,11 +260,28 @@ public class NotificationShadeWindowViewController implements Dumpable {
    }

    /**
     * Handle a touch event while dreaming by forwarding the event to the content view.
     * Handle a touch event while dreaming or on the hub by forwarding the event to the content
     * view.
     * <p>
     * Since important logic for handling touches lives in the dispatch/intercept phases, we
     * simulate going through all of these stages before sending onTouchEvent if intercepted.
     *
     * @param event The event to forward.
     */
    public void handleDreamTouch(MotionEvent event) {
        mView.dispatchTouchEvent(event);
    public void handleExternalTouch(MotionEvent event) {
        if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
            mExternalTouchIntercepted = false;
        }

        if (!mView.dispatchTouchEvent(event)) {
            return;
        }
        if (!mExternalTouchIntercepted) {
            mExternalTouchIntercepted = mView.onInterceptTouchEvent(event);
        }
        if (mExternalTouchIntercepted) {
            mView.onTouchEvent(event);
        }
    }

    /** Inflates the {@link R.layout#status_bar_expanded} layout and sets it up. */
+5 −4
Original line number Diff line number Diff line
@@ -288,11 +288,12 @@ public interface CentralSurfaces extends Dumpable, LifecycleOwner, CoreStartable
    void awakenDreams();

    /**
     * Handle a touch event while dreaming when the touch was initiated within a prescribed
     * swipeable area. This method is provided for cases where swiping in certain areas of a dream
     * should be handled by CentralSurfaces instead (e.g. swiping communal hub open).
     * Handle a touch event while dreaming or on the glanceable hub when the touch was initiated
     * within a prescribed swipeable area. This method is provided for cases where swiping in
     * certain areas should be handled by CentralSurfaces instead (e.g. swiping hub open, opening
     * the notification shade over dream or hub).
     */
    void handleDreamTouch(MotionEvent event);
    void handleExternalShadeWindowTouch(MotionEvent event);

    boolean isBouncerShowing();

Loading