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