diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/GestureManifold.java b/services/accessibility/java/com/android/server/accessibility/gestures/GestureManifold.java index 9b7adc883dee2..50d21ba599961 100644 --- a/services/accessibility/java/com/android/server/accessibility/gestures/GestureManifold.java +++ b/services/accessibility/java/com/android/server/accessibility/gestures/GestureManifold.java @@ -77,6 +77,8 @@ class GestureManifold implements GestureMatcher.StateChangeListener { // 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)); + // Second-finger double tap. + mGestures.add(new SecondFingerMultiTap(context, 2, GESTURE_DOUBLE_TAP, this)); // One-direction swipes. mGestures.add(new Swipe(context, RIGHT, GESTURE_SWIPE_RIGHT, this)); mGestures.add(new Swipe(context, LEFT, GESTURE_SWIPE_LEFT, this)); diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/SecondFingerMultiTap.java b/services/accessibility/java/com/android/server/accessibility/gestures/SecondFingerMultiTap.java new file mode 100644 index 0000000000000..eb38b53dc52ac --- /dev/null +++ b/services/accessibility/java/com/android/server/accessibility/gestures/SecondFingerMultiTap.java @@ -0,0 +1,167 @@ +/* + * 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.view.MotionEvent.INVALID_POINTER_ID; + +import android.content.Context; +import android.os.Handler; +import android.view.MotionEvent; +import android.view.ViewConfiguration; + +/** + * This class matches second-finger multi-tap gestures. A second-finger multi-tap gesture is where + * one finger is held down and a second finger executes the taps. The number of taps for each + * instance is specified in the constructor. + */ +class SecondFingerMultiTap extends GestureMatcher { + final int mTargetTaps; + int mDoubleTapSlop; + int mTouchSlop; + int mTapTimeout; + int mDoubleTapTimeout; + int mCurrentTaps; + int mSecondFingerPointerId; + float mBaseX; + float mBaseY; + + SecondFingerMultiTap( + 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; + mSecondFingerPointerId = INVALID_POINTER_ID; + super.clear(); + } + + @Override + protected void onPointerDown(MotionEvent event, MotionEvent rawEvent, int policyFlags) { + if (event.getPointerCount() > 2) { + cancelGesture(event, rawEvent, policyFlags); + return; + } + // Second finger has gone down. + int index = getActionIndex(event); + mSecondFingerPointerId = event.getPointerId(index); + cancelAfterTapTimeout(event, rawEvent, policyFlags); + if (Float.isNaN(mBaseX) && Float.isNaN(mBaseY)) { + mBaseX = event.getX(); + mBaseY = event.getY(); + } + if (!isSecondFingerInsideSlop(rawEvent, mDoubleTapSlop)) { + cancelGesture(event, rawEvent, policyFlags); + } + mBaseX = event.getX(); + mBaseY = event.getY(); + } + + @Override + protected void onPointerUp(MotionEvent event, MotionEvent rawEvent, int policyFlags) { + if (event.getPointerCount() > 2) { + cancelGesture(event, rawEvent, policyFlags); + return; + } + cancelAfterDoubleTapTimeout(event, rawEvent, policyFlags); + if (!isSecondFingerInsideSlop(rawEvent, mTouchSlop)) { + cancelGesture(event, rawEvent, policyFlags); + } + if (getState() == STATE_GESTURE_STARTED || getState() == STATE_CLEAR) { + mCurrentTaps++; + if (mCurrentTaps == mTargetTaps) { + // Done. + completeGesture(event, rawEvent, policyFlags); + return; + } + // Needs more taps. + cancelAfterDoubleTapTimeout(event, rawEvent, policyFlags); + } else { + // Nonsensical event stream. + cancelGesture(event, rawEvent, policyFlags); + } + } + + @Override + protected void onMove(MotionEvent event, MotionEvent rawEvent, int policyFlags) { + switch (event.getPointerCount()) { + case 1: + // We don't need to track anything about one-finger movements. + break; + case 2: + if (!isSecondFingerInsideSlop(rawEvent, mTouchSlop)) { + cancelGesture(event, rawEvent, policyFlags); + } + break; + default: + // More than two fingers means we stop tracking. + cancelGesture(event, rawEvent, policyFlags); + break; + } + } + + @Override + public String getGestureName() { + switch (mTargetTaps) { + case 2: + return "Second Finger Double Tap"; + case 3: + return "Second Finger Triple Tap"; + default: + return "Second Finger " + Integer.toString(mTargetTaps) + " Taps"; + } + } + + private boolean isSecondFingerInsideSlop(MotionEvent rawEvent, int slop) { + int pointerIndex = rawEvent.findPointerIndex(mSecondFingerPointerId); + if (pointerIndex == -1) { + return false; + } + final float deltaX = mBaseX - rawEvent.getX(pointerIndex); + final float deltaY = mBaseY - rawEvent.getY(pointerIndex); + if (deltaX == 0 && deltaY == 0) { + return true; + } + final double moveDelta = Math.hypot(deltaX, deltaY); + return moveDelta <= slop; + } + + private int getActionIndex(MotionEvent event) { + return event.getAction() + & MotionEvent.ACTION_POINTER_INDEX_MASK << MotionEvent.ACTION_POINTER_INDEX_SHIFT; + } + + @Override + public String toString() { + return super.toString() + + ", Taps:" + + mCurrentTaps + + ", mBaseX: " + + Float.toString(mBaseX) + + ", mBaseY: " + + Float.toString(mBaseY); + } +} 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 5f4163880366e..f6eb31b93f067 100644 --- a/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java +++ b/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java @@ -286,11 +286,6 @@ public class TouchExplorer extends BaseEventStreamTransformation @Override public void onDoubleTapAndHold() { - // Ignore the event if we aren't touch interacting. - if (!mState.isTouchInteracting()) { - return; - } - // Pointers should not be zero when running this command. if (mState.getLastReceivedEvent().getPointerCount() == 0) { return; @@ -304,10 +299,6 @@ public class TouchExplorer extends BaseEventStreamTransformation @Override public boolean onDoubleTap() { - if (!mState.isTouchInteracting()) { - return false; - } - mAms.onTouchInteractionEnd(); // Remove pending event deliveries. mSendHoverEnterAndMoveDelayed.cancel(); @@ -454,7 +445,7 @@ public class TouchExplorer extends BaseEventStreamTransformation handleActionDown(event, rawEvent, policyFlags); break; case MotionEvent.ACTION_POINTER_DOWN: - handleActionPointerDown(); + handleActionPointerDown(event, rawEvent, policyFlags); break; case MotionEvent.ACTION_MOVE: handleActionMoveStateTouchInteracting(event, rawEvent, policyFlags); @@ -479,7 +470,7 @@ public class TouchExplorer extends BaseEventStreamTransformation // We should have already received ACTION_DOWN. Ignore. break; case MotionEvent.ACTION_POINTER_DOWN: - handleActionPointerDown(); + handleActionPointerDown(event, rawEvent, policyFlags); break; case MotionEvent.ACTION_MOVE: handleActionMoveStateTouchExploring(event, rawEvent, policyFlags); @@ -496,12 +487,19 @@ public class TouchExplorer extends BaseEventStreamTransformation * Handles ACTION_POINTER_DOWN when in the touch exploring state. This event represents an * additional finger touching the screen. */ - private void handleActionPointerDown() { + private void handleActionPointerDown(MotionEvent event, MotionEvent rawEvent, int policyFlags) { // Another finger down means that if we have not started to deliver // hover events, we will not have to. The code for ACTION_MOVE will // decide what we will actually do next. - mSendHoverEnterAndMoveDelayed.cancel(); - mSendHoverExitDelayed.cancel(); + + if (mSendHoverEnterAndMoveDelayed.isPending()) { + mSendHoverEnterAndMoveDelayed.cancel(); + mSendHoverExitDelayed.cancel(); + } else { + // We have already delivered at least one hover event, so send hover exit to keep the + // stream consistent. + sendHoverExitAndTouchExplorationGestureEndIfNeeded(policyFlags); + } } /** * Handles ACTION_MOVE while in the touch interacting state. This is where transitions to