Add Touch Initiation Regions.

This changelist adds touch initiation regions for DreamTouchHandler.
Regions allow handlers to specify where the initial input event must
occur in for a session directed at it to start. In turn, this allows
for an accurate understanding of active sessions at any given time as
the touch manager can preemptively prune non participating sessions.

This changelist also updates the BouncerSwipeTouchHandler to now
specify a region rather than check the first event. A new method has
been added to touch session to query the number of active sessions.

Bug: 214292772
Test: atest DreamOverlayTouchMonitorTest BouncerSwipeTouchHandlerTest
Change-Id: Iaa5fe71d0375c8ee8e74e12eeffdcba3f51187ec
This commit is contained in:
Bryce Lee
2022-02-18 17:38:34 -08:00
parent caf89ec290
commit 927480798a
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 android.animation.ValueAnimator;
import android.graphics.Rect;
import android.graphics.Region;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.GestureDetector;
import android.view.InputEvent;
@@ -75,6 +78,8 @@ public class BouncerSwipeTouchHandler implements DreamTouchHandler {
private final FlingAnimationUtils mFlingAnimationUtils;
private final FlingAnimationUtils mFlingAnimationUtilsClosing;
private final DisplayMetrics mDisplayMetrics;
private Boolean mCapture;
private TouchSession mTouchSession;
@@ -85,40 +90,9 @@ public class BouncerSwipeTouchHandler implements DreamTouchHandler {
private final GestureDetector.OnGestureListener mOnGestureListener =
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
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
float distanceY) {
// Do not handle scroll gestures if not tracking touch events.
if (!mTrack) {
return false;
}
if (mCapture == null) {
// If the user scrolling favors a vertical direction, begin capturing
// scrolls.
@@ -141,8 +115,8 @@ public class BouncerSwipeTouchHandler implements DreamTouchHandler {
// (0).
final float screenTravelPercentage =
Math.abs((e1.getY() - e2.getY()) / mStatusBar.getDisplayHeight());
setPanelExpansion(
mBouncerPresent ? screenTravelPercentage : 1 - screenTravelPercentage);
setPanelExpansion(mStatusBar.isBouncerShowing()
? screenTravelPercentage : 1 - screenTravelPercentage);
return true;
}
@@ -155,6 +129,7 @@ public class BouncerSwipeTouchHandler implements DreamTouchHandler {
@Inject
public BouncerSwipeTouchHandler(
DisplayMetrics displayMetrics,
StatusBarKeyguardViewManager statusBarKeyguardViewManager,
StatusBar statusBar,
NotificationShadeWindowController notificationShadeWindowController,
@@ -165,6 +140,7 @@ public class BouncerSwipeTouchHandler implements DreamTouchHandler {
@Named(SWIPE_TO_BOUNCER_FLING_ANIMATION_UTILS_OPENING)
FlingAnimationUtils flingAnimationUtilsClosing,
@Named(SWIPE_TO_BOUNCER_START_REGION) float swipeRegionPercentage) {
mDisplayMetrics = displayMetrics;
mStatusBar = statusBar;
mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
mNotificationShadeWindowController = notificationShadeWindowController;
@@ -175,6 +151,21 @@ public class BouncerSwipeTouchHandler implements DreamTouchHandler {
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
public void onSessionStart(TouchSession session) {
mVelocityTracker = mVelocityTrackerFactory.obtain();
@@ -202,7 +193,9 @@ public class BouncerSwipeTouchHandler implements DreamTouchHandler {
final MotionEvent motionEvent = (MotionEvent) event;
switch(motionEvent.getAction()) {
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
mTouchSession.pop();
// If we are not capturing any input, there is no need to consider animating to
// finish transition.
if (mCapture == null || !mCapture) {
@@ -226,7 +219,6 @@ public class BouncerSwipeTouchHandler implements DreamTouchHandler {
if (expansion == KeyguardBouncer.EXPANSION_HIDDEN) {
mStatusBarKeyguardViewManager.reset(false);
}
mTouchSession.pop();
break;
default:
mVelocityTracker.addMovement(motionEvent);

View File

@@ -16,6 +16,7 @@
package com.android.systemui.dreams.touch;
import android.graphics.Region;
import android.view.GestureDetector;
import android.view.InputEvent;
import android.view.MotionEvent;
@@ -34,6 +35,7 @@ import com.android.systemui.shared.system.InputChannelCompat;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
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 DreamOverlayTouchMonitor}. It enables the monitor to access the associated listeners
@@ -146,6 +152,11 @@ public class DreamOverlayTouchMonitor {
return mTouchMonitor.pop(this);
}
@Override
public int getActiveSessionCount() {
return mTouchMonitor.getSessionCount();
}
/**
* Returns the active listeners to receive touch events.
*/
@@ -229,12 +240,39 @@ public class DreamOverlayTouchMonitor {
public void onInputEvent(InputEvent ev) {
// No Active sessions are receiving touches. Create sessions for each listener
if (mActiveTouchSessions.isEmpty()) {
final HashMap<DreamTouchHandler, DreamTouchHandler.TouchSession> sessionMap =
new HashMap<>();
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 =
new TouchSessionImpl(DreamOverlayTouchMonitor.this, null);
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.

View File

@@ -16,6 +16,7 @@
package com.android.systemui.dreams.touch;
import android.graphics.Region;
import android.view.GestureDetector;
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.
*/
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 org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyFloat;
@@ -27,12 +26,14 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.animation.ValueAnimator;
import android.graphics.Rect;
import android.graphics.Region;
import android.testing.AndroidTestingRunner;
import android.util.DisplayMetrics;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import androidx.test.filters.FlakyTest;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
@@ -51,8 +52,6 @@ import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import java.util.Random;
@SmallTest
@RunWith(AndroidTestingRunner.class)
public class BouncerSwipeTouchHandlerTest extends SysuiTestCase {
@@ -89,13 +88,20 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase {
@Mock
VelocityTracker mVelocityTracker;
final DisplayMetrics mDisplayMetrics = new DisplayMetrics();
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
public void setup() {
mDisplayMetrics.widthPixels = SCREEN_WIDTH_PX;
mDisplayMetrics.heightPixels = SCREEN_HEIGHT_PX;
MockitoAnnotations.initMocks(this);
mTouchHandler = new BouncerSwipeTouchHandler(
mDisplayMetrics,
mStatusBarKeyguardViewManager,
mStatusBar,
mNotificationShadeWindowController,
@@ -104,23 +110,29 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase {
mFlingAnimationUtils,
mFlingAnimationUtilsClosing,
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(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.
*/
@FlakyTest
@Test
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);
verify(mNotificationShadeWindowController).setForcePluginOpen(eq(true), any());
ArgumentCaptor<InputChannelCompat.InputEventListener> eventListenerCaptor =
@@ -130,34 +142,12 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase {
verify(mTouchSession).registerGestureListener(gestureListenerCaptor.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
beginValidSwipe(gestureListenerCaptor.getValue());
assertThat(gestureListenerCaptor.getValue()
.onScroll(Mockito.mock(MotionEvent.class),
Mockito.mock(MotionEvent.class),
random.nextFloat(),
random.nextFloat())).isTrue();
1,
2)).isTrue();
}
/**
@@ -170,8 +160,6 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase {
ArgumentCaptor.forClass(GestureDetector.OnGestureListener.class);
verify(mTouchSession).registerGestureListener(gestureListenerCaptor.capture());
beginValidSwipe(gestureListenerCaptor.getValue());
final float scrollAmount = .3f;
final float distanceY = SCREEN_HEIGHT_PX * scrollAmount;
@@ -203,8 +191,6 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase {
when(mVelocityTracker.getYVelocity()).thenReturn(velocityY);
beginValidSwipe(gestureListenerCaptor.getValue());
final float distanceY = SCREEN_HEIGHT_PX * position;
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.anyBoolean;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.graphics.Rect;
import android.graphics.Region;
import android.testing.AndroidTestingRunner;
import android.util.Pair;
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
public void testInputEventPropagation() {
final DreamTouchHandler touchHandler = Mockito.mock(DreamTouchHandler.class);