diff options
| author | 2020-06-22 21:29:04 +0800 | |
|---|---|---|
| committer | 2020-06-24 13:46:05 +0800 | |
| commit | 2047cd4ba1feebe88ad5eb3f15eaf5f3b3bbe17d (patch) | |
| tree | e2d64cf93d5d57852eda18ceaa05b12fcf626df4 | |
| parent | e658c76b5c1cdc67cf593ad000c5b862e31764fd (diff) | |
Fix the multi-fingers gesture conflict with TouchExplorer
TouchExplorer supports multi-finger gestures from R. However,
FullScreenMagnificationGestureHandler has higher priority of
receiving motion events than TouchExplorer. When the screen is
zoomed in, two pointersdown gesture makes it transiting to
PanningScalingState.
To fix it, We post a tap tmeout to transit to PanningScalingState
when receiving two pointers down. In this duration, any pointers
action will make it transiting to DelegatingState. We also add
a condition that is if the movement of any fingers exceeed the
touchSlope, it will transit to PanningScalingState.
Bug: 159508732
Test: atest FullScreenMagnificationGestureHandlerTest
atest MagnificationGestureHandlerTest
Change-Id: Ic72c0da68a6a4f1714da8d05f743d6218793a5da
3 files changed, 234 insertions, 32 deletions
diff --git a/services/accessibility/java/com/android/server/accessibility/FullScreenMagnificationGestureHandler.java b/services/accessibility/java/com/android/server/accessibility/FullScreenMagnificationGestureHandler.java index afe6238ca38f..b7f8e674f3ba 100644 --- a/services/accessibility/java/com/android/server/accessibility/FullScreenMagnificationGestureHandler.java +++ b/services/accessibility/java/com/android/server/accessibility/FullScreenMagnificationGestureHandler.java @@ -26,6 +26,7 @@ import static android.view.MotionEvent.ACTION_UP; import static com.android.internal.accessibility.util.AccessibilityStatsLogUtils.logMagnificationTripleTap; import static com.android.server.accessibility.gestures.GestureUtils.distance; +import static com.android.server.accessibility.gestures.GestureUtils.distanceClosestPointerToPoint; import static java.lang.Math.abs; import static java.util.Arrays.asList; @@ -37,6 +38,7 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.graphics.PointF; import android.os.Handler; import android.os.Looper; import android.os.Message; @@ -615,6 +617,7 @@ class FullScreenMagnificationGestureHandler extends MagnificationGestureHandler private static final int MESSAGE_ON_TRIPLE_TAP_AND_HOLD = 1; private static final int MESSAGE_TRANSITION_TO_DELEGATING_STATE = 2; + private static final int MESSAGE_TRANSITION_TO_PANNINGSCALING_STATE = 3; final int mLongTapMinDelay; final int mSwipeMinDistance; @@ -626,6 +629,7 @@ class FullScreenMagnificationGestureHandler extends MagnificationGestureHandler private MotionEvent mPreLastDown; private MotionEvent mLastUp; private MotionEvent mPreLastUp; + private PointF mSecondPointerDownLocation = new PointF(Float.NaN, Float.NaN); private long mLastDetectingDownEventTime; @@ -656,6 +660,10 @@ class FullScreenMagnificationGestureHandler extends MagnificationGestureHandler transitionToDelegatingStateAndClear(); } break; + case MESSAGE_TRANSITION_TO_PANNINGSCALING_STATE: { + transitToPanningScalingStateAndClear(); + } + break; default: { throw new IllegalArgumentException("Unknown message type: " + type); } @@ -702,14 +710,20 @@ class FullScreenMagnificationGestureHandler extends MagnificationGestureHandler } break; case ACTION_POINTER_DOWN: { - if (mMagnificationController.isMagnifying(mDisplayId)) { - transitionTo(mPanningScalingState); - clear(); + if (mMagnificationController.isMagnifying(mDisplayId) + && event.getPointerCount() == 2) { + storeSecondPointerDownLocation(event); + mHandler.sendEmptyMessageDelayed(MESSAGE_TRANSITION_TO_PANNINGSCALING_STATE, + ViewConfiguration.getTapTimeout()); } else { transitionToDelegatingStateAndClear(); } } break; + case ACTION_POINTER_UP: { + transitionToDelegatingStateAndClear(); + } + break; case ACTION_MOVE: { if (isFingerDown() && distance(mLastDown, /* move */ event) > mSwipeMinDistance) { @@ -719,11 +733,19 @@ class FullScreenMagnificationGestureHandler extends MagnificationGestureHandler // For convenience, viewport dragging takes precedence // over insta-delegating on 3tap&swipe // (which is a rare combo to be used aside from magnification) - if (isMultiTapTriggered(2 /* taps */)) { + if (isMultiTapTriggered(2 /* taps */) && event.getPointerCount() == 1) { transitionToViewportDraggingStateAndClear(event); + } else if (isMagnifying() && event.getPointerCount() == 2) { + //Primary pointer is swiping, so transit to PanningScalingState + transitToPanningScalingStateAndClear(); } else { transitionToDelegatingStateAndClear(); } + } else if (isMagnifying() && secondPointerDownValid() + && distanceClosestPointerToPoint( + mSecondPointerDownLocation, /* move */ event) > mSwipeMinDistance) { + //Second pointer is swiping, so transit to PanningScalingState + transitToPanningScalingStateAndClear(); } } break; @@ -755,6 +777,21 @@ class FullScreenMagnificationGestureHandler extends MagnificationGestureHandler } } + private void storeSecondPointerDownLocation(MotionEvent event) { + final int index = event.getActionIndex(); + mSecondPointerDownLocation.set(event.getX(index), event.getY(index)); + } + + private boolean secondPointerDownValid() { + return !(Float.isNaN(mSecondPointerDownLocation.x) && Float.isNaN( + mSecondPointerDownLocation.y)); + } + + private void transitToPanningScalingStateAndClear() { + transitionTo(mPanningScalingState); + clear(); + } + public boolean isMultiTapTriggered(int numTaps) { // Shortcut acts as the 2 initial taps @@ -822,11 +859,13 @@ class FullScreenMagnificationGestureHandler extends MagnificationGestureHandler setShortcutTriggered(false); removePendingDelayedMessages(); clearDelayedMotionEvents(); + mSecondPointerDownLocation.set(Float.NaN, Float.NaN); } private void removePendingDelayedMessages() { mHandler.removeMessages(MESSAGE_ON_TRIPLE_TAP_AND_HOLD); mHandler.removeMessages(MESSAGE_TRANSITION_TO_DELEGATING_STATE); + mHandler.removeMessages(MESSAGE_TRANSITION_TO_PANNINGSCALING_STATE); } private void cacheDelayedMotionEvent(MotionEvent event, MotionEvent rawEvent, @@ -890,6 +929,7 @@ class FullScreenMagnificationGestureHandler extends MagnificationGestureHandler transitionTo(mDelegatingState); sendDelayedMotionEvents(); removePendingDelayedMessages(); + mSecondPointerDownLocation.set(Float.NaN, Float.NaN); } private void onTripleTap(MotionEvent up) { @@ -907,6 +947,10 @@ class FullScreenMagnificationGestureHandler extends MagnificationGestureHandler } } + private boolean isMagnifying() { + return mMagnificationController.isMagnifying(mDisplayId); + } + void transitionToViewportDraggingStateAndClear(MotionEvent down) { if (DEBUG_DETECTING) Slog.i(LOG_TAG, "onTripleTapAndHold()"); diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/GestureUtils.java b/services/accessibility/java/com/android/server/accessibility/gestures/GestureUtils.java index ac6748089314..ec3041848356 100644 --- a/services/accessibility/java/com/android/server/accessibility/gestures/GestureUtils.java +++ b/services/accessibility/java/com/android/server/accessibility/gestures/GestureUtils.java @@ -1,5 +1,6 @@ package com.android.server.accessibility.gestures; +import android.graphics.PointF; import android.util.MathUtils; import android.view.MotionEvent; @@ -38,6 +39,27 @@ public final class GestureUtils { return MathUtils.dist(first.getX(), first.getY(), second.getX(), second.getY()); } + /** + * Returns the minimum distance between {@code pointerDown} and each pointer of + * {@link MotionEvent}. + * + * @param pointerDown The action pointer location of the {@link MotionEvent} with + * {@link MotionEvent#ACTION_DOWN} or {@link MotionEvent#ACTION_POINTER_DOWN} + * @param moveEvent The {@link MotionEvent} with {@link MotionEvent#ACTION_MOVE} + * @return the movement of the pointer. + */ + public static double distanceClosestPointerToPoint(PointF pointerDown, MotionEvent moveEvent) { + float movement = Float.MAX_VALUE; + for (int i = 0; i < moveEvent.getPointerCount(); i++) { + final float moveDelta = MathUtils.dist(pointerDown.x, pointerDown.y, moveEvent.getX(i), + moveEvent.getY(i)); + if (movement > moveDelta) { + movement = moveDelta; + } + } + return movement; + } + public static boolean isTimedOut(MotionEvent firstUp, MotionEvent secondUp, int timeout) { final long deltaTime = secondUp.getEventTime() - firstUp.getEventTime(); return (deltaTime >= timeout); diff --git a/services/tests/servicestests/src/com/android/server/accessibility/FullScreenMagnificationGestureHandlerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/FullScreenMagnificationGestureHandlerTest.java index 2007d4fff8c1..1cbee12720b0 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/FullScreenMagnificationGestureHandlerTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/FullScreenMagnificationGestureHandlerTest.java @@ -20,6 +20,7 @@ import static android.view.MotionEvent.ACTION_DOWN; import static android.view.MotionEvent.ACTION_MOVE; import static android.view.MotionEvent.ACTION_POINTER_DOWN; import static android.view.MotionEvent.ACTION_POINTER_UP; +import static android.view.MotionEvent.ACTION_UP; import static com.android.server.testutils.TestUtils.strictMock; @@ -38,11 +39,13 @@ import static org.mockito.Mockito.when; import android.animation.ValueAnimator; import android.annotation.NonNull; import android.content.Context; +import android.graphics.PointF; import android.os.Handler; import android.os.Message; import android.util.DebugUtils; import android.view.InputDevice; import android.view.MotionEvent; +import android.view.ViewConfiguration; import androidx.test.InstrumentationRegistry; import androidx.test.runner.AndroidJUnit4; @@ -56,6 +59,9 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.List; import java.util.function.IntConsumer; /** @@ -106,6 +112,7 @@ public class FullScreenMagnificationGestureHandlerTest { // Co-prime x and y, to potentially catch x-y-swapped errors public static final float DEFAULT_X = 301; public static final float DEFAULT_Y = 299; + public static final PointF DEFAULT_POINT = new PointF(DEFAULT_X, DEFAULT_Y); private static final int DISPLAY_0 = 0; @@ -327,6 +334,107 @@ public class FullScreenMagnificationGestureHandlerTest { }); } + @Test + public void testTwoFingersOneTap_zoomedState_dispatchMotionEvents() { + goFromStateIdleTo(STATE_ZOOMED); + final EventCaptor eventCaptor = new EventCaptor(); + mMgh.setNext(eventCaptor); + + send(downEvent()); + send(pointerEvent(ACTION_POINTER_DOWN, DEFAULT_X * 2, DEFAULT_Y)); + send(pointerEvent(ACTION_POINTER_UP, DEFAULT_X * 2, DEFAULT_Y)); + send(upEvent()); + + assertIn(STATE_ZOOMED); + final List<Integer> expectedActions = new ArrayList(); + expectedActions.add(Integer.valueOf(ACTION_DOWN)); + expectedActions.add(Integer.valueOf(ACTION_POINTER_DOWN)); + expectedActions.add(Integer.valueOf(ACTION_POINTER_UP)); + expectedActions.add(Integer.valueOf(ACTION_UP)); + assertActionsInOrder(eventCaptor.mEvents, expectedActions); + + returnToNormalFrom(STATE_ZOOMED); + } + + @Test + public void testThreeFingersOneTap_zoomedState_dispatchMotionEvents() { + goFromStateIdleTo(STATE_ZOOMED); + final EventCaptor eventCaptor = new EventCaptor(); + mMgh.setNext(eventCaptor); + PointF pointer1 = DEFAULT_POINT; + PointF pointer2 = new PointF(DEFAULT_X * 1.5f, DEFAULT_Y); + PointF pointer3 = new PointF(DEFAULT_X * 2, DEFAULT_Y); + + send(downEvent()); + send(pointerEvent(ACTION_POINTER_DOWN, new PointF[] {pointer1, pointer2})); + send(pointerEvent(ACTION_POINTER_DOWN, new PointF[] {pointer1, pointer2, pointer3})); + send(pointerEvent(ACTION_POINTER_UP, new PointF[] {pointer1, pointer2, pointer3})); + send(pointerEvent(ACTION_POINTER_UP, new PointF[] {pointer1, pointer2, pointer3})); + send(upEvent()); + + assertIn(STATE_ZOOMED); + final List<Integer> expectedActions = new ArrayList(); + expectedActions.add(Integer.valueOf(ACTION_DOWN)); + expectedActions.add(Integer.valueOf(ACTION_POINTER_DOWN)); + expectedActions.add(Integer.valueOf(ACTION_POINTER_DOWN)); + expectedActions.add(Integer.valueOf(ACTION_POINTER_UP)); + expectedActions.add(Integer.valueOf(ACTION_POINTER_UP)); + expectedActions.add(Integer.valueOf(ACTION_UP)); + assertActionsInOrder(eventCaptor.mEvents, expectedActions); + + returnToNormalFrom(STATE_ZOOMED); + } + + @Test + public void testFirstFingerSwipe_TwoPinterDownAndZoomedState_panningState() { + goFromStateIdleTo(STATE_ZOOMED); + PointF pointer1 = DEFAULT_POINT; + PointF pointer2 = new PointF(DEFAULT_X * 1.5f, DEFAULT_Y); + + send(downEvent()); + send(pointerEvent(ACTION_POINTER_DOWN, new PointF[] {pointer1, pointer2})); + //The minimum movement to transit to panningState. + final float sWipeMinDistance = ViewConfiguration.get(mContext).getScaledTouchSlop(); + pointer1.offset(sWipeMinDistance + 1, 0); + send(pointerEvent(ACTION_MOVE, new PointF[] {pointer1, pointer2})); + assertIn(STATE_PANNING); + + assertIn(STATE_PANNING); + returnToNormalFrom(STATE_PANNING); + } + + @Test + public void testSecondFingerSwipe_TwoPinterDownAndZoomedState_panningState() { + goFromStateIdleTo(STATE_ZOOMED); + PointF pointer1 = DEFAULT_POINT; + PointF pointer2 = new PointF(DEFAULT_X * 1.5f, DEFAULT_Y); + + send(downEvent()); + send(pointerEvent(ACTION_POINTER_DOWN, new PointF[] {pointer1, pointer2})); + //The minimum movement to transit to panningState. + final float sWipeMinDistance = ViewConfiguration.get(mContext).getScaledTouchSlop(); + pointer2.offset(sWipeMinDistance + 1, 0); + send(pointerEvent(ACTION_MOVE, new PointF[] {pointer1, pointer2})); + assertIn(STATE_PANNING); + + assertIn(STATE_PANNING); + returnToNormalFrom(STATE_PANNING); + } + + private void assertActionsInOrder(List<MotionEvent> actualEvents, + List<Integer> expectedActions) { + assertTrue(actualEvents.size() == expectedActions.size()); + final int size = actualEvents.size(); + for (int i = 0; i < size; i++) { + final int expectedAction = expectedActions.get(i); + final int actualAction = actualEvents.get(i).getActionMasked(); + assertTrue(String.format( + "%dth action %s is not matched, actual events : %s, ", i, + MotionEvent.actionToString(expectedAction), actualEvents), + actualAction == expectedAction); + } + } + private void assertZoomsImmediatelyOnSwipeFrom(int state) { goFromStateIdleTo(state); swipeAndHold(); @@ -467,6 +575,7 @@ public class FullScreenMagnificationGestureHandlerTest { goFromStateIdleTo(STATE_ZOOMED); send(downEvent()); send(pointerEvent(ACTION_POINTER_DOWN, DEFAULT_X * 2, DEFAULT_Y)); + fastForward(ViewConfiguration.getTapTimeout()); } break; case STATE_SCALING_AND_PANNING: { goFromStateIdleTo(STATE_PANNING); @@ -619,40 +728,67 @@ public class FullScreenMagnificationGestureHandlerTest { MotionEvent.ACTION_UP, x, y, 0)); } + private MotionEvent pointerEvent(int action, float x, float y) { - MotionEvent.PointerProperties defPointerProperties = new MotionEvent.PointerProperties(); - defPointerProperties.id = 0; - defPointerProperties.toolType = MotionEvent.TOOL_TYPE_FINGER; - MotionEvent.PointerProperties pointerProperties = new MotionEvent.PointerProperties(); - pointerProperties.id = 1; - pointerProperties.toolType = MotionEvent.TOOL_TYPE_FINGER; - - MotionEvent.PointerCoords defPointerCoords = new MotionEvent.PointerCoords(); - defPointerCoords.x = DEFAULT_X; - defPointerCoords.y = DEFAULT_Y; - MotionEvent.PointerCoords pointerCoords = new MotionEvent.PointerCoords(); - pointerCoords.x = x; - pointerCoords.y = y; + return pointerEvent(action, new PointF[] {DEFAULT_POINT, new PointF(x, y)}); + } + + private MotionEvent pointerEvent(int action, PointF[] pointersPosition) { + final MotionEvent.PointerProperties[] PointerPropertiesArray = + new MotionEvent.PointerProperties[pointersPosition.length]; + for (int i = 0; i < pointersPosition.length; i++) { + MotionEvent.PointerProperties pointerProperties = new MotionEvent.PointerProperties(); + pointerProperties.id = i; + pointerProperties.toolType = MotionEvent.TOOL_TYPE_FINGER; + PointerPropertiesArray[i] = pointerProperties; + } + + final MotionEvent.PointerCoords[] pointerCoordsArray = + new MotionEvent.PointerCoords[pointersPosition.length]; + for (int i = 0; i < pointersPosition.length; i++) { + MotionEvent.PointerCoords pointerCoords = new MotionEvent.PointerCoords(); + pointerCoords.x = pointersPosition[i].x; + pointerCoords.y = pointersPosition[i].y; + pointerCoordsArray[i] = pointerCoords; + } return MotionEvent.obtain( - /* downTime */ mClock.now(), - /* eventTime */ mClock.now(), - /* action */ action, - /* pointerCount */ 2, - /* pointerProperties */ new MotionEvent.PointerProperties[] { - defPointerProperties, pointerProperties}, - /* pointerCoords */ new MotionEvent.PointerCoords[] { defPointerCoords, pointerCoords }, - /* metaState */ 0, - /* buttonState */ 0, - /* xPrecision */ 1.0f, - /* yPrecision */ 1.0f, - /* deviceId */ 0, - /* edgeFlags */ 0, - /* source */ InputDevice.SOURCE_TOUCHSCREEN, - /* flags */ 0); + /* downTime */ mClock.now(), + /* eventTime */ mClock.now(), + /* action */ action, + /* pointerCount */ pointersPosition.length, + /* pointerProperties */ PointerPropertiesArray, + /* pointerCoords */ pointerCoordsArray, + /* metaState */ 0, + /* buttonState */ 0, + /* xPrecision */ 1.0f, + /* yPrecision */ 1.0f, + /* deviceId */ 0, + /* edgeFlags */ 0, + /* source */ InputDevice.SOURCE_TOUCHSCREEN, + /* flags */ 0); } + private String stateDump() { return "\nCurrent state dump:\n" + mMgh + "\n" + mHandler.getPendingMessages(); } + + private class EventCaptor implements EventStreamTransformation { + List<MotionEvent> mEvents = new ArrayList<>(); + + @Override + public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) { + mEvents.add(event.copy()); + } + + @Override + public void setNext(EventStreamTransformation next) { + } + + @Override + public EventStreamTransformation getNext() { + return null; + } + } } |