Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 9254b3e3 authored by Zachary Kuznia's avatar Zachary Kuznia
Browse files

Make AccessibilityGestureDetector handle gesture detection start and end.

Change-Id: I2c1861d5d6c5c0dc921e62f03ee6283f1f7a62b6
parent 676ae8c8
Loading
Loading
Loading
Loading
+87 −11
Original line number Diff line number Diff line
@@ -27,6 +27,8 @@ import android.gesture.Prediction;
import android.util.Slog;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.ViewConfiguration;

import com.android.internal.R;

@@ -46,7 +48,9 @@ class AccessibilityGestureDetector extends GestureDetector.SimpleOnGestureListen
    public interface Listener {
        public void onDoubleTapAndHold(MotionEvent event, int policyFlags);
        public boolean onDoubleTap(MotionEvent event, int policyFlags);
        public boolean onGesture(int gestureId);
        public boolean onGestureCompleted(int gestureId);
        public void onGestureStarted();
        public void onGestureCancelled(MotionEvent event, int policyFlags);
    }

    private final Listener mListener;
@@ -64,6 +68,10 @@ class AccessibilityGestureDetector extends GestureDetector.SimpleOnGestureListen
    // 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 mGestureConfirmed;

    // Indicates that motion events from the second pointer are being checked
    // for a double tap.
    private boolean mSecondFingerDoubleTap;
@@ -81,18 +89,41 @@ class AccessibilityGestureDetector extends GestureDetector.SimpleOnGestureListen
    // The Y of the previous event.
    private float mPreviousY;

    // The X of the down event.
    private float mBaseX;

    // The Y of the down event.
    private float mBaseY;

    // Slop between the first and second tap to be a double tap.
    private final int mDoubleTapSlop;

    // The scaled velocity above which we detect gestures.
    private final int mScaledGestureDetectionVelocity;

    // Buffer for storing points for gesture detection.
    private final ArrayList<GesturePoint> mStrokeBuffer = new ArrayList<GesturePoint>(100);

    // Helper to track gesture velocity.
    private final VelocityTracker mVelocityTracker = VelocityTracker.obtain();

    // 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;

    // The velocity above which we detect gestures.  Expressed in DIPs/Second.
    private static final int GESTURE_DETECTION_VELOCITY_DIP = 1000;

    // Constant used to calculate velocity in seconds.
    private static final int VELOCITY_UNITS_SECONDS = 1000;

    AccessibilityGestureDetector(Context context, Listener listener) {
        mListener = listener;

        mDoubleTapSlop = ViewConfiguration.get(context).getScaledDoubleTapSlop();

        mGestureDetector = new GestureDetector(context, this);
        mGestureDetector.setOnDoubleTapListener(this);

@@ -100,9 +131,14 @@ class AccessibilityGestureDetector extends GestureDetector.SimpleOnGestureListen
        mGestureLibrary.setOrientationStyle(8 /* GestureStore.ORIENTATION_SENSITIVE_8 */);
        mGestureLibrary.setSequenceType(GestureStore.SEQUENCE_SENSITIVE);
        mGestureLibrary.load();

        final float density = context.getResources().getDisplayMetrics().density;
        mScaledGestureDetectionVelocity = (int) (GESTURE_DETECTION_VELOCITY_DIP * density);
    }

    public boolean onMotionEvent(MotionEvent event, int policyFlags) {
        mVelocityTracker.addMovement(event);

        final float x = event.getX();
        final float y = event.getY();

@@ -112,14 +148,48 @@ class AccessibilityGestureDetector extends GestureDetector.SimpleOnGestureListen
                mDoubleTapDetected = false;
                mSecondFingerDoubleTap = false;
                mRecognizingGesture = true;
                mGestureConfirmed = false;
                mBaseX = x;
                mBaseY = y;
                mPreviousX = x;
                mPreviousY = y;
                mStrokeBuffer.clear();
                mStrokeBuffer.add(new GesturePoint(x, y, event.getEventTime()));
                mVelocityTracker.clear();
                mVelocityTracker.addMovement(event);
                break;

            case MotionEvent.ACTION_MOVE:
                if (mRecognizingGesture) {
                    if (!mGestureConfirmed) {
                        mVelocityTracker.addMovement(event);
                        // It is *important* to use the distance traveled by the pointers
                        // on the screen which may or may not be magnified.
                        final float deltaX = mBaseX - event.getX(0);
                        final float deltaY = mBaseY - event.getY(0);
                        final double moveDelta = Math.hypot(deltaX, deltaY);
                        // The user has moved enough for us to decide.
                        if (moveDelta > mDoubleTapSlop) {
                            // Check whether the user is performing a gesture. We
                            // detect gestures if the pointer is moving above a
                            // given velocity.
                            mVelocityTracker.computeCurrentVelocity(VELOCITY_UNITS_SECONDS);
                            final float maxAbsVelocity = Math.max(
                                    Math.abs(mVelocityTracker.getXVelocity(0)),
                                    Math.abs(mVelocityTracker.getYVelocity(0)));
                            if (maxAbsVelocity > mScaledGestureDetectionVelocity) {
                                // We have to perform gesture detection, so
                                // notify the listener.
                                mGestureConfirmed = true;
                                mListener.onGestureStarted();
                            } else {
                                // This won't match any gesture, so notify the
                                // listener.
                                cancelGesture();
                                mListener.onGestureCancelled(event, policyFlags);
                            }
                        }
                    }
                    final float dX = Math.abs(x - mPreviousX);
                    final float dY = Math.abs(y - mPreviousY);
                    if (dX >= TOUCH_TOLERANCE || dY >= TOUCH_TOLERANCE) {
@@ -134,12 +204,13 @@ class AccessibilityGestureDetector extends GestureDetector.SimpleOnGestureListen
                if (maybeFinishDoubleTap(event, policyFlags)) {
                    return true;
                }
                if (mRecognizingGesture) {
                if (mGestureConfirmed) {
                    mStrokeBuffer.add(new GesturePoint(x, y, event.getEventTime()));

                    if (recognizeGesture()) {
                        return true;
                    if (!recognizeGesture()) {
                        mListener.onGestureCancelled(event, policyFlags);
                    }
                    return true;
                }
                break;

@@ -167,6 +238,10 @@ class AccessibilityGestureDetector extends GestureDetector.SimpleOnGestureListen
                    return true;
                }
                break;

            case MotionEvent.ACTION_CANCEL:
                clear();
                break;
        }

        // If we're detecting taps on the second finger, map events from the
@@ -194,18 +269,13 @@ class AccessibilityGestureDetector extends GestureDetector.SimpleOnGestureListen
        mDoubleTapDetected = false;
        mSecondFingerDoubleTap = false;
        cancelGesture();
        mStrokeBuffer.clear();
        mVelocityTracker.clear();
    }

    public boolean firstTapDetected() {
        return mFirstTapDetected;
    }

    public void cancelGesture() {
        mRecognizingGesture = false;
        mStrokeBuffer.clear();
    }

    @Override
    public void onLongPress(MotionEvent e) {
        maybeSendLongPress(e, mPolicyFlags);
@@ -251,6 +321,12 @@ class AccessibilityGestureDetector extends GestureDetector.SimpleOnGestureListen
        return mListener.onDoubleTap(event, policyFlags);
    }

    private void cancelGesture() {
        mRecognizingGesture = false;
        mGestureConfirmed = false;
        mStrokeBuffer.clear();
    }

    private boolean recognizeGesture() {
        Gesture gesture = new Gesture();
        gesture.addStroke(new GestureStroke(mStrokeBuffer));
@@ -265,7 +341,7 @@ class AccessibilityGestureDetector extends GestureDetector.SimpleOnGestureListen
                }
                try {
                    final int gestureId = Integer.parseInt(bestPrediction.name);
                    if (mListener.onGesture(gestureId)) {
                    if (mListener.onGestureCompleted(gestureId)) {
                        return true;
                    }
                } catch (NumberFormatException nfe) {
+53 −98
Original line number Diff line number Diff line
@@ -25,7 +25,6 @@ import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.MotionEvent.PointerCoords;
import android.view.MotionEvent.PointerProperties;
import android.view.VelocityTracker;
import android.view.ViewConfiguration;
import android.view.WindowManagerPolicy;
import android.view.accessibility.AccessibilityEvent;
@@ -87,9 +86,6 @@ class TouchExplorer implements EventStreamTransformation, AccessibilityGestureDe
    // Invalid pointer ID.
    private static final int INVALID_POINTER_ID = -1;

    // The velocity above which we detect gestures.
    private static final int GESTURE_DETECTION_VELOCITY_DIP = 1000;

    // The minimal distance before we take the middle of the distance between
    // the two dragging pointers as opposed to use the location of the primary one.
    private static final int MIN_POINTER_DISTANCE_TO_USE_MIDDLE_LOCATION_DIP = 200;
@@ -134,15 +130,9 @@ class TouchExplorer implements EventStreamTransformation, AccessibilityGestureDe
    // the two dragging pointers as opposed to use the location of the primary one.
    private final int mScaledMinPointerDistanceToUseMiddleLocation;

    // The scaled velocity above which we detect gestures.
    private final int mScaledGestureDetectionVelocity;

    // The handler to which to delegate events.
    private EventStreamTransformation mNext;

    // Helper to track gesture velocity.
    private final VelocityTracker mVelocityTracker = VelocityTracker.obtain();

    // Helper class to track received pointers.
    private final ReceivedPointerTracker mReceivedPointerTracker;

@@ -200,7 +190,6 @@ class TouchExplorer implements EventStreamTransformation, AccessibilityGestureDe
        final float density = context.getResources().getDisplayMetrics().density;
        mScaledMinPointerDistanceToUseMiddleLocation =
            (int) (MIN_POINTER_DISTANCE_TO_USE_MIDDLE_LOCATION_DIP * density);
        mScaledGestureDetectionVelocity = (int) (GESTURE_DETECTION_VELOCITY_DIP * density);
    }

    @Override
@@ -289,11 +278,19 @@ class TouchExplorer implements EventStreamTransformation, AccessibilityGestureDe

        mReceivedPointerTracker.onMotionEvent(rawEvent);

        if (mGestureDetector.onMotionEvent(event, policyFlags)) {
        // The motion detector is interested in the movements in physical space,
        // so it uses the rawEvent to ignore magnification and other
        // transformations.
        if (mGestureDetector.onMotionEvent(rawEvent, policyFlags)) {
            // Event was handled by the gesture detector.
            return;
        }

        if (event.getActionMasked() == MotionEvent.ACTION_CANCEL) {
            clear(event, policyFlags);
            return;
        }

        switch(mCurrentState) {
            case STATE_TOUCH_EXPLORING: {
                handleMotionEventStateTouchExploring(event, rawEvent, policyFlags);
@@ -305,7 +302,7 @@ class TouchExplorer implements EventStreamTransformation, AccessibilityGestureDe
                handleMotionEventStateDelegating(event, policyFlags);
            } break;
            case STATE_GESTURE_DETECTING: {
                handleMotionEventGestureDetecting(rawEvent, policyFlags);
                // Already handled.
            } break;
            default:
                throw new IllegalStateException("Illegal state: " + mCurrentState);
@@ -440,26 +437,50 @@ class TouchExplorer implements EventStreamTransformation, AccessibilityGestureDe
    }

    @Override
    public boolean onGesture(int gestureId) {
    public boolean onGestureCompleted(int gestureId) {
        if (mCurrentState != STATE_GESTURE_DETECTING) {
            return false;
        }

        mAms.onTouchInteractionEnd();

        // Announce the end of the gesture recognition.
        sendAccessibilityEvent(AccessibilityEvent.TYPE_GESTURE_DETECTION_END);
        // Announce the end of a the touch interaction.
        sendAccessibilityEvent(AccessibilityEvent.TYPE_TOUCH_INTERACTION_END);
        endGestureDetection();

        mAms.onGesture(gestureId);

        mExitGestureDetectionModeDelayed.cancel();
        mCurrentState = STATE_TOUCH_EXPLORING;

        return true;
    }

    @Override
    public void onGestureStarted() {
      // We have to perform gesture detection, so
      // clear the current state and try to detect.
      mCurrentState = STATE_GESTURE_DETECTING;
      mSendHoverEnterAndMoveDelayed.cancel();
      mSendHoverExitDelayed.cancel();
      mExitGestureDetectionModeDelayed.post();
      // Send accessibility event to announce the start
      // of gesture recognition.
      sendAccessibilityEvent(AccessibilityEvent.TYPE_GESTURE_DETECTION_START);
    }

    @Override
    public void onGestureCancelled(MotionEvent event, int policyFlags) {
      if (mCurrentState == STATE_GESTURE_DETECTING) {
          endGestureDetection();
      } else if (mCurrentState == STATE_TOUCH_EXPLORING) {
          final int pointerId = mReceivedPointerTracker.getPrimaryPointerId();
          final int pointerIdBits = (1 << pointerId);

          // Cache the event until we discern exploration from gesturing.
          mSendHoverEnterAndMoveDelayed.addEvent(event);

          // We have just decided that the user is touch,
          // exploring so start sending events.
          mSendHoverEnterAndMoveDelayed.forceSendAndRemove();
          mSendHoverExitDelayed.cancel();
          sendMotionEvent(event, MotionEvent.ACTION_HOVER_MOVE, pointerIdBits, policyFlags);
      }
    }

    /**
     * Handles a motion event in touch exploring state.
     *
@@ -471,8 +492,6 @@ class TouchExplorer implements EventStreamTransformation, AccessibilityGestureDe
            int policyFlags) {
        ReceivedPointerTracker receivedTracker = mReceivedPointerTracker;

        mVelocityTracker.addMovement(rawEvent);

        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_DOWN: {
                mAms.onTouchInteractionStart();
@@ -525,46 +544,6 @@ class TouchExplorer implements EventStreamTransformation, AccessibilityGestureDe
                        if (mSendHoverEnterAndMoveDelayed.isPending()) {
                            // Cache the event until we discern exploration from gesturing.
                            mSendHoverEnterAndMoveDelayed.addEvent(event);

                            // It is *important* to use the distance traveled by the pointers
                            // on the screen which may or may not be magnified.
                            final float deltaX = receivedTracker.getReceivedPointerDownX(pointerId)
                                - rawEvent.getX(pointerIndex);
                            final float deltaY = receivedTracker.getReceivedPointerDownY(pointerId)
                                - rawEvent.getY(pointerIndex);
                            final double moveDelta = Math.hypot(deltaX, deltaY);
                            // The user has moved enough for us to decide.
                            if (moveDelta > mDoubleTapSlop) {
                                // Check whether the user is performing a gesture. We
                                // detect gestures if the pointer is moving above a
                                // given velocity.
                                mVelocityTracker.computeCurrentVelocity(1000);
                                final float maxAbsVelocity = Math.max(
                                        Math.abs(mVelocityTracker.getXVelocity(pointerId)),
                                        Math.abs(mVelocityTracker.getYVelocity(pointerId)));
                                if (maxAbsVelocity > mScaledGestureDetectionVelocity) {
                                    // We have to perform gesture detection, so
                                    // clear the current state and try to detect.
                                    mCurrentState = STATE_GESTURE_DETECTING;
                                    mVelocityTracker.clear();
                                    mSendHoverEnterAndMoveDelayed.cancel();
                                    mSendHoverExitDelayed.cancel();
                                    mExitGestureDetectionModeDelayed.post();
                                    // Send accessibility event to announce the start
                                    // of gesture recognition.
                                    sendAccessibilityEvent(
                                            AccessibilityEvent.TYPE_GESTURE_DETECTION_START);
                                } else {
                                    // We have just decided that the user is touch,
                                    // exploring so start sending events.
                                    mGestureDetector.cancelGesture();
                                    mSendHoverEnterAndMoveDelayed.forceSendAndRemove();
                                    mSendHoverExitDelayed.cancel();
                                    sendMotionEvent(event, MotionEvent.ACTION_HOVER_MOVE,
                                            pointerIdBits, policyFlags);
                                }
                                break;
                            }
                        } else {
                            if (mTouchExplorationInProgress) {
                                sendTouchExplorationGestureStartAndHoverEnterIfNeeded(policyFlags);
@@ -602,11 +581,6 @@ class TouchExplorer implements EventStreamTransformation, AccessibilityGestureDe
                            }
                        }

                        // We know that a new state transition is to happen and the
                        // new state will not be gesture recognition, so cancel
                        // the gesture.
                        mGestureDetector.cancelGesture();

                        if (isDraggingGesture(event)) {
                            // Two pointers moving in the same direction within
                            // a given distance perform a drag.
@@ -620,7 +594,6 @@ class TouchExplorer implements EventStreamTransformation, AccessibilityGestureDe
                            mCurrentState = STATE_DELEGATING;
                            sendDownForAllNotInjectedPointers(event, policyFlags);
                        }
                        mVelocityTracker.clear();
                    } break;
                    default: {
                        // More than one pointer so the user is not touch exploring
@@ -639,7 +612,6 @@ class TouchExplorer implements EventStreamTransformation, AccessibilityGestureDe
                        // More than two pointers are delegated to the view hierarchy.
                        mCurrentState = STATE_DELEGATING;
                        sendDownForAllNotInjectedPointers(event, policyFlags);
                        mVelocityTracker.clear();
                    }
                }
            } break;
@@ -648,8 +620,6 @@ class TouchExplorer implements EventStreamTransformation, AccessibilityGestureDe
                final int pointerId = event.getPointerId(event.getActionIndex());
                final int pointerIdBits = (1 << pointerId);

                mVelocityTracker.clear();

                if (mSendHoverEnterAndMoveDelayed.isPending()) {
                    // If we have not delivered the enter schedule an exit.
                    mSendHoverExitDelayed.post(event, pointerIdBits, policyFlags);
@@ -663,9 +633,6 @@ class TouchExplorer implements EventStreamTransformation, AccessibilityGestureDe
                }

            } break;
            case MotionEvent.ACTION_CANCEL: {
                clear(event, policyFlags);
            } break;
        }
    }

@@ -756,9 +723,6 @@ class TouchExplorer implements EventStreamTransformation, AccessibilityGestureDe
                }
                mCurrentState = STATE_TOUCH_EXPLORING;
            } break;
            case MotionEvent.ACTION_CANCEL: {
                clear(event, policyFlags);
            } break;
        }
    }

@@ -795,9 +759,6 @@ class TouchExplorer implements EventStreamTransformation, AccessibilityGestureDe

                mCurrentState = STATE_TOUCH_EXPLORING;
            } break;
            case MotionEvent.ACTION_CANCEL: {
                clear(event, policyFlags);
            } break;
            default: {
                // Deliver the event.
                sendMotionEvent(event, event.getAction(), ALL_POINTER_ID_BITS, policyFlags);
@@ -805,10 +766,9 @@ class TouchExplorer implements EventStreamTransformation, AccessibilityGestureDe
        }
    }

    private void handleMotionEventGestureDetecting(MotionEvent event, int policyFlags) {
        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_UP: {
    private void endGestureDetection() {
        mAms.onTouchInteractionEnd();

        // Announce the end of the gesture recognition.
        sendAccessibilityEvent(AccessibilityEvent.TYPE_GESTURE_DETECTION_END);
        // Announce the end of a the touch interaction.
@@ -816,11 +776,6 @@ class TouchExplorer implements EventStreamTransformation, AccessibilityGestureDe

        mExitGestureDetectionModeDelayed.cancel();
        mCurrentState = STATE_TOUCH_EXPLORING;
            } break;
            case MotionEvent.ACTION_CANCEL: {
                clear(event, policyFlags);
            } break;
        }
    }

    /**