summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/java/android/accessibilityservice/AccessibilityGestureEvent.java4
-rw-r--r--core/java/android/accessibilityservice/AccessibilityService.java12
-rw-r--r--services/accessibility/java/com/android/server/accessibility/gestures/AccessibilityGestureDetector.java640
-rw-r--r--services/accessibility/java/com/android/server/accessibility/gestures/GestureManifold.java232
-rw-r--r--services/accessibility/java/com/android/server/accessibility/gestures/GestureMatcher.java371
-rw-r--r--services/accessibility/java/com/android/server/accessibility/gestures/MultiTap.java145
-rw-r--r--services/accessibility/java/com/android/server/accessibility/gestures/MultiTapAndHold.java51
-rw-r--r--services/accessibility/java/com/android/server/accessibility/gestures/Swipe.java430
-rw-r--r--services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java16
-rw-r--r--services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java7
-rw-r--r--services/tests/servicestests/src/com/android/server/accessibility/gestures/GestureManifoldTest.java (renamed from services/tests/servicestests/src/com/android/server/accessibility/gestures/AccessibilityGestureDetectorTest.java)52
-rw-r--r--services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchExplorerTest.java4
12 files changed, 1290 insertions, 674 deletions
diff --git a/core/java/android/accessibilityservice/AccessibilityGestureEvent.java b/core/java/android/accessibilityservice/AccessibilityGestureEvent.java
index 8b62e2f83cff..d82b151e9ce9 100644
--- a/core/java/android/accessibilityservice/AccessibilityGestureEvent.java
+++ b/core/java/android/accessibilityservice/AccessibilityGestureEvent.java
@@ -17,6 +17,8 @@
package android.accessibilityservice;
+import static android.accessibilityservice.AccessibilityService.GESTURE_DOUBLE_TAP;
+import static android.accessibilityservice.AccessibilityService.GESTURE_DOUBLE_TAP_AND_HOLD;
import static android.accessibilityservice.AccessibilityService.GESTURE_SWIPE_DOWN;
import static android.accessibilityservice.AccessibilityService.GESTURE_SWIPE_DOWN_AND_LEFT;
import static android.accessibilityservice.AccessibilityService.GESTURE_SWIPE_DOWN_AND_RIGHT;
@@ -58,6 +60,8 @@ public final class AccessibilityGestureEvent implements Parcelable {
/** @hide */
@IntDef(prefix = { "GESTURE_" }, value = {
+ GESTURE_DOUBLE_TAP,
+ GESTURE_DOUBLE_TAP_AND_HOLD,
GESTURE_SWIPE_UP,
GESTURE_SWIPE_UP_AND_LEFT,
GESTURE_SWIPE_UP_AND_DOWN,
diff --git a/core/java/android/accessibilityservice/AccessibilityService.java b/core/java/android/accessibilityservice/AccessibilityService.java
index 47fdcdeafce9..0f619c8d1f76 100644
--- a/core/java/android/accessibilityservice/AccessibilityService.java
+++ b/core/java/android/accessibilityservice/AccessibilityService.java
@@ -300,6 +300,18 @@ public abstract class AccessibilityService extends Service {
public static final int GESTURE_SWIPE_DOWN_AND_RIGHT = 16;
/**
+ * The user has performed a double tap gesture on the touch screen.
+ * @hide
+ */
+ public static final int GESTURE_DOUBLE_TAP = 17;
+
+ /**
+ * The user has performed a double tap and hold gesture on the touch screen.
+ * @hide
+ */
+ public static final int GESTURE_DOUBLE_TAP_AND_HOLD = 18;
+
+ /**
* The {@link Intent} that must be declared as handled by the service.
*/
public static final String SERVICE_INTERFACE =
diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/AccessibilityGestureDetector.java b/services/accessibility/java/com/android/server/accessibility/gestures/AccessibilityGestureDetector.java
deleted file mode 100644
index 3dfe59e142a6..000000000000
--- a/services/accessibility/java/com/android/server/accessibility/gestures/AccessibilityGestureDetector.java
+++ /dev/null
@@ -1,640 +0,0 @@
-/*
- ** Copyright 2015, The Android Open Source Project
- **
- ** Licensed under the Apache License, Version 2.0 (the "License");
- ** you may not use this file except in compliance with the License.
- ** You may obtain a copy of the License at
- **
- ** http://www.apache.org/licenses/LICENSE-2.0
- **
- ** Unless required by applicable law or agreed to in writing, software
- ** distributed under the License is distributed on an "AS IS" BASIS,
- ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ** See the License for the specific language governing permissions and
- ** limitations under the License.
- */
-
-package com.android.server.accessibility.gestures;
-
-import android.accessibilityservice.AccessibilityGestureEvent;
-import android.accessibilityservice.AccessibilityService;
-import android.content.Context;
-import android.gesture.GesturePoint;
-import android.graphics.PointF;
-import android.util.Slog;
-import android.util.TypedValue;
-import android.view.GestureDetector;
-import android.view.MotionEvent;
-
-import java.util.ArrayList;
-
-/**
- * This class handles gesture detection for the Touch Explorer. It collects
- * touch events and determines when they match a gesture, as well as when they
- * won't match a gesture. These state changes are then surfaced to mListener.
- */
-class AccessibilityGestureDetector extends GestureDetector.SimpleOnGestureListener {
-
- private static final boolean DEBUG = false;
-
- // Tag for logging received events.
- private static final String LOG_TAG = "AccessibilityGestureDetector";
-
- // Constants for sampling motion event points.
- // We sample based on a minimum distance between points, primarily to improve accuracy by
- // reducing noisy minor changes in direction.
- private static final float MIN_INCHES_BETWEEN_SAMPLES = 0.1f;
- private final float mMinPixelsBetweenSamplesX;
- private final float mMinPixelsBetweenSamplesY;
-
- // Constants for separating gesture segments
- private static final float ANGLE_THRESHOLD = 0.0f;
-
- // Constants for line segment directions
- private static final int LEFT = 0;
- private static final int RIGHT = 1;
- private static final int UP = 2;
- private static final int DOWN = 3;
- private static final int[][] DIRECTIONS_TO_GESTURE_ID = {
- {
- AccessibilityService.GESTURE_SWIPE_LEFT,
- AccessibilityService.GESTURE_SWIPE_LEFT_AND_RIGHT,
- AccessibilityService.GESTURE_SWIPE_LEFT_AND_UP,
- AccessibilityService.GESTURE_SWIPE_LEFT_AND_DOWN
- },
- {
- AccessibilityService.GESTURE_SWIPE_RIGHT_AND_LEFT,
- AccessibilityService.GESTURE_SWIPE_RIGHT,
- AccessibilityService.GESTURE_SWIPE_RIGHT_AND_UP,
- AccessibilityService.GESTURE_SWIPE_RIGHT_AND_DOWN
- },
- {
- AccessibilityService.GESTURE_SWIPE_UP_AND_LEFT,
- AccessibilityService.GESTURE_SWIPE_UP_AND_RIGHT,
- AccessibilityService.GESTURE_SWIPE_UP,
- AccessibilityService.GESTURE_SWIPE_UP_AND_DOWN
- },
- {
- AccessibilityService.GESTURE_SWIPE_DOWN_AND_LEFT,
- AccessibilityService.GESTURE_SWIPE_DOWN_AND_RIGHT,
- AccessibilityService.GESTURE_SWIPE_DOWN_AND_UP,
- AccessibilityService.GESTURE_SWIPE_DOWN
- }
- };
-
-
- /**
- * Listener functions are called as a result of onMoveEvent(). The current
- * MotionEvent in the context of these functions is the event passed into
- * onMotionEvent.
- */
- public interface Listener {
- /**
- * Called when the user has performed a double tap and then held down
- * the second tap.
- *
- * @param event The most recent MotionEvent received.
- * @param policyFlags The policy flags of the most recent event.
- */
- void onDoubleTapAndHold(MotionEvent event, int policyFlags);
-
- /**
- * Called when the user lifts their finger on the second tap of a double
- * tap.
- *
- * @param event The most recent MotionEvent received.
- * @param policyFlags The policy flags of the most recent event.
- *
- * @return true if the event is consumed, else false
- */
- boolean onDoubleTap(MotionEvent event, int policyFlags);
-
- /**
- * Called when the system has decided the event stream is a gesture.
- *
- * @return true if the event is consumed, else false
- */
- boolean onGestureStarted();
-
- /**
- * Called when an event stream is recognized as a gesture.
- *
- * @param gestureEvent Information about the gesture.
- *
- * @return true if the event is consumed, else false
- */
- boolean onGestureCompleted(AccessibilityGestureEvent gestureEvent);
-
- /**
- * Called when the system has decided an event stream doesn't match any
- * known gesture.
- *
- * @param event The most recent MotionEvent received.
- * @param policyFlags The policy flags of the most recent event.
- *
- * @return true if the event is consumed, else false
- */
- public boolean onGestureCancelled(MotionEvent event, int policyFlags);
- }
-
- private final Listener mListener;
- private final Context mContext; // Retained for on-demand construction of GestureDetector.
- private final GestureDetector mGestureDetector; // Double-tap detector.
-
- // Indicates that a single tap has occurred.
- private boolean mFirstTapDetected;
-
- // Indicates that the down event of a double tap has occured.
- private boolean mDoubleTapDetected;
-
- // Indicates that motion events are being collected to match a gesture.
- private boolean mRecognizingGesture;
-
- // Indicates that we've collected enough data to be sure it could be a
- // gesture.
- private boolean mGestureStarted;
-
- // Indicates that motion events from the second pointer are being checked
- // for a double tap.
- private boolean mSecondFingerDoubleTap;
-
- // Tracks the most recent time where ACTION_POINTER_DOWN was sent for the
- // second pointer.
- private long mSecondPointerDownTime;
-
- // Policy flags of the previous event.
- private int mPolicyFlags;
-
- // These values track the previous point that was saved to use for gesture
- // detection. They are only updated when the user moves more than the
- // recognition threshold.
- private float mPreviousGestureX;
- private float mPreviousGestureY;
-
- // These values track the previous point that was used to determine if there
- // was a transition into or out of gesture detection. They are updated when
- // the user moves more than the detection threshold.
- private float mBaseX;
- private float mBaseY;
- private long mBaseTime;
-
- // This is the calculated movement threshold used track if the user is still
- // moving their finger.
- private final float mGestureDetectionThreshold;
-
- // Buffer for storing points for gesture detection.
- private final ArrayList<GesturePoint> mStrokeBuffer = new ArrayList<GesturePoint>(100);
-
- // The minimal delta between moves to add a gesture point.
- private static final int TOUCH_TOLERANCE = 3;
-
- // The minimal score for accepting a predicted gesture.
- private static final float MIN_PREDICTION_SCORE = 2.0f;
-
- // Distance a finger must travel before we decide if it is a gesture or not.
- private static final int GESTURE_CONFIRM_MM = 10;
-
- // Time threshold used to determine if an interaction is a gesture or not.
- // If the first movement of 1cm takes longer than this value, we assume it's
- // a slow movement, and therefore not a gesture.
- //
- // This value was determined by measuring the time for the first 1cm
- // movement when gesturing, and touch exploring. Based on user testing,
- // all gestures started with the initial movement taking less than 100ms.
- // When touch exploring, the first movement almost always takes longer than
- // 200ms.
- private static final long CANCEL_ON_PAUSE_THRESHOLD_NOT_STARTED_MS = 150;
-
- // Time threshold used to determine if a gesture should be cancelled. If
- // the finger takes more than this time to move 1cm, the ongoing gesture is
- // cancelled.
- private static final long CANCEL_ON_PAUSE_THRESHOLD_STARTED_MS = 300;
-
- /**
- * Construct the gesture detector for {@link TouchExplorer}.
- *
- * @see #AccessibilityGestureDetector(Context, Listener, GestureDetector)
- */
- AccessibilityGestureDetector(Context context, Listener listener) {
- this(context, listener, null);
- }
-
- /**
- * Construct the gesture detector for {@link TouchExplorer}.
- *
- * @param context A context handle for accessing resources.
- * @param listener A listener to callback with gesture state or information.
- * @param detector The gesture detector to handle touch event. If null the default one created
- * in place, or for testing purpose.
- */
- AccessibilityGestureDetector(Context context, Listener listener, GestureDetector detector) {
- mListener = listener;
- mContext = context;
-
- // Break the circular dependency between constructors and let the class to be testable
- if (detector == null) {
- mGestureDetector = new GestureDetector(context, this);
- } else {
- mGestureDetector = detector;
- }
- mGestureDetector.setOnDoubleTapListener(this);
- mGestureDetectionThreshold = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_MM, 1,
- context.getResources().getDisplayMetrics()) * GESTURE_CONFIRM_MM;
-
- // Calculate minimum gesture velocity
- final float pixelsPerInchX = context.getResources().getDisplayMetrics().xdpi;
- final float pixelsPerInchY = context.getResources().getDisplayMetrics().ydpi;
- mMinPixelsBetweenSamplesX = MIN_INCHES_BETWEEN_SAMPLES * pixelsPerInchX;
- mMinPixelsBetweenSamplesY = MIN_INCHES_BETWEEN_SAMPLES * pixelsPerInchY;
- }
-
- /**
- * Handle a motion event. If an action is completed, the appropriate
- * callback on mListener is called, and the return value of the callback is
- * passed to the caller.
- *
- * @param event The transformed motion event to be handled.
- * @param rawEvent The raw motion event. It's important that this be the raw
- * event, before any transformations have been applied, so that measurements
- * can be made in physical units.
- * @param policyFlags Policy flags for the event.
- *
- * @return true if the event is consumed, else false
- */
- public boolean onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
- // The accessibility gesture detector is interested in the movements in physical space,
- // so it uses the rawEvent to ignore magnification and other transformations.
- final float x = rawEvent.getX();
- final float y = rawEvent.getY();
- final long time = rawEvent.getEventTime();
-
- mPolicyFlags = policyFlags;
- switch (rawEvent.getActionMasked()) {
- case MotionEvent.ACTION_DOWN:
- mDoubleTapDetected = false;
- mSecondFingerDoubleTap = false;
- mRecognizingGesture = true;
- mGestureStarted = false;
- mPreviousGestureX = x;
- mPreviousGestureY = y;
- mStrokeBuffer.clear();
- mStrokeBuffer.add(new GesturePoint(x, y, time));
-
- mBaseX = x;
- mBaseY = y;
- mBaseTime = time;
- break;
-
- case MotionEvent.ACTION_MOVE:
- if (mRecognizingGesture) {
- final float deltaX = mBaseX - x;
- final float deltaY = mBaseY - y;
- final double moveDelta = Math.hypot(deltaX, deltaY);
- if (moveDelta > mGestureDetectionThreshold) {
- // If the pointer has moved more than the threshold,
- // update the stored values.
- mBaseX = x;
- mBaseY = y;
- mBaseTime = time;
-
- // Since the pointer has moved, this is not a double
- // tap.
- mFirstTapDetected = false;
- mDoubleTapDetected = false;
-
- // If this hasn't been confirmed as a gesture yet, send
- // the event.
- if (!mGestureStarted) {
- mGestureStarted = true;
- return mListener.onGestureStarted();
- }
- } else if (!mFirstTapDetected) {
- // The finger may not move if they are double tapping.
- // In that case, we shouldn't cancel the gesture.
- final long timeDelta = time - mBaseTime;
- final long threshold = mGestureStarted ?
- CANCEL_ON_PAUSE_THRESHOLD_STARTED_MS :
- CANCEL_ON_PAUSE_THRESHOLD_NOT_STARTED_MS;
-
- // If the pointer hasn't moved for longer than the
- // timeout, cancel gesture detection.
- if (timeDelta > threshold) {
- cancelGesture();
- return mListener.onGestureCancelled(rawEvent, policyFlags);
- }
- }
-
- final float dX = Math.abs(x - mPreviousGestureX);
- final float dY = Math.abs(y - mPreviousGestureY);
- if (dX >= mMinPixelsBetweenSamplesX || dY >= mMinPixelsBetweenSamplesY) {
- mPreviousGestureX = x;
- mPreviousGestureY = y;
- mStrokeBuffer.add(new GesturePoint(x, y, time));
- }
- }
- break;
-
- case MotionEvent.ACTION_UP:
- if (mDoubleTapDetected) {
- return finishDoubleTap(rawEvent, policyFlags);
- }
- if (mGestureStarted) {
- final float dX = Math.abs(x - mPreviousGestureX);
- final float dY = Math.abs(y - mPreviousGestureY);
- if (dX >= mMinPixelsBetweenSamplesX || dY >= mMinPixelsBetweenSamplesY) {
- mStrokeBuffer.add(new GesturePoint(x, y, time));
- }
- return recognizeGesture(rawEvent, policyFlags);
- }
- break;
-
- case MotionEvent.ACTION_POINTER_DOWN:
- // Once a second finger is used, we're definitely not
- // recognizing a gesture.
- cancelGesture();
-
- if (rawEvent.getPointerCount() == 2) {
- // If this was the second finger, attempt to recognize double
- // taps on it.
- mSecondFingerDoubleTap = true;
- mSecondPointerDownTime = time;
- } else {
- // If there are more than two fingers down, stop watching
- // for a double tap.
- mSecondFingerDoubleTap = false;
- }
- break;
-
- case MotionEvent.ACTION_POINTER_UP:
- // If we're detecting taps on the second finger, see if we
- // should finish the double tap.
- if (mSecondFingerDoubleTap && mDoubleTapDetected) {
- return finishDoubleTap(rawEvent, policyFlags);
- }
- break;
-
- case MotionEvent.ACTION_CANCEL:
- clear();
- break;
- }
-
- // If we're detecting taps on the second finger, map events from the
- // finger to the first finger.
- if (mSecondFingerDoubleTap) {
- MotionEvent newEvent = mapSecondPointerToFirstPointer(rawEvent);
- if (newEvent == null) {
- return false;
- }
- boolean handled = mGestureDetector.onTouchEvent(newEvent);
- newEvent.recycle();
- return handled;
- }
-
- if (!mRecognizingGesture) {
- return false;
- }
-
- // Pass the transformed event on to the standard gesture detector.
- return mGestureDetector.onTouchEvent(event);
- }
-
- public void clear() {
- mFirstTapDetected = false;
- mDoubleTapDetected = false;
- mSecondFingerDoubleTap = false;
- mGestureStarted = false;
- mGestureDetector.onTouchEvent(MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_CANCEL,
- 0.0f, 0.0f, 0));
- cancelGesture();
- }
-
-
- @Override
- public void onLongPress(MotionEvent e) {
- maybeSendLongPress(e, mPolicyFlags);
- }
-
- @Override
- public boolean onSingleTapUp(MotionEvent event) {
- mFirstTapDetected = true;
- return false;
- }
-
- @Override
- public boolean onSingleTapConfirmed(MotionEvent event) {
- clear();
- return false;
- }
-
- @Override
- public boolean onDoubleTap(MotionEvent event) {
- // The processing of the double tap is deferred until the finger is
- // lifted, so that we can detect a long press on the second tap.
- mDoubleTapDetected = true;
- return false;
- }
-
- private void maybeSendLongPress(MotionEvent event, int policyFlags) {
- if (!mDoubleTapDetected) {
- return;
- }
-
- clear();
-
- mListener.onDoubleTapAndHold(event, policyFlags);
- }
-
- private boolean finishDoubleTap(MotionEvent event, int policyFlags) {
- clear();
-
- return mListener.onDoubleTap(event, policyFlags);
- }
-
- private void cancelGesture() {
- mRecognizingGesture = false;
- mGestureStarted = false;
- mStrokeBuffer.clear();
- }
-
- /**
- * Looks at the sequence of motions in mStrokeBuffer, classifies the gesture, then calls
- * Listener callbacks for success or failure.
- *
- * @param event The raw motion event to pass to the listener callbacks.
- * @param policyFlags Policy flags for the event.
- *
- * @return true if the event is consumed, else false
- */
- private boolean recognizeGesture(MotionEvent event, int policyFlags) {
- if (mStrokeBuffer.size() < 2) {
- return mListener.onGestureCancelled(event, policyFlags);
- }
-
- // Look at mStrokeBuffer and extract 2 line segments, delimited by near-perpendicular
- // direction change.
- // Method: for each sampled motion event, check the angle of the most recent motion vector
- // versus the preceding motion vector, and segment the line if the angle is about
- // 90 degrees.
-
- ArrayList<PointF> path = new ArrayList<>();
- PointF lastDelimiter = new PointF(mStrokeBuffer.get(0).x, mStrokeBuffer.get(0).y);
- path.add(lastDelimiter);
-
- float dX = 0; // Sum of unit vectors from last delimiter to each following point
- float dY = 0;
- int count = 0; // Number of points since last delimiter
- float length = 0; // Vector length from delimiter to most recent point
-
- PointF next = new PointF();
- for (int i = 1; i < mStrokeBuffer.size(); ++i) {
- next = new PointF(mStrokeBuffer.get(i).x, mStrokeBuffer.get(i).y);
- if (count > 0) {
- // Average of unit vectors from delimiter to following points
- float currentDX = dX / count;
- float currentDY = dY / count;
-
- // newDelimiter is a possible new delimiter, based on a vector with length from
- // the last delimiter to the previous point, but in the direction of the average
- // unit vector from delimiter to previous points.
- // Using the averaged vector has the effect of "squaring off the curve",
- // creating a sharper angle between the last motion and the preceding motion from
- // the delimiter. In turn, this sharper angle achieves the splitting threshold
- // even in a gentle curve.
- PointF newDelimiter = new PointF(length * currentDX + lastDelimiter.x,
- length * currentDY + lastDelimiter.y);
-
- // Unit vector from newDelimiter to the most recent point
- float nextDX = next.x - newDelimiter.x;
- float nextDY = next.y - newDelimiter.y;
- float nextLength = (float) Math.sqrt(nextDX * nextDX + nextDY * nextDY);
- nextDX = nextDX / nextLength;
- nextDY = nextDY / nextLength;
-
- // Compare the initial motion direction to the most recent motion direction,
- // and segment the line if direction has changed by about 90 degrees.
- float dot = currentDX * nextDX + currentDY * nextDY;
- if (dot < ANGLE_THRESHOLD) {
- path.add(newDelimiter);
- lastDelimiter = newDelimiter;
- dX = 0;
- dY = 0;
- count = 0;
- }
- }
-
- // Vector from last delimiter to most recent point
- float currentDX = next.x - lastDelimiter.x;
- float currentDY = next.y - lastDelimiter.y;
- length = (float) Math.sqrt(currentDX * currentDX + currentDY * currentDY);
-
- // Increment sum of unit vectors from delimiter to each following point
- count = count + 1;
- dX = dX + currentDX / length;
- dY = dY + currentDY / length;
- }
-
- path.add(next);
- Slog.i(LOG_TAG, "path=" + path.toString());
-
- // Classify line segments, and call Listener callbacks.
- return recognizeGesturePath(event, policyFlags, path);
- }
-
- /**
- * Classifies a pair of line segments, by direction.
- * Calls Listener callbacks for success or failure.
- *
- * @param event The raw motion event to pass to the listener's onGestureCanceled method.
- * @param policyFlags Policy flags for the event.
- * @param path A sequence of motion line segments derived from motion points in mStrokeBuffer.
- *
- * @return true if the event is consumed, else false
- */
- private boolean recognizeGesturePath(MotionEvent event, int policyFlags,
- ArrayList<PointF> path) {
-
- final int displayId = event.getDisplayId();
- if (path.size() == 2) {
- PointF start = path.get(0);
- PointF end = path.get(1);
-
- float dX = end.x - start.x;
- float dY = end.y - start.y;
- int direction = toDirection(dX, dY);
- switch (direction) {
- case LEFT:
- return mListener.onGestureCompleted(
- new AccessibilityGestureEvent(AccessibilityService.GESTURE_SWIPE_LEFT,
- displayId));
- case RIGHT:
- return mListener.onGestureCompleted(
- new AccessibilityGestureEvent(AccessibilityService.GESTURE_SWIPE_RIGHT,
- displayId));
- case UP:
- return mListener.onGestureCompleted(
- new AccessibilityGestureEvent(AccessibilityService.GESTURE_SWIPE_UP,
- displayId));
- case DOWN:
- return mListener.onGestureCompleted(
- new AccessibilityGestureEvent(AccessibilityService.GESTURE_SWIPE_DOWN,
- displayId));
- default:
- // Do nothing.
- }
-
- } else if (path.size() == 3) {
- PointF start = path.get(0);
- PointF mid = path.get(1);
- PointF end = path.get(2);
-
- float dX0 = mid.x - start.x;
- float dY0 = mid.y - start.y;
-
- float dX1 = end.x - mid.x;
- float dY1 = end.y - mid.y;
-
- int segmentDirection0 = toDirection(dX0, dY0);
- int segmentDirection1 = toDirection(dX1, dY1);
- int gestureId = DIRECTIONS_TO_GESTURE_ID[segmentDirection0][segmentDirection1];
- return mListener.onGestureCompleted(
- new AccessibilityGestureEvent(gestureId, displayId));
- }
- // else if (path.size() < 2 || 3 < path.size()) then no gesture recognized.
- return mListener.onGestureCancelled(event, policyFlags);
- }
-
- /** Maps a vector to a dominant direction in set {LEFT, RIGHT, UP, DOWN}. */
- private static int toDirection(float dX, float dY) {
- if (Math.abs(dX) > Math.abs(dY)) {
- // Horizontal
- return (dX < 0) ? LEFT : RIGHT;
- } else {
- // Vertical
- return (dY < 0) ? UP : DOWN;
- }
- }
-
- private MotionEvent mapSecondPointerToFirstPointer(MotionEvent event) {
- // Only map basic events when two fingers are down.
- if (event.getPointerCount() != 2 ||
- (event.getActionMasked() != MotionEvent.ACTION_POINTER_DOWN &&
- event.getActionMasked() != MotionEvent.ACTION_POINTER_UP &&
- event.getActionMasked() != MotionEvent.ACTION_MOVE)) {
- return null;
- }
-
- int action = event.getActionMasked();
-
- if (action == MotionEvent.ACTION_POINTER_DOWN) {
- action = MotionEvent.ACTION_DOWN;
- } else if (action == MotionEvent.ACTION_POINTER_UP) {
- action = MotionEvent.ACTION_UP;
- }
-
- // Map the information from the second pointer to the first.
- return MotionEvent.obtain(mSecondPointerDownTime, event.getEventTime(), action,
- event.getX(1), event.getY(1), event.getPressure(1), event.getSize(1),
- event.getMetaState(), event.getXPrecision(), event.getYPrecision(),
- event.getDeviceId(), event.getEdgeFlags());
- }
-}
diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/GestureManifold.java b/services/accessibility/java/com/android/server/accessibility/gestures/GestureManifold.java
new file mode 100644
index 000000000000..9b7adc883dee
--- /dev/null
+++ b/services/accessibility/java/com/android/server/accessibility/gestures/GestureManifold.java
@@ -0,0 +1,232 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.accessibility.gestures;
+
+import static android.accessibilityservice.AccessibilityService.GESTURE_DOUBLE_TAP;
+import static android.accessibilityservice.AccessibilityService.GESTURE_DOUBLE_TAP_AND_HOLD;
+import static android.accessibilityservice.AccessibilityService.GESTURE_SWIPE_DOWN;
+import static android.accessibilityservice.AccessibilityService.GESTURE_SWIPE_DOWN_AND_LEFT;
+import static android.accessibilityservice.AccessibilityService.GESTURE_SWIPE_DOWN_AND_RIGHT;
+import static android.accessibilityservice.AccessibilityService.GESTURE_SWIPE_DOWN_AND_UP;
+import static android.accessibilityservice.AccessibilityService.GESTURE_SWIPE_LEFT;
+import static android.accessibilityservice.AccessibilityService.GESTURE_SWIPE_LEFT_AND_DOWN;
+import static android.accessibilityservice.AccessibilityService.GESTURE_SWIPE_LEFT_AND_RIGHT;
+import static android.accessibilityservice.AccessibilityService.GESTURE_SWIPE_LEFT_AND_UP;
+import static android.accessibilityservice.AccessibilityService.GESTURE_SWIPE_RIGHT;
+import static android.accessibilityservice.AccessibilityService.GESTURE_SWIPE_RIGHT_AND_DOWN;
+import static android.accessibilityservice.AccessibilityService.GESTURE_SWIPE_RIGHT_AND_LEFT;
+import static android.accessibilityservice.AccessibilityService.GESTURE_SWIPE_RIGHT_AND_UP;
+import static android.accessibilityservice.AccessibilityService.GESTURE_SWIPE_UP;
+import static android.accessibilityservice.AccessibilityService.GESTURE_SWIPE_UP_AND_DOWN;
+import static android.accessibilityservice.AccessibilityService.GESTURE_SWIPE_UP_AND_LEFT;
+import static android.accessibilityservice.AccessibilityService.GESTURE_SWIPE_UP_AND_RIGHT;
+
+import static com.android.server.accessibility.gestures.Swipe.DOWN;
+import static com.android.server.accessibility.gestures.Swipe.LEFT;
+import static com.android.server.accessibility.gestures.Swipe.RIGHT;
+import static com.android.server.accessibility.gestures.Swipe.UP;
+import static com.android.server.accessibility.gestures.TouchExplorer.DEBUG;
+
+import android.accessibilityservice.AccessibilityGestureEvent;
+import android.content.Context;
+import android.os.Handler;
+import android.util.Slog;
+import android.view.MotionEvent;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This class coordinates a series of individual gesture matchers to serve as a unified gesture
+ * detector. Gesture matchers are tied to a single gesture. It calls listener callback functions
+ * when a gesture starts or completes.
+ */
+class GestureManifold implements GestureMatcher.StateChangeListener {
+
+ private static final String LOG_TAG = "GestureManifold";
+
+ private final List<GestureMatcher> mGestures = new ArrayList<>();
+ private final Context mContext;
+ // Handler for performing asynchronous operations.
+ private final Handler mHandler;
+ // Listener to be notified of gesture start and end.
+ private Listener mListener;
+ // Shared state information.
+ private TouchState mState;
+
+ GestureManifold(Context context, Listener listener, TouchState state) {
+ mContext = context;
+ mHandler = new Handler(context.getMainLooper());
+ mListener = listener;
+ mState = state;
+ // Set up gestures.
+ // Start with double tap.
+ mGestures.add(new MultiTap(context, 2, GESTURE_DOUBLE_TAP, this));
+ mGestures.add(new MultiTapAndHold(context, 2, GESTURE_DOUBLE_TAP_AND_HOLD, this));
+ // One-direction swipes.
+ mGestures.add(new Swipe(context, RIGHT, GESTURE_SWIPE_RIGHT, this));
+ mGestures.add(new Swipe(context, LEFT, GESTURE_SWIPE_LEFT, this));
+ mGestures.add(new Swipe(context, UP, GESTURE_SWIPE_UP, this));
+ mGestures.add(new Swipe(context, DOWN, GESTURE_SWIPE_DOWN, this));
+ // Two-direction swipes.
+ mGestures.add(new Swipe(context, LEFT, RIGHT, GESTURE_SWIPE_LEFT_AND_RIGHT, this));
+ mGestures.add(new Swipe(context, LEFT, UP, GESTURE_SWIPE_LEFT_AND_UP, this));
+ mGestures.add(new Swipe(context, LEFT, DOWN, GESTURE_SWIPE_LEFT_AND_DOWN, this));
+ mGestures.add(new Swipe(context, RIGHT, UP, GESTURE_SWIPE_RIGHT_AND_UP, this));
+ mGestures.add(new Swipe(context, RIGHT, DOWN, GESTURE_SWIPE_RIGHT_AND_DOWN, this));
+ mGestures.add(new Swipe(context, RIGHT, LEFT, GESTURE_SWIPE_RIGHT_AND_LEFT, this));
+ mGestures.add(new Swipe(context, DOWN, UP, GESTURE_SWIPE_DOWN_AND_UP, this));
+ mGestures.add(new Swipe(context, DOWN, LEFT, GESTURE_SWIPE_DOWN_AND_LEFT, this));
+ mGestures.add(new Swipe(context, DOWN, RIGHT, GESTURE_SWIPE_DOWN_AND_RIGHT, this));
+ mGestures.add(new Swipe(context, UP, DOWN, GESTURE_SWIPE_UP_AND_DOWN, this));
+ mGestures.add(new Swipe(context, UP, LEFT, GESTURE_SWIPE_UP_AND_LEFT, this));
+ mGestures.add(new Swipe(context, UP, RIGHT, GESTURE_SWIPE_UP_AND_RIGHT, this));
+ }
+
+ /**
+ * Processes a motion event.
+ *
+ * @param event The event as received from the previous entry in the event stream.
+ * @param rawEvent The event without any transformations e.g. magnification.
+ * @param policyFlags
+ * @return True if the event has been appropriately handled by the gesture manifold and related
+ * callback functions, false if it should be handled further by the calling function.
+ */
+ boolean onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
+ if (mState.isClear()) {
+ if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
+ // Sanity safeguard: if touch state is clear, then matchers should always be clear
+ // before processing the next down event.
+ clear();
+ } else {
+ // If for some reason other events come through while in the clear state they could
+ // compromise the state of particular matchers, so we just ignore them.
+ return false;
+ }
+ }
+ for (GestureMatcher matcher : mGestures) {
+ if (matcher.getState() != GestureMatcher.STATE_GESTURE_CANCELED) {
+ if (DEBUG) {
+ Slog.d(LOG_TAG, matcher.toString());
+ }
+ matcher.onMotionEvent(event, rawEvent, policyFlags);
+ if (DEBUG) {
+ Slog.d(LOG_TAG, matcher.toString());
+ }
+ if (matcher.getState() == GestureMatcher.STATE_GESTURE_COMPLETED) {
+ // Here we just clear and return. The actual gesture dispatch is done in
+ // onStateChanged().
+ clear();
+ // No need to process this event any further.
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ public void clear() {
+ for (GestureMatcher matcher : mGestures) {
+ matcher.clear();
+ }
+ }
+
+ /**
+ * Listener that receives notifications of the state of the gesture detector. Listener functions
+ * are called as a result of onMotionEvent(). The current MotionEvent in the context of these
+ * functions is the event passed into onMotionEvent.
+ */
+ public interface Listener {
+ /**
+ * Called when the user has performed a double tap and then held down the second tap.
+ */
+ void onDoubleTapAndHold();
+
+ /**
+ * Called when the user lifts their finger on the second tap of a double tap.
+ * @return true if the event is consumed, else false
+ */
+ boolean onDoubleTap();
+
+ /**
+ * Called when the system has decided the event stream is a gesture.
+ *
+ * @return true if the event is consumed, else false
+ */
+ boolean onGestureStarted();
+
+ /**
+ * Called when an event stream is recognized as a gesture.
+ *
+ * @param gestureEvent Information about the gesture.
+ * @return true if the event is consumed, else false
+ */
+ boolean onGestureCompleted(AccessibilityGestureEvent gestureEvent);
+
+ /**
+ * Called when the system has decided an event stream doesn't match any known gesture.
+ *
+ * @param event The most recent MotionEvent received.
+ * @param policyFlags The policy flags of the most recent event.
+ * @return true if the event is consumed, else false
+ */
+ boolean onGestureCancelled(MotionEvent event, MotionEvent rawEvent, int policyFlags);
+ }
+
+ @Override
+ public void onStateChanged(
+ int gestureId, int state, MotionEvent event, MotionEvent rawEvent, int policyFlags) {
+ if (state == GestureMatcher.STATE_GESTURE_STARTED && !mState.isGestureDetecting()) {
+ mListener.onGestureStarted();
+ } else if (state == GestureMatcher.STATE_GESTURE_COMPLETED) {
+ onGestureCompleted(gestureId);
+ } else if (state == GestureMatcher.STATE_GESTURE_CANCELED && mState.isGestureDetecting()) {
+ // We only want to call the cancelation callback if there are no other pending
+ // detectors.
+ for (GestureMatcher matcher : mGestures) {
+ if (matcher.getState() == GestureMatcher.STATE_GESTURE_STARTED) {
+ return;
+ }
+ }
+ if (DEBUG) {
+ Slog.d(LOG_TAG, "Cancelling.");
+ }
+ mListener.onGestureCancelled(event, rawEvent, policyFlags);
+ }
+ }
+
+ private void onGestureCompleted(int gestureId) {
+ MotionEvent event = mState.getLastReceivedEvent();
+ // Note that gestures that complete immediately call clear() from onMotionEvent.
+ // Gestures that complete on a delay call clear() here.
+ switch (gestureId) {
+ case GESTURE_DOUBLE_TAP:
+ mListener.onDoubleTap();
+ clear();
+ break;
+ case GESTURE_DOUBLE_TAP_AND_HOLD:
+ mListener.onDoubleTapAndHold();
+ clear();
+ break;
+ default:
+ AccessibilityGestureEvent gestureEvent =
+ new AccessibilityGestureEvent(gestureId, event.getDisplayId());
+ mListener.onGestureCompleted(gestureEvent);
+ break;
+ }
+ }
+}
diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/GestureMatcher.java b/services/accessibility/java/com/android/server/accessibility/gestures/GestureMatcher.java
new file mode 100644
index 000000000000..0b30ff57ddde
--- /dev/null
+++ b/services/accessibility/java/com/android/server/accessibility/gestures/GestureMatcher.java
@@ -0,0 +1,371 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.accessibility.gestures;
+
+import static com.android.server.accessibility.gestures.TouchExplorer.DEBUG;
+
+import android.annotation.IntDef;
+import android.os.Handler;
+import android.util.Slog;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+
+/**
+ * This class describes a common base for gesture matchers. A gesture matcher checks a series of
+ * motion events against a single gesture. Coordinating the individual gesture matchers is done by
+ * the GestureManifold. To create a new Gesture, extend this class and override the onDown, onMove,
+ * onUp, etc methods as necessary. If you don't override a method your matcher will do nothing in
+ * response to that type of event. Finally, be sure to give your gesture a name by overriding
+ * getGestureName().
+ */
+abstract class GestureMatcher {
+ // Potential states for this individual gesture matcher.
+ // In STATE_CLEAR, this matcher is accepting new motion events but has not formally signaled
+ // that there is enough data to judge that a gesture has started.
+ static final int STATE_CLEAR = 0;
+ // In STATE_GESTURE_STARTED, this matcher continues to accept motion events and it has signaled
+ // to the gesture manifold that what looks like the specified gesture has started.
+ static final int STATE_GESTURE_STARTED = 1;
+ // In STATE_GESTURE_COMPLETED, this matcher has successfully matched the specified gesture. and
+ // will not accept motion events until it is cleared.
+ static final int STATE_GESTURE_COMPLETED = 2;
+ // In STATE_GESTURE_CANCELED, this matcher will not accept new motion events because it is
+ // impossible that this set of motion events will match the specified gesture.
+ static final int STATE_GESTURE_CANCELED = 3;
+
+ @IntDef({STATE_CLEAR, STATE_GESTURE_STARTED, STATE_GESTURE_COMPLETED, STATE_GESTURE_CANCELED})
+ public @interface State {}
+
+ @State private int mState = STATE_CLEAR;
+ // The id number of the gesture that gets passed to accessibility services.
+ private final int mGestureId;
+ // handler for asynchronous operations like timeouts
+ private final Handler mHandler;
+
+ private final StateChangeListener mListener;
+
+ // Use this to transition to new states after a delay.
+ // e.g. cancel or complete after some timeout.
+ // Convenience functions for tapTimeout and doubleTapTimeout are already defined here.
+ protected final DelayedTransition mDelayedTransition;
+
+ GestureMatcher(int gestureId, Handler handler, StateChangeListener listener) {
+ mGestureId = gestureId;
+ mHandler = handler;
+ mDelayedTransition = new DelayedTransition();
+ mListener = listener;
+ }
+
+ /**
+ * Resets all state information for this matcher. Subclasses that include their own state
+ * information should override this method to reset their own state information and call
+ * super.clear().
+ */
+ protected void clear() {
+ mState = STATE_CLEAR;
+ cancelPendingTransitions();
+ }
+
+ public int getState() {
+ return mState;
+ }
+
+ /**
+ * Transitions to a new state and notifies any listeners. Note that any pending transitions are
+ * canceled.
+ */
+ private void setState(
+ @State int state, MotionEvent event, MotionEvent rawEvent, int policyFlags) {
+ mState = state;
+ cancelPendingTransitions();
+ mListener.onStateChanged(mGestureId, mState, event, rawEvent, policyFlags);
+ }
+
+ /** Indicates that there is evidence to suggest that this gesture has started. */
+ protected final void startGesture(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
+ setState(STATE_GESTURE_STARTED, event, rawEvent, policyFlags);
+ }
+
+ /** Indicates this stream of motion events can no longer match this gesture. */
+ protected final void cancelGesture(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
+ setState(STATE_GESTURE_CANCELED, event, rawEvent, policyFlags);
+ }
+
+ /** Indicates this gesture is completed. */
+ protected final void completeGesture(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
+ setState(STATE_GESTURE_COMPLETED, event, rawEvent, policyFlags);
+ }
+
+ public int getGestureId() {
+ return mGestureId;
+ }
+
+ /**
+ * Process a motion event and attempt to match it to this gesture.
+ *
+ * @param event the event as passed in from the event stream.
+ * @param rawEvent the original un-modified event. Useful for calculating movements in physical
+ * space.
+ * @param policyFlags the policy flags as passed in from the event stream.
+ * @return the state of this matcher.
+ */
+ public final int onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
+ if (mState == STATE_GESTURE_CANCELED || mState == STATE_GESTURE_COMPLETED) {
+ return mState;
+ }
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN:
+ onDown(event, rawEvent, policyFlags);
+ break;
+ case MotionEvent.ACTION_POINTER_DOWN:
+ onPointerDown(event, rawEvent, policyFlags);
+ break;
+ case MotionEvent.ACTION_MOVE:
+ onMove(event, rawEvent, policyFlags);
+ break;
+ case MotionEvent.ACTION_POINTER_UP:
+ onPointerUp(event, rawEvent, policyFlags);
+ break;
+ case MotionEvent.ACTION_UP:
+ onUp(event, rawEvent, policyFlags);
+ break;
+ default:
+ // Cancel because of invalid event.
+ setState(STATE_GESTURE_CANCELED, event, rawEvent, policyFlags);
+ break;
+ }
+ return mState;
+ }
+
+ /**
+ * Matchers override this method to respond to ACTION_DOWN events. ACTION_DOWN events indicate
+ * the first finger has touched the screen. If not overridden the default response is to do
+ * nothing.
+ */
+ protected void onDown(MotionEvent event, MotionEvent rawEvent, int policyFlags) {}
+
+ /**
+ * Matchers override this method to respond to ACTION_POINTER_DOWN events. ACTION_POINTER_DOWN
+ * indicates that more than one finger has touched the screen. If not overridden the default
+ * response is to do nothing.
+ *
+ * @param event the event as passed in from the event stream.
+ * @param rawEvent the original un-modified event. Useful for calculating movements in physical
+ * space.
+ * @param policyFlags the policy flags as passed in from the event stream.
+ */
+ protected void onPointerDown(MotionEvent event, MotionEvent rawEvent, int policyFlags) {}
+
+ /**
+ * Matchers override this method to respond to ACTION_MOVE events. ACTION_MOVE indicates that
+ * one or fingers has moved. If not overridden the default response is to do nothing.
+ *
+ * @param event the event as passed in from the event stream.
+ * @param rawEvent the original un-modified event. Useful for calculating movements in physical
+ * space.
+ * @param policyFlags the policy flags as passed in from the event stream.
+ */
+ protected void onMove(MotionEvent event, MotionEvent rawEvent, int policyFlags) {}
+
+ /**
+ * Matchers override this method to respond to ACTION_POINTER_UP events. ACTION_POINTER_UP
+ * indicates that a finger has lifted from the screen but at least one finger continues to touch
+ * the screen. If not overridden the default response is to do nothing.
+ *
+ * @param event the event as passed in from the event stream.
+ * @param rawEvent the original un-modified event. Useful for calculating movements in physical
+ * space.
+ * @param policyFlags the policy flags as passed in from the event stream.
+ */
+ protected void onPointerUp(MotionEvent event, MotionEvent rawEvent, int policyFlags) {}
+
+ /**
+ * Matchers override this method to respond to ACTION_UP events. ACTION_UP indicates that there
+ * are no more fingers touching the screen. If not overridden the default response is to do
+ * nothing.
+ *
+ * @param event the event as passed in from the event stream.
+ * @param rawEvent the original un-modified event. Useful for calculating movements in physical
+ * space.
+ * @param policyFlags the policy flags as passed in from the event stream.
+ */
+ protected void onUp(MotionEvent event, MotionEvent rawEvent, int policyFlags) {}
+
+ /** Cancels this matcher after the tap timeout. Any pending state transitions are removed. */
+ protected void cancelAfterTapTimeout(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
+ cancelAfter(ViewConfiguration.getTapTimeout(), event, rawEvent, policyFlags);
+ }
+
+ /** Cancels this matcher after the double tap timeout. Any pending cancelations are removed. */
+ protected final void cancelAfterDoubleTapTimeout(
+ MotionEvent event, MotionEvent rawEvent, int policyFlags) {
+ cancelAfter(ViewConfiguration.getDoubleTapTimeout(), event, rawEvent, policyFlags);
+ }
+
+ /**
+ * Cancels this matcher after the specified timeout. Any pending cancelations are removed. Used
+ * to prevent this matcher from accepting motion events until it is cleared.
+ */
+ protected final void cancelAfter(
+ long timeout, MotionEvent event, MotionEvent rawEvent, int policyFlags) {
+ mDelayedTransition.cancel();
+ mDelayedTransition.post(STATE_GESTURE_CANCELED, timeout, event, rawEvent, policyFlags);
+ }
+
+ /** Cancels any delayed transitions between states scheduled for this matcher. */
+ protected final void cancelPendingTransitions() {
+ mDelayedTransition.cancel();
+ }
+
+ /**
+ * Signals that this gesture has been completed after the tap timeout has expired. Used to
+ * ensure that there is no conflict with another gesture or for gestures that explicitly require
+ * a hold.
+ */
+ protected final void completeAfterLongPressTimeout(
+ MotionEvent event, MotionEvent rawEvent, int policyFlags) {
+ completeAfter(ViewConfiguration.getLongPressTimeout(), event, rawEvent, policyFlags);
+ }
+
+ /**
+ * Signals that this gesture has been completed after the tap timeout has expired. Used to
+ * ensure that there is no conflict with another gesture or for gestures that explicitly require
+ * a hold.
+ */
+ protected final void completeAfterTapTimeout(
+ MotionEvent event, MotionEvent rawEvent, int policyFlags) {
+ completeAfter(ViewConfiguration.getTapTimeout(), event, rawEvent, policyFlags);
+ }
+
+ /**
+ * Signals that this gesture has been completed after the specified timeout has expired. Used to
+ * ensure that there is no conflict with another gesture or for gestures that explicitly require
+ * a hold.
+ */
+ protected final void completeAfter(
+ long timeout, MotionEvent event, MotionEvent rawEvent, int policyFlags) {
+ mDelayedTransition.cancel();
+ mDelayedTransition.post(STATE_GESTURE_COMPLETED, timeout, event, rawEvent, policyFlags);
+ }
+
+ /**
+ * Signals that this gesture has been completed after the double-tap timeout has expired. Used
+ * to ensure that there is no conflict with another gesture or for gestures that explicitly
+ * require a hold.
+ */
+ protected final void completeAfterDoubleTapTimeout(
+ MotionEvent event, MotionEvent rawEvent, int policyFlags) {
+ completeAfter(ViewConfiguration.getDoubleTapTimeout(), event, rawEvent, policyFlags);
+ }
+
+ public static String getStateSymbolicName(@State int state) {
+ switch (state) {
+ case STATE_CLEAR:
+ return "STATE_CLEAR";
+ case STATE_GESTURE_STARTED:
+ return "STATE_GESTURE_STARTED";
+ case STATE_GESTURE_COMPLETED:
+ return "STATE_GESTURE_COMPLETED";
+ case STATE_GESTURE_CANCELED:
+ return "STATE_GESTURE_CANCELED";
+ default:
+ return "Unknown state: " + state;
+ }
+ }
+
+ /**
+ * Returns a readable name for this matcher that can be displayed to the user and in system
+ * logs.
+ */
+ abstract String getGestureName();
+
+ /**
+ * Returns a String representation of this matcher. Each matcher can override this method to add
+ * extra state information to the string representation.
+ */
+ public String toString() {
+ return getGestureName() + ":" + getStateSymbolicName(mState);
+ }
+
+ /** This class allows matchers to transition between states on a delay. */
+ protected final class DelayedTransition implements Runnable {
+
+ private static final String LOG_TAG = "GestureMatcher.DelayedTransition";
+ int mTargetState;
+ MotionEvent mEvent;
+ MotionEvent mRawEvent;
+ int mPolicyFlags;
+
+ public void cancel() {
+ // Avoid meaningless debug messages.
+ if (DEBUG && isPending()) {
+ Slog.d(
+ LOG_TAG,
+ getGestureName()
+ + ": canceling delayed transition to "
+ + getStateSymbolicName(mTargetState));
+ }
+ mHandler.removeCallbacks(this);
+ }
+
+ public void post(
+ int state, long delay, MotionEvent event, MotionEvent rawEvent, int policyFlags) {
+ mTargetState = state;
+ mEvent = event;
+ mRawEvent = rawEvent;
+ mPolicyFlags = policyFlags;
+ mHandler.postDelayed(this, delay);
+ if (DEBUG) {
+ Slog.d(
+ LOG_TAG,
+ getGestureName()
+ + ": posting delayed transition to "
+ + getStateSymbolicName(mTargetState));
+ }
+ }
+
+ public boolean isPending() {
+ return mHandler.hasCallbacks(this);
+ }
+
+ public void forceSendAndRemove() {
+ if (isPending()) {
+ run();
+ cancel();
+ }
+ }
+
+ @Override
+ public void run() {
+ if (DEBUG) {
+ Slog.d(
+ LOG_TAG,
+ getGestureName()
+ + ": executing delayed transition to "
+ + getStateSymbolicName(mTargetState));
+ }
+ setState(mTargetState, mEvent, mRawEvent, mPolicyFlags);
+ }
+ }
+
+ /** Interface to allow a class to listen for state changes in a specific gesture matcher */
+ interface StateChangeListener {
+
+ void onStateChanged(
+ int gestureId, int state, MotionEvent event, MotionEvent rawEvent, int policyFlags);
+ }
+}
diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/MultiTap.java b/services/accessibility/java/com/android/server/accessibility/gestures/MultiTap.java
new file mode 100644
index 000000000000..2891c6c294f5
--- /dev/null
+++ b/services/accessibility/java/com/android/server/accessibility/gestures/MultiTap.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.accessibility.gestures;
+
+import android.content.Context;
+import android.os.Handler;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+
+/**
+ * This class matches multi-tap gestures. The number of taps for each instance is specified in the
+ * constructor.
+ */
+class MultiTap extends GestureMatcher {
+
+ // Maximum reasonable number of taps.
+ public static final int MAX_TAPS = 10;
+ final int mTargetTaps;
+ // The acceptable distance between two taps
+ int mDoubleTapSlop;
+ // The acceptable distance the pointer can move and still count as a tap.
+ int mTouchSlop;
+ int mTapTimeout;
+ int mDoubleTapTimeout;
+ int mCurrentTaps;
+ float mBaseX;
+ float mBaseY;
+
+ MultiTap(Context context, int taps, int gesture, GestureMatcher.StateChangeListener listener) {
+ super(gesture, new Handler(context.getMainLooper()), listener);
+ mTargetTaps = taps;
+ mDoubleTapSlop = ViewConfiguration.get(context).getScaledDoubleTapSlop();
+ mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
+ mTapTimeout = ViewConfiguration.getTapTimeout();
+ mDoubleTapTimeout = ViewConfiguration.getDoubleTapTimeout();
+ clear();
+ }
+
+ @Override
+ protected void clear() {
+ mCurrentTaps = 0;
+ mBaseX = Float.NaN;
+ mBaseY = Float.NaN;
+ super.clear();
+ }
+
+ @Override
+ protected void onDown(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
+ cancelAfterTapTimeout(event, rawEvent, policyFlags);
+ if (Float.isNaN(mBaseX) && Float.isNaN(mBaseY)) {
+ mBaseX = event.getX();
+ mBaseY = event.getY();
+ }
+ if (!isInsideSlop(rawEvent, mDoubleTapSlop)) {
+ cancelGesture(event, rawEvent, policyFlags);
+ }
+ mBaseX = event.getX();
+ mBaseY = event.getY();
+ }
+
+ @Override
+ protected void onUp(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
+ cancelAfterDoubleTapTimeout(event, rawEvent, policyFlags);
+ if (!isInsideSlop(rawEvent, mTouchSlop)) {
+ cancelGesture(event, rawEvent, policyFlags);
+ }
+ if (getState() == STATE_GESTURE_STARTED || getState() == STATE_CLEAR) {
+ mCurrentTaps++;
+ if (mCurrentTaps == mTargetTaps) {
+ // Done.
+ completeAfterTapTimeout(event, rawEvent, policyFlags);
+ return;
+ }
+ // Needs more taps.
+ cancelAfterDoubleTapTimeout(event, rawEvent, policyFlags);
+ } else {
+ // Either too many taps or nonsensical event stream.
+ cancelGesture(event, rawEvent, policyFlags);
+ }
+ }
+
+ @Override
+ protected void onMove(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
+ if (!isInsideSlop(rawEvent, mTouchSlop)) {
+ cancelGesture(event, rawEvent, policyFlags);
+ }
+ }
+
+ @Override
+ protected void onPointerDown(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
+ cancelGesture(event, rawEvent, policyFlags);
+ }
+
+ @Override
+ protected void onPointerUp(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
+ cancelGesture(event, rawEvent, policyFlags);
+ }
+
+ @Override
+ public String getGestureName() {
+ switch (mTargetTaps) {
+ case 2:
+ return "Double Tap";
+ case 3:
+ return "Triple Tap";
+ default:
+ return Integer.toString(mTargetTaps) + " Taps";
+ }
+ }
+
+ private boolean isInsideSlop(MotionEvent rawEvent, int slop) {
+ final float deltaX = mBaseX - rawEvent.getX();
+ final float deltaY = mBaseY - rawEvent.getY();
+ if (deltaX == 0 && deltaY == 0) {
+ return true;
+ }
+ final double moveDelta = Math.hypot(deltaX, deltaY);
+ return moveDelta <= slop;
+ }
+
+ @Override
+ public String toString() {
+ return super.toString()
+ + ", Taps:"
+ + mCurrentTaps
+ + ", mBaseX: "
+ + Float.toString(mBaseX)
+ + ", mBaseY: "
+ + Float.toString(mBaseY);
+ }
+}
diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/MultiTapAndHold.java b/services/accessibility/java/com/android/server/accessibility/gestures/MultiTapAndHold.java
new file mode 100644
index 000000000000..6a1f1a546bc2
--- /dev/null
+++ b/services/accessibility/java/com/android/server/accessibility/gestures/MultiTapAndHold.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.accessibility.gestures;
+
+import android.content.Context;
+import android.view.MotionEvent;
+
+/**
+ * This class matches gestures of the form multi-tap and hold. The number of taps for each instance
+ * is specified in the constructor.
+ */
+class MultiTapAndHold extends MultiTap {
+ MultiTapAndHold(
+ Context context, int taps, int gesture, GestureMatcher.StateChangeListener listener) {
+ super(context, taps, gesture, listener);
+ }
+
+ @Override
+ protected void onDown(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
+ super.onDown(event, rawEvent, policyFlags);
+ if (mCurrentTaps + 1 == mTargetTaps) {
+ completeAfterLongPressTimeout(event, rawEvent, policyFlags);
+ }
+ }
+
+ @Override
+ public String getGestureName() {
+ switch (mTargetTaps) {
+ case 2:
+ return "Double Tap and Hold";
+ case 3:
+ return "Triple Tap and Hold";
+ default:
+ return Integer.toString(mTargetTaps) + " Taps and Hold";
+ }
+ }
+}
diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/Swipe.java b/services/accessibility/java/com/android/server/accessibility/gestures/Swipe.java
new file mode 100644
index 000000000000..b246c67944c7
--- /dev/null
+++ b/services/accessibility/java/com/android/server/accessibility/gestures/Swipe.java
@@ -0,0 +1,430 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.accessibility.gestures;
+
+import static com.android.server.accessibility.gestures.TouchExplorer.DEBUG;
+
+import android.content.Context;
+import android.gesture.GesturePoint;
+import android.graphics.PointF;
+import android.os.Handler;
+import android.util.DisplayMetrics;
+import android.util.Slog;
+import android.util.TypedValue;
+import android.view.MotionEvent;
+
+import java.util.ArrayList;
+
+/**
+ * This class is responsible for matching one-finger swipe gestures. Each instance matches one swipe
+ * gesture. A swipe is specified as a series of one or more directions e.g. left, left and up, etc.
+ * At this time swipes with more than two directions are not supported.
+ */
+class Swipe extends GestureMatcher {
+
+ // Direction constants.
+ public static final int LEFT = 0;
+ public static final int RIGHT = 1;
+ public static final int UP = 2;
+ public static final int DOWN = 3;
+ // This is the calculated movement threshold used track if the user is still
+ // moving their finger.
+ private final float mGestureDetectionThreshold;
+
+ // Buffer for storing points for gesture detection.
+ private final ArrayList<GesturePoint> mStrokeBuffer = new ArrayList<GesturePoint>(100);
+
+ // The minimal delta between moves to add a gesture point.
+ private static final int TOUCH_TOLERANCE_PIX = 3;
+
+ // The minimal score for accepting a predicted gesture.
+ private static final float MIN_PREDICTION_SCORE = 2.0f;
+
+ // Distance a finger must travel before we decide if it is a gesture or not.
+ private static final int GESTURE_CONFIRM_CM = 1;
+
+ // Time threshold used to determine if an interaction is a gesture or not.
+ // If the first movement of 1cm takes longer than this value, we assume it's
+ // a slow movement, and therefore not a gesture.
+ //
+ // This value was determined by measuring the time for the first 1cm
+ // movement when gesturing, and touch exploring. Based on user testing,
+ // all gestures started with the initial movement taking less than 100ms.
+ // When touch exploring, the first movement almost always takes longer than
+ // 200ms.
+ private static final long CANCEL_ON_PAUSE_THRESHOLD_NOT_STARTED_MS = 150;
+
+ // Time threshold used to determine if a gesture should be cancelled. If
+ // the finger takes more than this time to move 1cm, the ongoing gesture is
+ // cancelled.
+ private static final long CANCEL_ON_PAUSE_THRESHOLD_STARTED_MS = 300;
+
+ private int[] mDirections;
+ private float mBaseX;
+ private float mBaseY;
+ private long mBaseTime;
+ private float mPreviousGestureX;
+ private float mPreviousGestureY;
+ // Constants for sampling motion event points.
+ // We sample based on a minimum distance between points, primarily to improve accuracy by
+ // reducing noisy minor changes in direction.
+ private static final float MIN_CM_BETWEEN_SAMPLES = 0.25f;
+ private final float mMinPixelsBetweenSamplesX;
+ private final float mMinPixelsBetweenSamplesY;
+
+ // Constants for separating gesture segments
+ private static final float ANGLE_THRESHOLD = 0.0f;
+
+ Swipe(
+ Context context,
+ int direction,
+ int gesture,
+ GestureMatcher.StateChangeListener listener) {
+ this(context, new int[] {direction}, gesture, listener);
+ }
+
+ Swipe(
+ Context context,
+ int direction1,
+ int direction2,
+ int gesture,
+ GestureMatcher.StateChangeListener listener) {
+ this(context, new int[] {direction1, direction2}, gesture, listener);
+ }
+
+ private Swipe(
+ Context context,
+ int[] directions,
+ int gesture,
+ GestureMatcher.StateChangeListener listener) {
+ super(gesture, new Handler(context.getMainLooper()), listener);
+ mDirections = directions;
+ DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
+ mGestureDetectionThreshold =
+ TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_MM, 10, displayMetrics)
+ * GESTURE_CONFIRM_CM;
+ // Calculate minimum gesture velocity
+ final float pixelsPerCmX = displayMetrics.xdpi / 2.54f;
+ final float pixelsPerCmY = displayMetrics.ydpi / 2.54f;
+ mMinPixelsBetweenSamplesX = MIN_CM_BETWEEN_SAMPLES * pixelsPerCmX;
+ mMinPixelsBetweenSamplesY = MIN_CM_BETWEEN_SAMPLES * pixelsPerCmY;
+ clear();
+ }
+
+ @Override
+ protected void clear() {
+ mBaseX = Float.NaN;
+ mBaseY = Float.NaN;
+ mBaseTime = 0;
+ mStrokeBuffer.clear();
+ super.clear();
+ }
+
+ @Override
+ protected void onDown(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
+ cancelAfterDelay(event, rawEvent, policyFlags);
+ if (Float.isNaN(mBaseX) && Float.isNaN(mBaseY)) {
+ mBaseX = rawEvent.getX();
+ mBaseY = rawEvent.getY();
+ mBaseTime = event.getEventTime();
+ mPreviousGestureX = mBaseX;
+ mPreviousGestureY = mBaseY;
+ }
+ // Otherwise do nothing because this event doesn't make sense in the middle of a gesture.
+ }
+
+ @Override
+ protected void onMove(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
+ final float x = rawEvent.getX();
+ final float y = rawEvent.getY();
+ final long time = event.getEventTime();
+ final float dX = Math.abs(x - mPreviousGestureX);
+ final float dY = Math.abs(y - mPreviousGestureY);
+ final long timeDelta = time - mBaseTime;
+ final double moveDelta = Math.hypot(Math.abs(x - mBaseX), Math.abs(y - mBaseY));
+ if (DEBUG) {
+ Slog.d(
+ getGestureName(),
+ "moveDelta:"
+ + Double.toString(moveDelta)
+ + " mGestureDetectionThreshold: "
+ + Float.toString(mGestureDetectionThreshold));
+ }
+ if (getState() == STATE_CLEAR) {
+ if (mStrokeBuffer.size() == 0) {
+ // First, make sure the pointer is going in the right direction.
+ cancelAfterDelay(event, rawEvent, policyFlags);
+ int direction = toDirection(x - mBaseX, y - mBaseY);
+ if (direction != mDirections[0]) {
+ cancelGesture(event, rawEvent, policyFlags);
+ return;
+ } else {
+ // This is confirmed to be some kind of swipe so start tracking points.
+ mStrokeBuffer.add(new GesturePoint(mBaseX, mBaseY, mBaseTime));
+ }
+ }
+ if (moveDelta > mGestureDetectionThreshold) {
+ // If the pointer has moved more than the threshold,
+ // update the stored values.
+ mBaseX = x;
+ mBaseY = y;
+ mBaseTime = time;
+ if (getState() == STATE_CLEAR) {
+ startGesture(event, rawEvent, policyFlags);
+ cancelAfterDelay(event, rawEvent, policyFlags);
+ }
+ }
+ }
+ if (getState() == STATE_GESTURE_STARTED) {
+ if (dX >= mMinPixelsBetweenSamplesX || dY >= mMinPixelsBetweenSamplesY) {
+ mPreviousGestureX = x;
+ mPreviousGestureY = y;
+ mStrokeBuffer.add(new GesturePoint(x, y, time));
+ cancelAfterDelay(event, rawEvent, policyFlags);
+ }
+ }
+ }
+
+ @Override
+ protected void onUp(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
+ if (getState() != STATE_GESTURE_STARTED) {
+ cancelGesture(event, rawEvent, policyFlags);
+ return;
+ }
+
+ final float x = rawEvent.getX();
+ final float y = rawEvent.getY();
+ final long time = event.getEventTime();
+ final float dX = Math.abs(x - mPreviousGestureX);
+ final float dY = Math.abs(y - mPreviousGestureY);
+ if (dX >= mMinPixelsBetweenSamplesX || dY >= mMinPixelsBetweenSamplesY) {
+ mStrokeBuffer.add(new GesturePoint(x, y, time));
+ }
+ recognizeGesture(event, rawEvent, policyFlags);
+ }
+
+ @Override
+ protected void onPointerDown(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
+ cancelGesture(event, rawEvent, policyFlags);
+ }
+
+ @Override
+ protected void onPointerUp(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
+ cancelGesture(event, rawEvent, policyFlags);
+ }
+
+ /**
+ * queues a transition to STATE_GESTURE_CANCEL based on the current state. If we have
+ * transitioned to STATE_GESTURE_STARTED the delay is longer.
+ */
+ private void cancelAfterDelay(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
+ cancelPendingTransitions();
+ switch (getState()) {
+ case STATE_CLEAR:
+ cancelAfter(CANCEL_ON_PAUSE_THRESHOLD_NOT_STARTED_MS, event, rawEvent, policyFlags);
+ break;
+ case STATE_GESTURE_STARTED:
+ cancelAfter(CANCEL_ON_PAUSE_THRESHOLD_STARTED_MS, event, rawEvent, policyFlags);
+ break;
+ default:
+ break;
+ }
+ }
+
+ /**
+ * Looks at the sequence of motions in mStrokeBuffer, classifies the gesture, then calls
+ * Listener callbacks for success or failure.
+ *
+ * @param event The raw motion event to pass to the listener callbacks.
+ * @param policyFlags Policy flags for the event.
+ * @return true if the event is consumed, else false
+ */
+ private void recognizeGesture(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
+ if (mStrokeBuffer.size() < 2) {
+ cancelGesture(event, rawEvent, policyFlags);
+ return;
+ }
+
+ // Look at mStrokeBuffer and extract 2 line segments, delimited by near-perpendicular
+ // direction change.
+ // Method: for each sampled motion event, check the angle of the most recent motion vector
+ // versus the preceding motion vector, and segment the line if the angle is about
+ // 90 degrees.
+
+ ArrayList<PointF> path = new ArrayList<>();
+ PointF lastDelimiter = new PointF(mStrokeBuffer.get(0).x, mStrokeBuffer.get(0).y);
+ path.add(lastDelimiter);
+
+ float dX = 0; // Sum of unit vectors from last delimiter to each following point
+ float dY = 0;
+ int count = 0; // Number of points since last delimiter
+ float length = 0; // Vector length from delimiter to most recent point
+
+ PointF next = new PointF();
+ for (int i = 1; i < mStrokeBuffer.size(); ++i) {
+ next = new PointF(mStrokeBuffer.get(i).x, mStrokeBuffer.get(i).y);
+ if (count > 0) {
+ // Average of unit vectors from delimiter to following points
+ float currentDX = dX / count;
+ float currentDY = dY / count;
+
+ // newDelimiter is a possible new delimiter, based on a vector with length from
+ // the last delimiter to the previous point, but in the direction of the average
+ // unit vector from delimiter to previous points.
+ // Using the averaged vector has the effect of "squaring off the curve",
+ // creating a sharper angle between the last motion and the preceding motion from
+ // the delimiter. In turn, this sharper angle achieves the splitting threshold
+ // even in a gentle curve.
+ PointF newDelimiter =
+ new PointF(
+ length * currentDX + lastDelimiter.x,
+ length * currentDY + lastDelimiter.y);
+
+ // Unit vector from newDelimiter to the most recent point
+ float nextDX = next.x - newDelimiter.x;
+ float nextDY = next.y - newDelimiter.y;
+ float nextLength = (float) Math.sqrt(nextDX * nextDX + nextDY * nextDY);
+ nextDX = nextDX / nextLength;
+ nextDY = nextDY / nextLength;
+
+ // Compare the initial motion direction to the most recent motion direction,
+ // and segment the line if direction has changed by about 90 degrees.
+ float dot = currentDX * nextDX + currentDY * nextDY;
+ if (dot < ANGLE_THRESHOLD) {
+ path.add(newDelimiter);
+ lastDelimiter = newDelimiter;
+ dX = 0;
+ dY = 0;
+ count = 0;
+ }
+ }
+
+ // Vector from last delimiter to most recent point
+ float currentDX = next.x - lastDelimiter.x;
+ float currentDY = next.y - lastDelimiter.y;
+ length = (float) Math.sqrt(currentDX * currentDX + currentDY * currentDY);
+
+ // Increment sum of unit vectors from delimiter to each following point
+ count = count + 1;
+ dX = dX + currentDX / length;
+ dY = dY + currentDY / length;
+ }
+
+ path.add(next);
+ if (DEBUG) {
+ Slog.d(getGestureName(), "path=" + path.toString());
+ }
+ // Classify line segments, and call Listener callbacks.
+ recognizeGesturePath(event, rawEvent, policyFlags, path);
+ }
+
+ /**
+ * Classifies a pair of line segments, by direction. Calls Listener callbacks for success or
+ * failure.
+ *
+ * @param event The raw motion event to pass to the listener's onGestureCanceled method.
+ * @param policyFlags Policy flags for the event.
+ * @param path A sequence of motion line segments derived from motion points in mStrokeBuffer.
+ * @return true if the event is consumed, else false
+ */
+ private void recognizeGesturePath(
+ MotionEvent event, MotionEvent rawEvent, int policyFlags, ArrayList<PointF> path) {
+
+ final int displayId = event.getDisplayId();
+ if (path.size() != mDirections.length + 1) {
+ cancelGesture(event, rawEvent, policyFlags);
+ return;
+ }
+ for (int i = 0; i < path.size() - 1; ++i) {
+ PointF start = path.get(i);
+ PointF end = path.get(i + 1);
+
+ float dX = end.x - start.x;
+ float dY = end.y - start.y;
+ int direction = toDirection(dX, dY);
+ if (direction != mDirections[i]) {
+ if (DEBUG) {
+ Slog.d(
+ getGestureName(),
+ "Found direction "
+ + directionToString(direction)
+ + " when expecting "
+ + directionToString(mDirections[i]));
+ }
+ cancelGesture(event, rawEvent, policyFlags);
+ return;
+ }
+ }
+ if (DEBUG) {
+ Slog.d(getGestureName(), "Completed.");
+ }
+ completeGesture(event, rawEvent, policyFlags);
+ }
+
+ private static int toDirection(float dX, float dY) {
+ if (Math.abs(dX) > Math.abs(dY)) {
+ // Horizontal
+ return (dX < 0) ? LEFT : RIGHT;
+ } else {
+ // Vertical
+ return (dY < 0) ? UP : DOWN;
+ }
+ }
+
+ public static String directionToString(int direction) {
+ switch (direction) {
+ case LEFT:
+ return "left";
+ case RIGHT:
+ return "right";
+ case UP:
+ return "up";
+ case DOWN:
+ return "down";
+ default:
+ return "Unknown Direction";
+ }
+ }
+
+ @Override
+ String getGestureName() {
+ StringBuilder builder = new StringBuilder();
+ builder.append("Swipe ").append(directionToString(mDirections[0]));
+ for (int i = 1; i < mDirections.length; ++i) {
+ builder.append(" and ").append(directionToString(mDirections[i]));
+ }
+ return builder.toString();
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder(super.toString());
+ if (getState() != STATE_GESTURE_CANCELED) {
+ builder.append(", mBaseX: ")
+ .append(mBaseX)
+ .append(", mBaseY: ")
+ .append(mBaseY)
+ .append(", mGestureDetectionThreshold:")
+ .append(mGestureDetectionThreshold)
+ .append(", mMinPixelsBetweenSamplesX:")
+ .append(mMinPixelsBetweenSamplesX)
+ .append(", mMinPixelsBetweenSamplesY:")
+ .append(mMinPixelsBetweenSamplesY);
+ }
+ return builder.toString();
+ }
+}
diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java b/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java
index b62e260aacad..5f4163880366 100644
--- a/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java
+++ b/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java
@@ -59,7 +59,7 @@ import java.util.List;
* @hide
*/
public class TouchExplorer extends BaseEventStreamTransformation
- implements AccessibilityGestureDetector.Listener {
+ implements GestureManifold.Listener {
static final boolean DEBUG = false;
@@ -104,7 +104,7 @@ public class TouchExplorer extends BaseEventStreamTransformation
private final ExitGestureDetectionModeDelayed mExitGestureDetectionModeDelayed;
// Helper to detect gestures.
- private final AccessibilityGestureDetector mGestureDetector;
+ private final GestureManifold mGestureDetector;
// Helper class to track received pointers.
private final TouchState.ReceivedPointerTracker mReceivedPointerTracker;
@@ -142,7 +142,7 @@ public class TouchExplorer extends BaseEventStreamTransformation
* one created in place, or for testing purpose.
*/
public TouchExplorer(Context context, AccessibilityManagerService service,
- AccessibilityGestureDetector detector) {
+ GestureManifold detector) {
mContext = context;
mAms = service;
mState = new TouchState();
@@ -161,7 +161,7 @@ public class TouchExplorer extends BaseEventStreamTransformation
AccessibilityEvent.TYPE_TOUCH_INTERACTION_END,
mDetermineUserIntentTimeout);
if (detector == null) {
- mGestureDetector = new AccessibilityGestureDetector(context, this);
+ mGestureDetector = new GestureManifold(context, this, mState);
} else {
mGestureDetector = detector;
}
@@ -285,7 +285,7 @@ public class TouchExplorer extends BaseEventStreamTransformation
}
@Override
- public void onDoubleTapAndHold(MotionEvent event, int policyFlags) {
+ public void onDoubleTapAndHold() {
// Ignore the event if we aren't touch interacting.
if (!mState.isTouchInteracting()) {
return;
@@ -303,7 +303,7 @@ public class TouchExplorer extends BaseEventStreamTransformation
}
@Override
- public boolean onDoubleTap(MotionEvent event, int policyFlags) {
+ public boolean onDoubleTap() {
if (!mState.isTouchInteracting()) {
return false;
}
@@ -319,7 +319,7 @@ public class TouchExplorer extends BaseEventStreamTransformation
// Announce the end of a new touch interaction.
mDispatcher.sendAccessibilityEvent(AccessibilityEvent.TYPE_TOUCH_INTERACTION_END);
-
+ mSendTouchInteractionEndDelayed.cancel();
// Try to use the standard accessibility API to click
if (!mAms.performActionOnAccessibilityFocusedItem(
AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK)) {
@@ -356,7 +356,7 @@ public class TouchExplorer extends BaseEventStreamTransformation
}
@Override
- public boolean onGestureCancelled(MotionEvent event, int policyFlags) {
+ public boolean onGestureCancelled(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
if (mState.isGestureDetecting()) {
endGestureDetection(event.getActionMasked() == MotionEvent.ACTION_UP);
return true;
diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java b/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java
index f463260a9d02..d23dbbefd325 100644
--- a/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java
+++ b/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java
@@ -71,7 +71,10 @@ public class TouchState {
// Helper class to track received pointers.
// Todo: collapse or hide this class so multiple classes don't modify it.
private final ReceivedPointerTracker mReceivedPointerTracker;
+ // The most recently received motion event.
private MotionEvent mLastReceivedEvent;
+ // The accompanying raw event without any transformations.
+ private MotionEvent mLastReceivedRawEvent;
public TouchState() {
mReceivedPointerTracker = new ReceivedPointerTracker();
@@ -97,6 +100,9 @@ public class TouchState {
if (mLastReceivedEvent != null) {
mLastReceivedEvent.recycle();
}
+ if (mLastReceivedRawEvent != null) {
+ mLastReceivedRawEvent.recycle();
+ }
mLastReceivedEvent = MotionEvent.obtain(rawEvent);
mReceivedPointerTracker.onMotionEvent(rawEvent);
}
@@ -246,7 +252,6 @@ public class TouchState {
// or if it goes up the next one that most recently went down.
private int mPrimaryPointerId;
-
ReceivedPointerTracker() {
clear();
}
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/gestures/AccessibilityGestureDetectorTest.java b/services/tests/servicestests/src/com/android/server/accessibility/gestures/GestureManifoldTest.java
index b7079124fb79..538e2d51e88f 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/gestures/AccessibilityGestureDetectorTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/gestures/GestureManifoldTest.java
@@ -24,22 +24,21 @@ import static org.mockito.Mockito.when;
import android.accessibilityservice.AccessibilityService;
import android.content.Context;
-import android.content.res.Resources;
import android.graphics.Point;
import android.graphics.PointF;
-import android.util.DisplayMetrics;
-import android.view.GestureDetector;
import android.view.MotionEvent;
+import androidx.test.InstrumentationRegistry;
+
import org.junit.Before;
import org.junit.Test;
import java.util.ArrayList;
/**
- * Tests for AccessibilityGestureDetector
+ * Tests for GestureManifold
*/
-public class AccessibilityGestureDetectorTest {
+public class GestureManifoldTest {
// Constants for testRecognizeGesturePath()
private static final PointF PATH_START = new PointF(300f, 300f);
@@ -47,24 +46,21 @@ public class AccessibilityGestureDetectorTest {
private static final long PATH_STEP_MILLISEC = 100;
// Data used by all tests
- private AccessibilityGestureDetector mDetector;
- private AccessibilityGestureDetector.Listener mResultListener;
+ private GestureManifold mManifold;
+ private TouchState mState;
+ private GestureManifold.Listener mResultListener;
@Before
public void setUp() {
- // Construct a mock Context.
- DisplayMetrics displayMetricsMock = mock(DisplayMetrics.class);
- displayMetricsMock.xdpi = 500;
- displayMetricsMock.ydpi = 500;
- Resources mockResources = mock(Resources.class);
- when(mockResources.getDisplayMetrics()).thenReturn(displayMetricsMock);
- Context contextMock = mock(Context.class);
- when(contextMock.getResources()).thenReturn(mockResources);
-
- // Construct a testable AccessibilityGestureDetector.
- mResultListener = mock(AccessibilityGestureDetector.Listener.class);
- GestureDetector doubleTapDetectorMock = mock(GestureDetector.class);
- mDetector = new AccessibilityGestureDetector(contextMock, mResultListener, doubleTapDetectorMock);
+ Context context = InstrumentationRegistry.getContext();
+ // Construct a testable GestureManifold.
+ mResultListener = mock(GestureManifold.Listener.class);
+ mState = new TouchState();
+ mManifold = new GestureManifold(context, mResultListener, mState);
+ // Play the role of touch explorer in updating the shared state.
+ when(mResultListener.onGestureStarted()).thenReturn(onGestureStarted());
+
+
}
@@ -141,8 +137,8 @@ public class AccessibilityGestureDetectorTest {
// For each path step from start (non-inclusive) to end ... add a motion point.
for (int step = 1; step < numSteps; ++step) {
path.add(new PointF(
- (start.x + (stepX * (float) step)),
- (start.y + (stepY * (float) step))));
+ (start.x + (stepX * (float) step)),
+ (start.y + (stepY * (float) step))));
}
}
@@ -170,12 +166,22 @@ public class AccessibilityGestureDetectorTest {
point.x, point.y, 0);
// Send event.
- mDetector.onMotionEvent(event, event, policyFlags);
+ mState.onReceivedMotionEvent(event);
+ mManifold.onMotionEvent(event, event, policyFlags);
eventTimeMs += PATH_STEP_MILLISEC;
+ if (mState.isClear()) {
+ mState.startTouchInteracting();
+ }
}
+ mState.clear();
// Check that correct gesture was recognized.
verify(mResultListener).onGestureCompleted(
argThat(gestureEvent -> gestureEvent.getGestureId() == gestureId));
}
+
+ private boolean onGestureStarted() {
+ mState.startGestureDetecting();
+ return false;
+ }
}
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchExplorerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchExplorerTest.java
index 4b1ec6fe032b..a4ceadb3028b 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchExplorerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchExplorerTest.java
@@ -74,7 +74,7 @@ public class TouchExplorerTest {
private TouchExplorer mTouchExplorer;
private long mLastDownTime = Integer.MIN_VALUE;
- // mock package-private AccessibilityGestureDetector class
+ // mock package-private GestureManifold class
@Rule
public final DexmakerShareClassLoaderRule mDexmakerShareClassLoaderRule =
new DexmakerShareClassLoaderRule();
@@ -108,7 +108,7 @@ public class TouchExplorerTest {
public void setUp() {
Context context = InstrumentationRegistry.getContext();
AccessibilityManagerService ams = new AccessibilityManagerService(context);
- AccessibilityGestureDetector detector = mock(AccessibilityGestureDetector.class);
+ GestureManifold detector = mock(GestureManifold.class);
mCaptor = new EventCaptor();
mTouchExplorer = new TouchExplorer(context, ams, detector);
mTouchExplorer.setNext(mCaptor);