diff options
8 files changed, 203 insertions, 51 deletions
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.java index 27bffd0818e7..04b930ed73b0 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.java @@ -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; @@ -52,63 +57,104 @@ public class ShadeTouchHandlerTest extends SysuiTestCase { 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, + } } diff --git a/packages/SystemUI/src/com/android/systemui/ambient/touch/ShadeTouchHandler.java b/packages/SystemUI/src/com/android/systemui/ambient/touch/ShadeTouchHandler.java index 9ef9938ab8ad..fcd7ef53d42a 100644 --- a/packages/SystemUI/src/com/android/systemui/ambient/touch/ShadeTouchHandler.java +++ b/packages/SystemUI/src/com/android/systemui/ambient/touch/ShadeTouchHandler.java @@ -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); diff --git a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt index bf0843b8fa4e..2def6c7cfdc2 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt @@ -157,9 +157,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 diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java index 6efa6334b968..c01b7b6f4883 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java @@ -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. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java index 05a43917f7e0..7434891805ca 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java @@ -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(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesEmptyImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesEmptyImpl.kt index a7b54847cdf9..906baa2a42f8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesEmptyImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesEmptyImpl.kt @@ -80,7 +80,7 @@ abstract class CentralSurfacesEmptyImpl : CentralSurfaces { override fun updateScrimController() {} override fun shouldIgnoreTouch() = false override fun isDeviceInteractive() = false - override fun handleDreamTouch(event: MotionEvent?) {} + override fun handleExternalShadeWindowTouch(event: MotionEvent?) {} override fun handleCommunalHubTouch(event: MotionEvent?) {} override fun awakenDreams() {} override fun isBouncerShowing() = false diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java index 7567f36302b4..42680ab4beba 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -2954,8 +2954,8 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { }; @Override - public void handleDreamTouch(MotionEvent event) { - getNotificationShadeWindowViewController().handleDreamTouch(event); + public void handleExternalShadeWindowTouch(MotionEvent event) { + getNotificationShadeWindowViewController().handleExternalTouch(event); } @Override diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt index 4a867a8ecf22..586adbd65ce8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt @@ -519,6 +519,46 @@ class NotificationShadeWindowViewControllerTest(flags: FlagsParameterization) : } @Test + fun handleExternalTouch_intercepted_sendsOnTouch() { + // Accept dispatch and also intercept. + whenever(view.dispatchTouchEvent(any())).thenReturn(true) + whenever(view.onInterceptTouchEvent(any())).thenReturn(true) + + underTest.handleExternalTouch(DOWN_EVENT) + underTest.handleExternalTouch(MOVE_EVENT) + + // Once intercepted, both events are sent to the view. + verify(view).onTouchEvent(DOWN_EVENT) + verify(view).onTouchEvent(MOVE_EVENT) + } + + @Test + fun handleExternalTouch_notDispatched_interceptNotCalled() { + // Don't accept dispatch + whenever(view.dispatchTouchEvent(any())).thenReturn(false) + + underTest.handleExternalTouch(DOWN_EVENT) + + // Interception is not offered. + verify(view, never()).onInterceptTouchEvent(any()) + } + + @Test + fun handleExternalTouch_notIntercepted_onTouchNotSent() { + // Accept dispatch, but don't dispatch + whenever(view.dispatchTouchEvent(any())).thenReturn(true) + whenever(view.onInterceptTouchEvent(any())).thenReturn(false) + + underTest.handleExternalTouch(DOWN_EVENT) + underTest.handleExternalTouch(MOVE_EVENT) + + // Interception offered for both events, but onTouchEvent is never called. + verify(view).onInterceptTouchEvent(DOWN_EVENT) + verify(view).onInterceptTouchEvent(MOVE_EVENT) + verify(view, never()).onTouchEvent(any()) + } + + @Test fun testGetKeyguardMessageArea() = testScope.runTest { underTest.keyguardMessageArea |