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 f3806ec8cd451b8b504635e313698afa0cd72439.
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
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 27bffd0..04b930e 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 @@
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.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 @@
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);
+ }
+
+ // Verifies that a swipe down in the gesture region is captured by the shade touch handler.
+ @Test
+ public void testSwipeDown_captured() {
+ final boolean captured = swipe(Direction.DOWN);
+
+ assertThat(captured).isTrue();
+ }
+
+ // Verifies that a swipe in the upward direction is not catpured.
+ @Test
+ public void testSwipeUp_notCaptured() {
+ final boolean captured = swipe(Direction.UP);
+
+ // Motion events not captured as the swipe is going in the wrong direction.
+ assertThat(captured).isFalse();
+ }
+
+ // Verifies that a swipe down forwards captured touches to central surfaces for handling.
+ @Test
+ public void testSwipeDown_sentToCentralSurfaces() {
+ swipe(Direction.DOWN);
+
+ // Both motion events are sent for the shade window to process.
+ verify(mCentralSurfaces, times(2)).handleExternalShadeWindowTouch(any());
+ }
+
+ // Verifies that a swipe down forwards captured touches to central surfaces for handling.
+ @Test
+ 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());
+ }
+
+ // 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());
}
/**
- * Verify that touches aren't handled when the bouncer is showing.
+ * 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.
*/
- @Test
- public void testInactiveOnBouncer() {
- when(mCentralSurfaces.isBouncerShowing()).thenReturn(true);
+ private boolean swipe(Direction direction) {
+ Mockito.clearInvocations(mTouchSession);
mTouchHandler.onSessionStart(mTouchSession);
- verify(mTouchSession).pop();
+
+ 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;
}
- /**
- * Make sure {@link ShadeTouchHandler}
- */
- @Test
- public void testTouchPilferingOnScroll() {
- final MotionEvent motionEvent1 = Mockito.mock(MotionEvent.class);
- final MotionEvent motionEvent2 = Mockito.mock(MotionEvent.class);
-
- final ArgumentCaptor<GestureDetector.OnGestureListener> gestureListenerArgumentCaptor =
- ArgumentCaptor.forClass(GestureDetector.OnGestureListener.class);
-
- mTouchHandler.onSessionStart(mTouchSession);
- verify(mTouchSession).registerGestureListener(gestureListenerArgumentCaptor.capture());
-
- assertThat(gestureListenerArgumentCaptor.getValue()
- .onScroll(motionEvent1, motionEvent2, 1, 1))
- .isTrue();
+ private enum Direction {
+ DOWN, UP,
}
-
- /**
- * Ensure touches are propagated to the {@link ShadeViewController}.
- */
- @Test
- public void testEventPropagation() {
- final MotionEvent motionEvent = Mockito.mock(MotionEvent.class);
-
- final ArgumentCaptor<InputChannelCompat.InputEventListener>
- inputEventListenerArgumentCaptor =
- ArgumentCaptor.forClass(InputChannelCompat.InputEventListener.class);
-
- mTouchHandler.onSessionStart(mTouchSession);
- verify(mTouchSession).registerInputListener(inputEventListenerArgumentCaptor.capture());
- inputEventListenerArgumentCaptor.getValue().onInputEvent(motionEvent);
- verify(mShadeViewController).handleExternalTouch(motionEvent);
- }
-
}
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 9ef9938..fcd7ef5 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 @@
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 @@
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 @@
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 1d8b7e5b..ee0ecf9 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
@@ -125,9 +125,15 @@
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 6efa633..c01b7b6 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 @@
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 @@
}
/**
- * 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 05a4391..7434891 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 @@
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 a7b5484..906baa2 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 @@
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 78a8036..5379258 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -2950,8 +2950,8 @@
};
@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 4a867a8..586adbd 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 @@
}
@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