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 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);