summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--services/accessibility/java/com/android/server/accessibility/FullScreenMagnificationGestureHandler.java52
-rw-r--r--services/accessibility/java/com/android/server/accessibility/gestures/GestureUtils.java22
-rw-r--r--services/tests/servicestests/src/com/android/server/accessibility/FullScreenMagnificationGestureHandlerTest.java192
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;
+ }
+ }
}