Merge "Add Touch Initiation Regions." into tm-dev

This commit is contained in:
Bryce Lee
2022-02-26 17:17:38 +00:00
committed by Android (Google) Code Review
5 changed files with 184 additions and 76 deletions

View File

@@ -21,6 +21,9 @@ import static com.android.systemui.dreams.touch.dagger.BouncerSwipeModule.SWIPE_
import static com.android.systemui.dreams.touch.dagger.BouncerSwipeModule.SWIPE_TO_BOUNCER_START_REGION; import static com.android.systemui.dreams.touch.dagger.BouncerSwipeModule.SWIPE_TO_BOUNCER_START_REGION;
import android.animation.ValueAnimator; import android.animation.ValueAnimator;
import android.graphics.Rect;
import android.graphics.Region;
import android.util.DisplayMetrics;
import android.util.Log; import android.util.Log;
import android.view.GestureDetector; import android.view.GestureDetector;
import android.view.InputEvent; import android.view.InputEvent;
@@ -75,6 +78,8 @@ public class BouncerSwipeTouchHandler implements DreamTouchHandler {
private final FlingAnimationUtils mFlingAnimationUtils; private final FlingAnimationUtils mFlingAnimationUtils;
private final FlingAnimationUtils mFlingAnimationUtilsClosing; private final FlingAnimationUtils mFlingAnimationUtilsClosing;
private final DisplayMetrics mDisplayMetrics;
private Boolean mCapture; private Boolean mCapture;
private TouchSession mTouchSession; private TouchSession mTouchSession;
@@ -85,40 +90,9 @@ public class BouncerSwipeTouchHandler implements DreamTouchHandler {
private final GestureDetector.OnGestureListener mOnGestureListener = private final GestureDetector.OnGestureListener mOnGestureListener =
new GestureDetector.SimpleOnGestureListener() { new GestureDetector.SimpleOnGestureListener() {
boolean mTrack;
boolean mBouncerPresent;
@Override
public boolean onDown(MotionEvent e) {
// We only consider gestures that originate from the lower portion of the
// screen.
final float displayHeight = mStatusBar.getDisplayHeight();
mBouncerPresent = mStatusBar.isBouncerShowing();
// The target zone is either at the top or bottom of the screen, dependent on
// whether the bouncer is present.
final float zonePercentage =
Math.abs(e.getY() - (mBouncerPresent ? 0 : displayHeight))
/ displayHeight;
mTrack = zonePercentage < mBouncerZoneScreenPercentage;
// Never capture onDown. While this might lead to some false positive touches
// being sent to other windows/layers, this is necessary to make sure the
// proper touch event sequence is received by others in the event we do not
// consume the sequence here.
return false;
}
@Override @Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
float distanceY) { float distanceY) {
// Do not handle scroll gestures if not tracking touch events.
if (!mTrack) {
return false;
}
if (mCapture == null) { if (mCapture == null) {
// If the user scrolling favors a vertical direction, begin capturing // If the user scrolling favors a vertical direction, begin capturing
// scrolls. // scrolls.
@@ -141,8 +115,8 @@ public class BouncerSwipeTouchHandler implements DreamTouchHandler {
// (0). // (0).
final float screenTravelPercentage = final float screenTravelPercentage =
Math.abs((e1.getY() - e2.getY()) / mStatusBar.getDisplayHeight()); Math.abs((e1.getY() - e2.getY()) / mStatusBar.getDisplayHeight());
setPanelExpansion( setPanelExpansion(mStatusBar.isBouncerShowing()
mBouncerPresent ? screenTravelPercentage : 1 - screenTravelPercentage); ? screenTravelPercentage : 1 - screenTravelPercentage);
return true; return true;
} }
@@ -155,6 +129,7 @@ public class BouncerSwipeTouchHandler implements DreamTouchHandler {
@Inject @Inject
public BouncerSwipeTouchHandler( public BouncerSwipeTouchHandler(
DisplayMetrics displayMetrics,
StatusBarKeyguardViewManager statusBarKeyguardViewManager, StatusBarKeyguardViewManager statusBarKeyguardViewManager,
StatusBar statusBar, StatusBar statusBar,
NotificationShadeWindowController notificationShadeWindowController, NotificationShadeWindowController notificationShadeWindowController,
@@ -165,6 +140,7 @@ public class BouncerSwipeTouchHandler implements DreamTouchHandler {
@Named(SWIPE_TO_BOUNCER_FLING_ANIMATION_UTILS_OPENING) @Named(SWIPE_TO_BOUNCER_FLING_ANIMATION_UTILS_OPENING)
FlingAnimationUtils flingAnimationUtilsClosing, FlingAnimationUtils flingAnimationUtilsClosing,
@Named(SWIPE_TO_BOUNCER_START_REGION) float swipeRegionPercentage) { @Named(SWIPE_TO_BOUNCER_START_REGION) float swipeRegionPercentage) {
mDisplayMetrics = displayMetrics;
mStatusBar = statusBar; mStatusBar = statusBar;
mStatusBarKeyguardViewManager = statusBarKeyguardViewManager; mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
mNotificationShadeWindowController = notificationShadeWindowController; mNotificationShadeWindowController = notificationShadeWindowController;
@@ -175,6 +151,21 @@ public class BouncerSwipeTouchHandler implements DreamTouchHandler {
mVelocityTrackerFactory = velocityTrackerFactory; mVelocityTrackerFactory = velocityTrackerFactory;
} }
@Override
public void getTouchInitiationRegion(Region region) {
if (mStatusBar.isBouncerShowing()) {
region.op(new Rect(0, 0, mDisplayMetrics.widthPixels,
Math.round(mDisplayMetrics.heightPixels * mBouncerZoneScreenPercentage)),
Region.Op.UNION);
} else {
region.op(new Rect(0,
Math.round(mDisplayMetrics.heightPixels * (1 - mBouncerZoneScreenPercentage)),
mDisplayMetrics.widthPixels,
mDisplayMetrics.heightPixels),
Region.Op.UNION);
}
}
@Override @Override
public void onSessionStart(TouchSession session) { public void onSessionStart(TouchSession session) {
mVelocityTracker = mVelocityTrackerFactory.obtain(); mVelocityTracker = mVelocityTrackerFactory.obtain();
@@ -202,7 +193,9 @@ public class BouncerSwipeTouchHandler implements DreamTouchHandler {
final MotionEvent motionEvent = (MotionEvent) event; final MotionEvent motionEvent = (MotionEvent) event;
switch(motionEvent.getAction()) { switch(motionEvent.getAction()) {
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP: case MotionEvent.ACTION_UP:
mTouchSession.pop();
// If we are not capturing any input, there is no need to consider animating to // If we are not capturing any input, there is no need to consider animating to
// finish transition. // finish transition.
if (mCapture == null || !mCapture) { if (mCapture == null || !mCapture) {
@@ -226,7 +219,6 @@ public class BouncerSwipeTouchHandler implements DreamTouchHandler {
if (expansion == KeyguardBouncer.EXPANSION_HIDDEN) { if (expansion == KeyguardBouncer.EXPANSION_HIDDEN) {
mStatusBarKeyguardViewManager.reset(false); mStatusBarKeyguardViewManager.reset(false);
} }
mTouchSession.pop();
break; break;
default: default:
mVelocityTracker.addMovement(motionEvent); mVelocityTracker.addMovement(motionEvent);

View File

@@ -16,6 +16,7 @@
package com.android.systemui.dreams.touch; package com.android.systemui.dreams.touch;
import android.graphics.Region;
import android.view.GestureDetector; import android.view.GestureDetector;
import android.view.InputEvent; import android.view.InputEvent;
import android.view.MotionEvent; import android.view.MotionEvent;
@@ -34,6 +35,7 @@ import com.android.systemui.shared.system.InputChannelCompat;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
@@ -100,6 +102,10 @@ public class DreamOverlayTouchMonitor {
}); });
} }
private int getSessionCount() {
return mActiveTouchSessions.size();
}
/** /**
* {@link TouchSessionImpl} implements {@link DreamTouchHandler.TouchSession} for * {@link TouchSessionImpl} implements {@link DreamTouchHandler.TouchSession} for
* {@link DreamOverlayTouchMonitor}. It enables the monitor to access the associated listeners * {@link DreamOverlayTouchMonitor}. It enables the monitor to access the associated listeners
@@ -146,6 +152,11 @@ public class DreamOverlayTouchMonitor {
return mTouchMonitor.pop(this); return mTouchMonitor.pop(this);
} }
@Override
public int getActiveSessionCount() {
return mTouchMonitor.getSessionCount();
}
/** /**
* Returns the active listeners to receive touch events. * Returns the active listeners to receive touch events.
*/ */
@@ -229,12 +240,39 @@ public class DreamOverlayTouchMonitor {
public void onInputEvent(InputEvent ev) { public void onInputEvent(InputEvent ev) {
// No Active sessions are receiving touches. Create sessions for each listener // No Active sessions are receiving touches. Create sessions for each listener
if (mActiveTouchSessions.isEmpty()) { if (mActiveTouchSessions.isEmpty()) {
final HashMap<DreamTouchHandler, DreamTouchHandler.TouchSession> sessionMap =
new HashMap<>();
for (DreamTouchHandler handler : mHandlers) { for (DreamTouchHandler handler : mHandlers) {
final Region initiationRegion = Region.obtain();
handler.getTouchInitiationRegion(initiationRegion);
if (!initiationRegion.isEmpty()) {
// Initiation regions require a motion event to determine pointer location
// within the region.
if (!(ev instanceof MotionEvent)) {
continue;
}
final MotionEvent motionEvent = (MotionEvent) ev;
// If the touch event is outside the region, then ignore.
if (!initiationRegion.contains(Math.round(motionEvent.getX()),
Math.round(motionEvent.getY()))) {
continue;
}
}
final TouchSessionImpl sessionStack = final TouchSessionImpl sessionStack =
new TouchSessionImpl(DreamOverlayTouchMonitor.this, null); new TouchSessionImpl(DreamOverlayTouchMonitor.this, null);
mActiveTouchSessions.add(sessionStack); mActiveTouchSessions.add(sessionStack);
handler.onSessionStart(sessionStack); sessionMap.put(handler, sessionStack);
} }
// Informing handlers of new sessions is delayed until we have all created so the
// final session is correct.
sessionMap.forEach((dreamTouchHandler, touchSession)
-> dreamTouchHandler.onSessionStart(touchSession));
} }
// Find active sessions and invoke on InputEvent. // Find active sessions and invoke on InputEvent.

View File

@@ -16,6 +16,7 @@
package com.android.systemui.dreams.touch; package com.android.systemui.dreams.touch;
import android.graphics.Region;
import android.view.GestureDetector; import android.view.GestureDetector;
import com.android.systemui.shared.system.InputChannelCompat; import com.android.systemui.shared.system.InputChannelCompat;
@@ -71,6 +72,19 @@ public interface DreamTouchHandler {
* if the popped {@link TouchSession} was the initial session or has already been popped. * if the popped {@link TouchSession} was the initial session or has already been popped.
*/ */
ListenableFuture<TouchSession> pop(); ListenableFuture<TouchSession> pop();
/**
* Returns the number of currently active sessions.
*/
int getActiveSessionCount();
}
/**
* Returns the region the touch handler is interested in. By default, no region is specified,
* indicating the entire screen should be considered.
* @param region A {@link Region} that is passed in to the target entry touch region.
*/
default void getTouchInitiationRegion(Region region) {
} }
/** /**

View File

@@ -18,7 +18,6 @@ package com.android.systemui.dreams.touch;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.ArgumentMatchers.anyFloat;
@@ -27,12 +26,14 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import android.animation.ValueAnimator; import android.animation.ValueAnimator;
import android.graphics.Rect;
import android.graphics.Region;
import android.testing.AndroidTestingRunner; import android.testing.AndroidTestingRunner;
import android.util.DisplayMetrics;
import android.view.GestureDetector; import android.view.GestureDetector;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.VelocityTracker; import android.view.VelocityTracker;
import androidx.test.filters.FlakyTest;
import androidx.test.filters.SmallTest; import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase; import com.android.systemui.SysuiTestCase;
@@ -51,8 +52,6 @@ import org.mockito.Mock;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.mockito.MockitoAnnotations; import org.mockito.MockitoAnnotations;
import java.util.Random;
@SmallTest @SmallTest
@RunWith(AndroidTestingRunner.class) @RunWith(AndroidTestingRunner.class)
public class BouncerSwipeTouchHandlerTest extends SysuiTestCase { public class BouncerSwipeTouchHandlerTest extends SysuiTestCase {
@@ -89,13 +88,20 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase {
@Mock @Mock
VelocityTracker mVelocityTracker; VelocityTracker mVelocityTracker;
final DisplayMetrics mDisplayMetrics = new DisplayMetrics();
private static final float TOUCH_REGION = .3f; private static final float TOUCH_REGION = .3f;
private static final float SCREEN_HEIGHT_PX = 100; private static final int SCREEN_WIDTH_PX = 1024;
private static final int SCREEN_HEIGHT_PX = 100;
@Before @Before
public void setup() { public void setup() {
mDisplayMetrics.widthPixels = SCREEN_WIDTH_PX;
mDisplayMetrics.heightPixels = SCREEN_HEIGHT_PX;
MockitoAnnotations.initMocks(this); MockitoAnnotations.initMocks(this);
mTouchHandler = new BouncerSwipeTouchHandler( mTouchHandler = new BouncerSwipeTouchHandler(
mDisplayMetrics,
mStatusBarKeyguardViewManager, mStatusBarKeyguardViewManager,
mStatusBar, mStatusBar,
mNotificationShadeWindowController, mNotificationShadeWindowController,
@@ -104,23 +110,29 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase {
mFlingAnimationUtils, mFlingAnimationUtils,
mFlingAnimationUtilsClosing, mFlingAnimationUtilsClosing,
TOUCH_REGION); TOUCH_REGION);
when(mStatusBar.getDisplayHeight()).thenReturn(SCREEN_HEIGHT_PX);
when(mStatusBar.getDisplayHeight()).thenReturn((float) SCREEN_HEIGHT_PX);
when(mValueAnimatorCreator.create(anyFloat(), anyFloat())).thenReturn(mValueAnimator); when(mValueAnimatorCreator.create(anyFloat(), anyFloat())).thenReturn(mValueAnimator);
when(mVelocityTrackerFactory.obtain()).thenReturn(mVelocityTracker); when(mVelocityTrackerFactory.obtain()).thenReturn(mVelocityTracker);
} }
private static void beginValidSwipe(GestureDetector.OnGestureListener listener) {
listener.onDown(MotionEvent.obtain(0, 0,
MotionEvent.ACTION_DOWN, 0,
SCREEN_HEIGHT_PX - (.5f * TOUCH_REGION * SCREEN_HEIGHT_PX), 0));
}
/** /**
* Ensures expansion only happens when touch down happens in valid part of the screen. * Ensures expansion only happens when touch down happens in valid part of the screen.
*/ */
@FlakyTest
@Test @Test
public void testSessionStart() { public void testSessionStart() {
final Region region = Region.obtain();
mTouchHandler.getTouchInitiationRegion(region);
final Rect bounds = region.getBounds();
final Rect expected = new Rect();
expected.set(0, Math.round(SCREEN_HEIGHT_PX * (1 - TOUCH_REGION)), SCREEN_WIDTH_PX,
SCREEN_HEIGHT_PX);
assertThat(bounds).isEqualTo(expected);
mTouchHandler.onSessionStart(mTouchSession); mTouchHandler.onSessionStart(mTouchSession);
verify(mNotificationShadeWindowController).setForcePluginOpen(eq(true), any()); verify(mNotificationShadeWindowController).setForcePluginOpen(eq(true), any());
ArgumentCaptor<InputChannelCompat.InputEventListener> eventListenerCaptor = ArgumentCaptor<InputChannelCompat.InputEventListener> eventListenerCaptor =
@@ -130,34 +142,12 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase {
verify(mTouchSession).registerGestureListener(gestureListenerCaptor.capture()); verify(mTouchSession).registerGestureListener(gestureListenerCaptor.capture());
verify(mTouchSession).registerInputListener(eventListenerCaptor.capture()); verify(mTouchSession).registerInputListener(eventListenerCaptor.capture());
final Random random = new Random(System.currentTimeMillis());
// If an initial touch down meeting criteria has been met, scroll behavior should be
// ignored.
assertThat(gestureListenerCaptor.getValue()
.onScroll(Mockito.mock(MotionEvent.class),
Mockito.mock(MotionEvent.class),
random.nextFloat(),
random.nextFloat())).isFalse();
// A touch at the top of the screen should also not trigger listening.
final MotionEvent touchDownEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN,
0, 0, 0);
gestureListenerCaptor.getValue().onDown(touchDownEvent);
assertThat(gestureListenerCaptor.getValue()
.onScroll(Mockito.mock(MotionEvent.class),
Mockito.mock(MotionEvent.class),
random.nextFloat(),
random.nextFloat())).isFalse();
// A touch within range at the bottom of the screen should trigger listening // A touch within range at the bottom of the screen should trigger listening
beginValidSwipe(gestureListenerCaptor.getValue());
assertThat(gestureListenerCaptor.getValue() assertThat(gestureListenerCaptor.getValue()
.onScroll(Mockito.mock(MotionEvent.class), .onScroll(Mockito.mock(MotionEvent.class),
Mockito.mock(MotionEvent.class), Mockito.mock(MotionEvent.class),
random.nextFloat(), 1,
random.nextFloat())).isTrue(); 2)).isTrue();
} }
/** /**
@@ -170,8 +160,6 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase {
ArgumentCaptor.forClass(GestureDetector.OnGestureListener.class); ArgumentCaptor.forClass(GestureDetector.OnGestureListener.class);
verify(mTouchSession).registerGestureListener(gestureListenerCaptor.capture()); verify(mTouchSession).registerGestureListener(gestureListenerCaptor.capture());
beginValidSwipe(gestureListenerCaptor.getValue());
final float scrollAmount = .3f; final float scrollAmount = .3f;
final float distanceY = SCREEN_HEIGHT_PX * scrollAmount; final float distanceY = SCREEN_HEIGHT_PX * scrollAmount;
@@ -203,8 +191,6 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase {
when(mVelocityTracker.getYVelocity()).thenReturn(velocityY); when(mVelocityTracker.getYVelocity()).thenReturn(velocityY);
beginValidSwipe(gestureListenerCaptor.getValue());
final float distanceY = SCREEN_HEIGHT_PX * position; final float distanceY = SCREEN_HEIGHT_PX * position;
final MotionEvent event1 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, final MotionEvent event1 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE,

View File

@@ -21,10 +21,13 @@ import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.never; import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import android.graphics.Rect;
import android.graphics.Region;
import android.testing.AndroidTestingRunner; import android.testing.AndroidTestingRunner;
import android.util.Pair; import android.util.Pair;
import android.view.GestureDetector; import android.view.GestureDetector;
@@ -136,6 +139,81 @@ public class DreamOverlayTouchMonitorTest extends SysuiTestCase {
} }
} }
@Test
public void testEntryTouchZone() {
final DreamTouchHandler touchHandler = Mockito.mock(DreamTouchHandler.class);
final Rect touchArea = new Rect(4, 4, 8 , 8);
doAnswer(invocation -> {
final Region region = (Region) invocation.getArguments()[0];
region.set(touchArea);
return null;
}).when(touchHandler).getTouchInitiationRegion(any());
final Environment environment = new Environment(Stream.of(touchHandler)
.collect(Collectors.toCollection(HashSet::new)));
// Ensure touch outside specified region is not delivered.
final MotionEvent initialEvent = Mockito.mock(MotionEvent.class);
when(initialEvent.getX()).thenReturn(0.0f);
when(initialEvent.getY()).thenReturn(1.0f);
environment.publishInputEvent(initialEvent);
verify(touchHandler, never()).onSessionStart(any());
// Make sure touch inside region causes session start.
when(initialEvent.getX()).thenReturn(5.0f);
when(initialEvent.getY()).thenReturn(5.0f);
environment.publishInputEvent(initialEvent);
verify(touchHandler).onSessionStart(any());
}
@Test
public void testSessionCount() {
final DreamTouchHandler touchHandler = Mockito.mock(DreamTouchHandler.class);
final Rect touchArea = new Rect(4, 4, 8 , 8);
final DreamTouchHandler unzonedTouchHandler = Mockito.mock(DreamTouchHandler.class);
doAnswer(invocation -> {
final Region region = (Region) invocation.getArguments()[0];
region.set(touchArea);
return null;
}).when(touchHandler).getTouchInitiationRegion(any());
final Environment environment = new Environment(Stream.of(touchHandler, unzonedTouchHandler)
.collect(Collectors.toCollection(HashSet::new)));
// Ensure touch outside specified region is delivered to unzoned touch handler.
final MotionEvent initialEvent = Mockito.mock(MotionEvent.class);
when(initialEvent.getX()).thenReturn(0.0f);
when(initialEvent.getY()).thenReturn(1.0f);
environment.publishInputEvent(initialEvent);
ArgumentCaptor<DreamTouchHandler.TouchSession> touchSessionCaptor = ArgumentCaptor.forClass(
DreamTouchHandler.TouchSession.class);
// Make sure only one active session.
{
verify(unzonedTouchHandler).onSessionStart(touchSessionCaptor.capture());
final DreamTouchHandler.TouchSession touchSession = touchSessionCaptor.getValue();
assertThat(touchSession.getActiveSessionCount()).isEqualTo(1);
touchSession.pop();
environment.executeAll();
}
// Make sure touch inside the touch region.
when(initialEvent.getX()).thenReturn(5.0f);
when(initialEvent.getY()).thenReturn(5.0f);
environment.publishInputEvent(initialEvent);
// Make sure there are two active sessions.
{
verify(touchHandler).onSessionStart(touchSessionCaptor.capture());
final DreamTouchHandler.TouchSession touchSession = touchSessionCaptor.getValue();
assertThat(touchSession.getActiveSessionCount()).isEqualTo(2);
touchSession.pop();
}
}
@Test @Test
public void testInputEventPropagation() { public void testInputEventPropagation() {
final DreamTouchHandler touchHandler = Mockito.mock(DreamTouchHandler.class); final DreamTouchHandler touchHandler = Mockito.mock(DreamTouchHandler.class);