diff options
8 files changed, 625 insertions, 20 deletions
diff --git a/api/current.txt b/api/current.txt index 9f1b8c652ad5..e3e87607ad7c 100644 --- a/api/current.txt +++ b/api/current.txt @@ -2875,9 +2875,17 @@ package android.accessibilityservice { method public boolean takeScreenshot(int, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.graphics.Bitmap>); field public static final int GESTURE_2_FINGER_DOUBLE_TAP = 20; // 0x14 field public static final int GESTURE_2_FINGER_SINGLE_TAP = 19; // 0x13 + field public static final int GESTURE_2_FINGER_SWIPE_DOWN = 26; // 0x1a + field public static final int GESTURE_2_FINGER_SWIPE_LEFT = 27; // 0x1b + field public static final int GESTURE_2_FINGER_SWIPE_RIGHT = 28; // 0x1c + field public static final int GESTURE_2_FINGER_SWIPE_UP = 25; // 0x19 field public static final int GESTURE_2_FINGER_TRIPLE_TAP = 21; // 0x15 field public static final int GESTURE_3_FINGER_DOUBLE_TAP = 23; // 0x17 field public static final int GESTURE_3_FINGER_SINGLE_TAP = 22; // 0x16 + field public static final int GESTURE_3_FINGER_SWIPE_DOWN = 30; // 0x1e + field public static final int GESTURE_3_FINGER_SWIPE_LEFT = 31; // 0x1f + field public static final int GESTURE_3_FINGER_SWIPE_RIGHT = 32; // 0x20 + field public static final int GESTURE_3_FINGER_SWIPE_UP = 29; // 0x1d field public static final int GESTURE_3_FINGER_TRIPLE_TAP = 24; // 0x18 field public static final int GESTURE_DOUBLE_TAP = 17; // 0x11 field public static final int GESTURE_DOUBLE_TAP_AND_HOLD = 18; // 0x12 diff --git a/core/java/android/accessibilityservice/AccessibilityGestureEvent.java b/core/java/android/accessibilityservice/AccessibilityGestureEvent.java index 47fc7e1cdf13..9cf1de93e344 100644 --- a/core/java/android/accessibilityservice/AccessibilityGestureEvent.java +++ b/core/java/android/accessibilityservice/AccessibilityGestureEvent.java @@ -19,9 +19,17 @@ package android.accessibilityservice; import static android.accessibilityservice.AccessibilityService.GESTURE_2_FINGER_DOUBLE_TAP; import static android.accessibilityservice.AccessibilityService.GESTURE_2_FINGER_SINGLE_TAP; +import static android.accessibilityservice.AccessibilityService.GESTURE_2_FINGER_SWIPE_DOWN; +import static android.accessibilityservice.AccessibilityService.GESTURE_2_FINGER_SWIPE_LEFT; +import static android.accessibilityservice.AccessibilityService.GESTURE_2_FINGER_SWIPE_RIGHT; +import static android.accessibilityservice.AccessibilityService.GESTURE_2_FINGER_SWIPE_UP; import static android.accessibilityservice.AccessibilityService.GESTURE_2_FINGER_TRIPLE_TAP; import static android.accessibilityservice.AccessibilityService.GESTURE_3_FINGER_DOUBLE_TAP; import static android.accessibilityservice.AccessibilityService.GESTURE_3_FINGER_SINGLE_TAP; +import static android.accessibilityservice.AccessibilityService.GESTURE_3_FINGER_SWIPE_DOWN; +import static android.accessibilityservice.AccessibilityService.GESTURE_3_FINGER_SWIPE_LEFT; +import static android.accessibilityservice.AccessibilityService.GESTURE_3_FINGER_SWIPE_RIGHT; +import static android.accessibilityservice.AccessibilityService.GESTURE_3_FINGER_SWIPE_UP; import static android.accessibilityservice.AccessibilityService.GESTURE_3_FINGER_TRIPLE_TAP; import static android.accessibilityservice.AccessibilityService.GESTURE_DOUBLE_TAP; import static android.accessibilityservice.AccessibilityService.GESTURE_DOUBLE_TAP_AND_HOLD; @@ -89,7 +97,15 @@ public final class AccessibilityGestureEvent implements Parcelable { GESTURE_SWIPE_RIGHT, GESTURE_SWIPE_RIGHT_AND_UP, GESTURE_SWIPE_RIGHT_AND_LEFT, - GESTURE_SWIPE_RIGHT_AND_DOWN + GESTURE_SWIPE_RIGHT_AND_DOWN, + GESTURE_2_FINGER_SWIPE_DOWN, + GESTURE_2_FINGER_SWIPE_LEFT, + GESTURE_2_FINGER_SWIPE_RIGHT, + GESTURE_2_FINGER_SWIPE_UP, + GESTURE_3_FINGER_SWIPE_DOWN, + GESTURE_3_FINGER_SWIPE_LEFT, + GESTURE_3_FINGER_SWIPE_RIGHT, + GESTURE_3_FINGER_SWIPE_UP }) @Retention(RetentionPolicy.SOURCE) public @interface GestureId {} @@ -167,6 +183,14 @@ public final class AccessibilityGestureEvent implements Parcelable { case GESTURE_SWIPE_UP_AND_LEFT: return "GESTURE_SWIPE_UP_AND_LEFT"; case GESTURE_SWIPE_UP_AND_DOWN: return "GESTURE_SWIPE_UP_AND_DOWN"; case GESTURE_SWIPE_UP_AND_RIGHT: return "GESTURE_SWIPE_UP_AND_RIGHT"; + case GESTURE_2_FINGER_SWIPE_DOWN: return "GESTURE_2_FINGER_SWIPE_DOWN"; + case GESTURE_2_FINGER_SWIPE_LEFT: return "GESTURE_2_FINGER_SWIPE_LEFT"; + case GESTURE_2_FINGER_SWIPE_RIGHT: return "GESTURE_2_FINGER_SWIPE_RIGHT"; + case GESTURE_2_FINGER_SWIPE_UP: return "GESTURE_2_FINGER_SWIPE_UP"; + case GESTURE_3_FINGER_SWIPE_DOWN: return "GESTURE_3_FINGER_SWIPE_DOWN"; + case GESTURE_3_FINGER_SWIPE_LEFT: return "GESTURE_3_FINGER_SWIPE_LEFT"; + case GESTURE_3_FINGER_SWIPE_RIGHT: return "GESTURE_3_FINGER_SWIPE_RIGHT"; + case GESTURE_3_FINGER_SWIPE_UP: return "GESTURE_3_FINGER_SWIPE_UP"; default: return Integer.toHexString(eventType); } } diff --git a/core/java/android/accessibilityservice/AccessibilityService.java b/core/java/android/accessibilityservice/AccessibilityService.java index 27cd2857f38f..2165fb35a0e5 100644 --- a/core/java/android/accessibilityservice/AccessibilityService.java +++ b/core/java/android/accessibilityservice/AccessibilityService.java @@ -349,6 +349,46 @@ public abstract class AccessibilityService extends Service { public static final int GESTURE_3_FINGER_TRIPLE_TAP = 24; /** + * The user has performed a two-finger swipe up gesture on the touch screen. + */ + public static final int GESTURE_2_FINGER_SWIPE_UP = 25; + + /** + * The user has performed a two-finger swipe down gesture on the touch screen. + */ + public static final int GESTURE_2_FINGER_SWIPE_DOWN = 26; + + /** + * The user has performed a two-finger swipe left gesture on the touch screen. + */ + public static final int GESTURE_2_FINGER_SWIPE_LEFT = 27; + + /** + * The user has performed a two-finger swipe right gesture on the touch screen. + */ + public static final int GESTURE_2_FINGER_SWIPE_RIGHT = 28; + + /** + * The user has performed a three-finger swipe up gesture on the touch screen. + */ + public static final int GESTURE_3_FINGER_SWIPE_UP = 29; + + /** + * The user has performed a three-finger swipe down gesture on the touch screen. + */ + public static final int GESTURE_3_FINGER_SWIPE_DOWN = 30; + + /** + * The user has performed a three-finger swipe left gesture on the touch screen. + */ + public static final int GESTURE_3_FINGER_SWIPE_LEFT = 31; + + /** + * The user has performed a three-finger swipe right gesture on the touch screen. + */ + public static final int GESTURE_3_FINGER_SWIPE_RIGHT = 32; + + /** * 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/GestureManifold.java b/services/accessibility/java/com/android/server/accessibility/gestures/GestureManifold.java index 1fe162c86408..5d170d34d77d 100644 --- a/services/accessibility/java/com/android/server/accessibility/gestures/GestureManifold.java +++ b/services/accessibility/java/com/android/server/accessibility/gestures/GestureManifold.java @@ -18,9 +18,17 @@ package com.android.server.accessibility.gestures; import static android.accessibilityservice.AccessibilityService.GESTURE_2_FINGER_DOUBLE_TAP; import static android.accessibilityservice.AccessibilityService.GESTURE_2_FINGER_SINGLE_TAP; +import static android.accessibilityservice.AccessibilityService.GESTURE_2_FINGER_SWIPE_DOWN; +import static android.accessibilityservice.AccessibilityService.GESTURE_2_FINGER_SWIPE_LEFT; +import static android.accessibilityservice.AccessibilityService.GESTURE_2_FINGER_SWIPE_RIGHT; +import static android.accessibilityservice.AccessibilityService.GESTURE_2_FINGER_SWIPE_UP; import static android.accessibilityservice.AccessibilityService.GESTURE_2_FINGER_TRIPLE_TAP; import static android.accessibilityservice.AccessibilityService.GESTURE_3_FINGER_DOUBLE_TAP; import static android.accessibilityservice.AccessibilityService.GESTURE_3_FINGER_SINGLE_TAP; +import static android.accessibilityservice.AccessibilityService.GESTURE_3_FINGER_SWIPE_DOWN; +import static android.accessibilityservice.AccessibilityService.GESTURE_3_FINGER_SWIPE_LEFT; +import static android.accessibilityservice.AccessibilityService.GESTURE_3_FINGER_SWIPE_RIGHT; +import static android.accessibilityservice.AccessibilityService.GESTURE_3_FINGER_SWIPE_UP; import static android.accessibilityservice.AccessibilityService.GESTURE_3_FINGER_TRIPLE_TAP; import static android.accessibilityservice.AccessibilityService.GESTURE_DOUBLE_TAP; import static android.accessibilityservice.AccessibilityService.GESTURE_DOUBLE_TAP_AND_HOLD; @@ -110,6 +118,7 @@ class GestureManifold implements GestureMatcher.StateChangeListener { 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)); + // Set up multi-finger gestures to be enabled later. // Two-finger taps. mMultiFingerGestures.add( new MultiFingerMultiTap(mContext, 2, 1, GESTURE_2_FINGER_SINGLE_TAP, this)); @@ -124,6 +133,24 @@ class GestureManifold implements GestureMatcher.StateChangeListener { new MultiFingerMultiTap(mContext, 3, 2, GESTURE_3_FINGER_DOUBLE_TAP, this)); mMultiFingerGestures.add( new MultiFingerMultiTap(mContext, 3, 3, GESTURE_3_FINGER_TRIPLE_TAP, this)); + // Two-finger swipes. + mMultiFingerGestures.add( + new MultiFingerSwipe(context, 2, DOWN, GESTURE_2_FINGER_SWIPE_DOWN, this)); + mMultiFingerGestures.add( + new MultiFingerSwipe(context, 2, LEFT, GESTURE_2_FINGER_SWIPE_LEFT, this)); + mMultiFingerGestures.add( + new MultiFingerSwipe(context, 2, RIGHT, GESTURE_2_FINGER_SWIPE_RIGHT, this)); + mMultiFingerGestures.add( + new MultiFingerSwipe(context, 2, UP, GESTURE_2_FINGER_SWIPE_UP, this)); + // Three-finger swipes. + mMultiFingerGestures.add( + new MultiFingerSwipe(context, 3, DOWN, GESTURE_3_FINGER_SWIPE_DOWN, this)); + mMultiFingerGestures.add( + new MultiFingerSwipe(context, 3, LEFT, GESTURE_3_FINGER_SWIPE_LEFT, this)); + mMultiFingerGestures.add( + new MultiFingerSwipe(context, 3, RIGHT, GESTURE_3_FINGER_SWIPE_RIGHT, this)); + mMultiFingerGestures.add( + new MultiFingerSwipe(context, 3, UP, GESTURE_3_FINGER_SWIPE_UP, this)); } /** 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 0f5dd08e02b4..ac6748089314 100644 --- a/services/accessibility/java/com/android/server/accessibility/gestures/GestureUtils.java +++ b/services/accessibility/java/com/android/server/accessibility/gestures/GestureUtils.java @@ -8,6 +8,9 @@ import android.view.MotionEvent; */ public final class GestureUtils { + public static int MM_PER_CM = 10; + public static float CM_PER_INCH = 2.54f; + private GestureUtils() { /* cannot be instantiated */ } @@ -85,4 +88,12 @@ public final class GestureUtils { return true; } + + /** + * Gets the index of the pointer that went up or down from a motion event. + */ + public static int getActionIndex(MotionEvent event) { + return (event.getAction() + & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; + } } diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/MultiFingerSwipe.java b/services/accessibility/java/com/android/server/accessibility/gestures/MultiFingerSwipe.java new file mode 100644 index 000000000000..8249239e3602 --- /dev/null +++ b/services/accessibility/java/com/android/server/accessibility/gestures/MultiFingerSwipe.java @@ -0,0 +1,496 @@ +/* + * Copyright (C) 2020 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.view.MotionEvent.INVALID_POINTER_ID; + +import static com.android.server.accessibility.gestures.GestureUtils.MM_PER_CM; +import static com.android.server.accessibility.gestures.GestureUtils.getActionIndex; +import static com.android.server.accessibility.gestures.Swipe.CANCEL_ON_PAUSE_THRESHOLD_NOT_STARTED_MS; +import static com.android.server.accessibility.gestures.Swipe.CANCEL_ON_PAUSE_THRESHOLD_STARTED_MS; +import static com.android.server.accessibility.gestures.Swipe.GESTURE_CONFIRM_CM; +import static com.android.server.accessibility.gestures.TouchExplorer.DEBUG; + +import android.content.Context; +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 android.view.ViewConfiguration; + +import java.util.ArrayList; +import java.util.Arrays; + +/** + * 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 MultiFingerSwipe 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 mGestureDetectionThresholdPixels; + + // Buffer for storing points for gesture detection. + private final ArrayList<PointF>[] mStrokeBuffers; + + // The swipe direction for this matcher. + private int mDirection; + private int[] mPointerIds; + // The starting point of each finger's path in the gesture. + private PointF[] mBase; + // The most recent entry in each finger's gesture path. + private PointF[] mPreviousGesturePoint; + private int mTargetFingerCount; + private int mCurrentFingerCount; + // Whether the appropriate number of fingers have gone down at some point. This is reset only on + // clear. + private boolean mTargetFingerCountReached = false; + // 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; + // The minmimum distance the finger must travel before we evaluate the initial direction of the + // swipe. + // Anything less is still considered a touch. + private int mTouchSlop; + + MultiFingerSwipe( + Context context, + int fingerCount, + int direction, + int gesture, + GestureMatcher.StateChangeListener listener) { + super(gesture, new Handler(context.getMainLooper()), listener); + mTargetFingerCount = fingerCount; + mPointerIds = new int[mTargetFingerCount]; + mBase = new PointF[mTargetFingerCount]; + mPreviousGesturePoint = new PointF[mTargetFingerCount]; + mStrokeBuffers = new ArrayList[mTargetFingerCount]; + mDirection = direction; + DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics(); + mGestureDetectionThresholdPixels = + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_MM, MM_PER_CM, displayMetrics) + * GESTURE_CONFIRM_CM; + // Calculate minimum gesture velocity + final float pixelsPerCmX = displayMetrics.xdpi / GestureUtils.CM_PER_INCH; + final float pixelsPerCmY = displayMetrics.ydpi / GestureUtils.CM_PER_INCH; + mMinPixelsBetweenSamplesX = MIN_CM_BETWEEN_SAMPLES * pixelsPerCmX; + mMinPixelsBetweenSamplesY = MIN_CM_BETWEEN_SAMPLES * pixelsPerCmY; + mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); + clear(); + } + + @Override + protected void clear() { + mTargetFingerCountReached = false; + mCurrentFingerCount = 0; + for (int i = 0; i < mTargetFingerCount; ++i) { + mPointerIds[i] = INVALID_POINTER_ID; + if (mBase[i] == null) { + mBase[i] = new PointF(); + } + mBase[i].x = Float.NaN; + mBase[i].y = Float.NaN; + if (mPreviousGesturePoint[i] == null) { + mPreviousGesturePoint[i] = new PointF(); + } + mPreviousGesturePoint[i].x = Float.NaN; + mPreviousGesturePoint[i].y = Float.NaN; + if (mStrokeBuffers[i] == null) { + mStrokeBuffers[i] = new ArrayList<>(100); + } + mStrokeBuffers[i].clear(); + } + super.clear(); + } + + @Override + protected void onDown(MotionEvent event, MotionEvent rawEvent, int policyFlags) { + if (mCurrentFingerCount > 0) { + cancelGesture(event, rawEvent, policyFlags); + return; + } + mCurrentFingerCount = 1; + final int actionIndex = getActionIndex(rawEvent); + final int pointerId = rawEvent.getPointerId(actionIndex); + int pointerIndex = rawEvent.getPointerCount() - 1; + if (pointerId < 0 || pointerId > rawEvent.getPointerCount() - 1) { + // Nonsensical pointer id. + cancelGesture(event, rawEvent, policyFlags); + return; + } + if (mPointerIds[pointerIndex] != INVALID_POINTER_ID) { + // Inconsistent event stream. + cancelGesture(event, rawEvent, policyFlags); + return; + } + mPointerIds[pointerIndex] = pointerId; + cancelAfterPauseThreshold(event, rawEvent, policyFlags); + if (Float.isNaN(mBase[pointerIndex].x) && Float.isNaN(mBase[pointerIndex].y)) { + final float x = rawEvent.getX(actionIndex); + final float y = rawEvent.getY(actionIndex); + if (x < 0f || y < 0f) { + cancelGesture(event, rawEvent, policyFlags); + return; + } + mBase[pointerIndex].x = x; + mBase[pointerIndex].y = y; + mPreviousGesturePoint[pointerIndex].x = x; + mPreviousGesturePoint[pointerIndex].y = y; + } else { + // This event doesn't make sense in the middle of a gesture. + cancelGesture(event, rawEvent, policyFlags); + return; + } + } + + @Override + protected void onPointerDown(MotionEvent event, MotionEvent rawEvent, int policyFlags) { + if (event.getPointerCount() > mTargetFingerCount) { + cancelGesture(event, rawEvent, policyFlags); + return; + } + mCurrentFingerCount += 1; + if (mCurrentFingerCount != rawEvent.getPointerCount()) { + cancelGesture(event, rawEvent, policyFlags); + return; + } + if (mCurrentFingerCount == mTargetFingerCount) { + mTargetFingerCountReached = true; + } + final int actionIndex = getActionIndex(rawEvent); + final int pointerId = rawEvent.getPointerId(actionIndex); + if (pointerId < 0 || pointerId > rawEvent.getPointerCount() - 1) { + // Nonsensical pointer id. + cancelGesture(event, rawEvent, policyFlags); + return; + } + int pointerIndex = mCurrentFingerCount - 1; + if (mPointerIds[pointerIndex] != INVALID_POINTER_ID) { + // Inconsistent event stream. + cancelGesture(event, rawEvent, policyFlags); + return; + } + mPointerIds[pointerIndex] = pointerId; + cancelAfterPauseThreshold(event, rawEvent, policyFlags); + if (Float.isNaN(mBase[pointerIndex].x) && Float.isNaN(mBase[pointerIndex].y)) { + final float x = rawEvent.getX(actionIndex); + final float y = rawEvent.getY(actionIndex); + if (x < 0f || y < 0f) { + cancelGesture(event, rawEvent, policyFlags); + return; + } + mBase[pointerIndex].x = x; + mBase[pointerIndex].y = y; + mPreviousGesturePoint[pointerIndex].x = x; + mPreviousGesturePoint[pointerIndex].y = y; + } else { + cancelGesture(event, rawEvent, policyFlags); + return; + } + } + + @Override + protected void onPointerUp(MotionEvent event, MotionEvent rawEvent, int policyFlags) { + if (!mTargetFingerCountReached) { + cancelGesture(event, rawEvent, policyFlags); + return; + } + mCurrentFingerCount -= 1; + final int actionIndex = getActionIndex(event); + final int pointerId = event.getPointerId(actionIndex); + if (pointerId < 0 || pointerId > rawEvent.getPointerCount() - 1) { + // Nonsensical pointer id. + cancelGesture(event, rawEvent, policyFlags); + return; + } + final int pointerIndex = Arrays.binarySearch(mPointerIds, pointerId); + if (pointerIndex < 0) { + cancelGesture(event, rawEvent, policyFlags); + return; + } + final float x = rawEvent.getX(actionIndex); + final float y = rawEvent.getY(actionIndex); + if (x < 0f || y < 0f) { + cancelGesture(event, rawEvent, policyFlags); + return; + } + final float dX = Math.abs(x - mPreviousGesturePoint[pointerIndex].x); + final float dY = Math.abs(y - mPreviousGesturePoint[pointerIndex].y); + if (dX >= mMinPixelsBetweenSamplesX || dY >= mMinPixelsBetweenSamplesY) { + mStrokeBuffers[pointerIndex].add(new PointF(x, y)); + } + // We will evaluate all the paths on ACTION_UP. + } + + @Override + protected void onMove(MotionEvent event, MotionEvent rawEvent, int policyFlags) { + for (int pointerIndex = 0; pointerIndex < rawEvent.getPointerCount(); ++pointerIndex) { + if (DEBUG) { + Slog.d(getGestureName(), "Processing move on finger " + pointerIndex); + } + int index = rawEvent.findPointerIndex(mPointerIds[pointerIndex]); + final float x = rawEvent.getX(index); + final float y = rawEvent.getY(index); + if (x < 0f || y < 0f) { + cancelGesture(event, rawEvent, policyFlags); + return; + } + final float dX = Math.abs(x - mPreviousGesturePoint[pointerIndex].x); + final float dY = Math.abs(y - mPreviousGesturePoint[pointerIndex].y); + final double moveDelta = + Math.hypot( + Math.abs(x - mBase[pointerIndex].x), + Math.abs(y - mBase[pointerIndex].y)); + if (DEBUG) { + Slog.d( + getGestureName(), + "moveDelta:" + + Double.toString(moveDelta) + + " mGestureDetectionThreshold: " + + Float.toString(mGestureDetectionThresholdPixels)); + } + if (getState() == STATE_CLEAR) { + if (moveDelta < mTouchSlop) { + // This still counts as a touch not a swipe. + continue; + } else if (mStrokeBuffers[pointerIndex].size() == 0) { + // First, make sure we have the right number of fingers down. + if (mCurrentFingerCount != mTargetFingerCount) { + cancelGesture(event, rawEvent, policyFlags); + return; + } + // Then, make sure the pointer is going in the right direction. + int direction = + toDirection(x - mBase[pointerIndex].x, y - mBase[pointerIndex].y); + if (direction != mDirection) { + cancelGesture(event, rawEvent, policyFlags); + return; + } else { + // This is confirmed to be some kind of swipe so start tracking points. + cancelAfterPauseThreshold(event, rawEvent, policyFlags); + for (int i = 0; i < mTargetFingerCount; ++i) { + mStrokeBuffers[i].add(new PointF(mBase[i])); + } + } + } + if (moveDelta > mGestureDetectionThresholdPixels) { + // Try to cancel if the finger starts to go the wrong way. + // Note that this only works because this matcher assumes one direction. + int direction = + toDirection(x - mBase[pointerIndex].x, y - mBase[pointerIndex].y); + if (direction != mDirection) { + cancelGesture(event, rawEvent, policyFlags); + return; + } + // If the pointer has moved more than the threshold, + // update the stored values. + mBase[pointerIndex].x = x; + mBase[pointerIndex].y = y; + mPreviousGesturePoint[pointerIndex].x = x; + mPreviousGesturePoint[pointerIndex].y = y; + if (getState() == STATE_CLEAR) { + startGesture(event, rawEvent, policyFlags); + cancelAfterPauseThreshold(event, rawEvent, policyFlags); + } + } + } + if (getState() == STATE_GESTURE_STARTED) { + if (dX >= mMinPixelsBetweenSamplesX || dY >= mMinPixelsBetweenSamplesY) { + // Sample every 2.5 MM in order to guard against minor variations in path. + mPreviousGesturePoint[pointerIndex].x = x; + mPreviousGesturePoint[pointerIndex].y = y; + mStrokeBuffers[pointerIndex].add(new PointF(x, y)); + cancelAfterPauseThreshold(event, rawEvent, policyFlags); + } + } + } + } + + @Override + protected void onUp(MotionEvent event, MotionEvent rawEvent, int policyFlags) { + if (getState() != STATE_GESTURE_STARTED) { + cancelGesture(event, rawEvent, policyFlags); + return; + } + mCurrentFingerCount = 0; + final int actionIndex = getActionIndex(event); + final int pointerId = event.getPointerId(actionIndex); + final int pointerIndex = Arrays.binarySearch(mPointerIds, pointerId); + if (pointerIndex < 0) { + cancelGesture(event, rawEvent, policyFlags); + return; + } + final float x = rawEvent.getX(actionIndex); + final float y = rawEvent.getY(actionIndex); + if (x < 0f || y < 0f) { + cancelGesture(event, rawEvent, policyFlags); + return; + } + final float dX = Math.abs(x - mPreviousGesturePoint[pointerIndex].x); + final float dY = Math.abs(y - mPreviousGesturePoint[pointerIndex].y); + if (dX >= mMinPixelsBetweenSamplesX || dY >= mMinPixelsBetweenSamplesY) { + mStrokeBuffers[pointerIndex].add(new PointF(x, y)); + } + recognizeGesture(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 cancelAfterPauseThreshold( + 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 transitions + * to the complete or cancel state depending on the result. + */ + private void recognizeGesture(MotionEvent event, MotionEvent rawEvent, int policyFlags) { + // Check the path of each finger against the specified direction. + // Note that we sample every 2.5 MMm, and the direction matching is extremely tolerant (each + // direction has a 90-degree arch of tolerance) meaning that minor perpendicular movements + // should not create false negatives. + for (int i = 0; i < mTargetFingerCount; ++i) { + if (DEBUG) { + Slog.d(getGestureName(), "Recognizing finger: " + i); + } + if (mStrokeBuffers[i].size() < 2) { + Slog.d(getGestureName(), "Too few points."); + cancelGesture(event, rawEvent, policyFlags); + return; + } + ArrayList<PointF> path = mStrokeBuffers[i]; + + if (DEBUG) { + Slog.d(getGestureName(), "path=" + path.toString()); + } + // Classify line segments, and call Listener callbacks. + if (!recognizeGesturePath(event, rawEvent, policyFlags, path)) { + cancelGesture(event, rawEvent, policyFlags); + return; + } + } + // If we reach this point then all paths match. + completeGesture(event, rawEvent, policyFlags); + } + + /** + * Tests the path of a given finger against the direction specified in this matcher. + * + * @return True if the path matches the specified direction for this matcher, otherwise false. + */ + private boolean recognizeGesturePath( + MotionEvent event, MotionEvent rawEvent, int policyFlags, ArrayList<PointF> path) { + + final int displayId = event.getDisplayId(); + 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 != mDirection) { + if (DEBUG) { + Slog.d( + getGestureName(), + "Found direction " + + directionToString(direction) + + " when expecting " + + directionToString(mDirection)); + } + return false; + } + } + if (DEBUG) { + Slog.d(getGestureName(), "Completed."); + } + return true; + } + + 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(mTargetFingerCount).append("-finger "); + builder.append("Swipe ").append(directionToString(mDirection)); + return builder.toString(); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(super.toString()); + if (getState() != STATE_GESTURE_CANCELED) { + builder.append(", mBase: ") + .append(mBase.toString()) + .append(", mGestureDetectionThreshold:") + .append(mGestureDetectionThresholdPixels) + .append(", mMinPixelsBetweenSamplesX:") + .append(mMinPixelsBetweenSamplesX) + .append(", mMinPixelsBetweenSamplesY:") + .append(mMinPixelsBetweenSamplesY); + } + return builder.toString(); + } +} diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/SecondFingerMultiTap.java b/services/accessibility/java/com/android/server/accessibility/gestures/SecondFingerMultiTap.java index 8a566fcdb48a..ada251f2363c 100644 --- a/services/accessibility/java/com/android/server/accessibility/gestures/SecondFingerMultiTap.java +++ b/services/accessibility/java/com/android/server/accessibility/gestures/SecondFingerMultiTap.java @@ -18,6 +18,8 @@ package com.android.server.accessibility.gestures; import static android.view.MotionEvent.INVALID_POINTER_ID; +import static com.android.server.accessibility.gestures.GestureUtils.getActionIndex; + import android.content.Context; import android.os.Handler; import android.view.MotionEvent; @@ -155,11 +157,6 @@ class SecondFingerMultiTap extends GestureMatcher { return moveDelta <= slop; } - private int getActionIndex(MotionEvent event) { - return event.getAction() - & MotionEvent.ACTION_POINTER_INDEX_MASK << MotionEvent.ACTION_POINTER_INDEX_SHIFT; - } - @Override public String toString() { return super.toString() diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/Swipe.java b/services/accessibility/java/com/android/server/accessibility/gestures/Swipe.java index a285c10cc6ff..0929610eef9c 100644 --- a/services/accessibility/java/com/android/server/accessibility/gestures/Swipe.java +++ b/services/accessibility/java/com/android/server/accessibility/gestures/Swipe.java @@ -16,6 +16,7 @@ package com.android.server.accessibility.gestures; +import static com.android.server.accessibility.gestures.GestureUtils.MM_PER_CM; import static com.android.server.accessibility.gestures.TouchExplorer.DEBUG; import android.content.Context; @@ -44,7 +45,7 @@ class Swipe extends GestureMatcher { 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; + private final float mGestureDetectionThresholdPixels; // Buffer for storing points for gesture detection. private final ArrayList<GesturePoint> mStrokeBuffer = new ArrayList<GesturePoint>(100); @@ -56,7 +57,7 @@ class Swipe extends GestureMatcher { 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; + public 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 @@ -67,12 +68,12 @@ class Swipe extends GestureMatcher { // 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; + public 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; + public static final long CANCEL_ON_PAUSE_THRESHOLD_STARTED_MS = 300; private int[] mDirections; private float mBaseX; @@ -119,8 +120,8 @@ class Swipe extends GestureMatcher { super(gesture, new Handler(context.getMainLooper()), listener); mDirections = directions; DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics(); - mGestureDetectionThreshold = - TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_MM, 10, displayMetrics) + mGestureDetectionThresholdPixels = + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_MM, MM_PER_CM, displayMetrics) * GESTURE_CONFIRM_CM; // Calculate minimum gesture velocity final float pixelsPerCmX = displayMetrics.xdpi / 2.54f; @@ -142,7 +143,7 @@ class Swipe extends GestureMatcher { @Override protected void onDown(MotionEvent event, MotionEvent rawEvent, int policyFlags) { - cancelAfterDelay(event, rawEvent, policyFlags); + cancelAfterPauseThreshold(event, rawEvent, policyFlags); if (Float.isNaN(mBaseX) && Float.isNaN(mBaseY)) { mBaseX = rawEvent.getX(); mBaseY = rawEvent.getY(); @@ -168,7 +169,7 @@ class Swipe extends GestureMatcher { "moveDelta:" + Double.toString(moveDelta) + " mGestureDetectionThreshold: " - + Float.toString(mGestureDetectionThreshold)); + + Float.toString(mGestureDetectionThresholdPixels)); } if (getState() == STATE_CLEAR) { if (moveDelta < mTouchSlop) { @@ -176,7 +177,7 @@ class Swipe extends GestureMatcher { return; } else if (mStrokeBuffer.size() == 0) { // First, make sure the pointer is going in the right direction. - cancelAfterDelay(event, rawEvent, policyFlags); + cancelAfterPauseThreshold(event, rawEvent, policyFlags); int direction = toDirection(x - mBaseX, y - mBaseY); if (direction != mDirections[0]) { cancelGesture(event, rawEvent, policyFlags); @@ -186,7 +187,7 @@ class Swipe extends GestureMatcher { mStrokeBuffer.add(new GesturePoint(mBaseX, mBaseY, mBaseTime)); } } - if (moveDelta > mGestureDetectionThreshold) { + if (moveDelta > mGestureDetectionThresholdPixels) { // If the pointer has moved more than the threshold, // update the stored values. mBaseX = x; @@ -194,7 +195,7 @@ class Swipe extends GestureMatcher { mBaseTime = time; if (getState() == STATE_CLEAR) { startGesture(event, rawEvent, policyFlags); - cancelAfterDelay(event, rawEvent, policyFlags); + cancelAfterPauseThreshold(event, rawEvent, policyFlags); } } } @@ -203,7 +204,7 @@ class Swipe extends GestureMatcher { mPreviousGestureX = x; mPreviousGestureY = y; mStrokeBuffer.add(new GesturePoint(x, y, time)); - cancelAfterDelay(event, rawEvent, policyFlags); + cancelAfterPauseThreshold(event, rawEvent, policyFlags); } } } @@ -240,7 +241,8 @@ class Swipe extends GestureMatcher { * 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) { + private void cancelAfterPauseThreshold( + MotionEvent event, MotionEvent rawEvent, int policyFlags) { cancelPendingTransitions(); switch (getState()) { case STATE_CLEAR: @@ -428,7 +430,7 @@ class Swipe extends GestureMatcher { .append(", mBaseY: ") .append(mBaseY) .append(", mGestureDetectionThreshold:") - .append(mGestureDetectionThreshold) + .append(mGestureDetectionThresholdPixels) .append(", mMinPixelsBetweenSamplesX:") .append(mMinPixelsBetweenSamplesX) .append(", mMinPixelsBetweenSamplesY:") |