Fix touchexploration multi-finger gesture conflict

WindowMagnificationGestureHandler has higher priority to address
motion events. When the user put two fingers down on the screen,
magnification gesture detector will intercept all motion events.
It ends up the user couldn't perform any multi-finger gestures.

To fix it, we make magnification gesture detection more accurate:
1. swiping gesture sucesses only with one finger.
2. Regarding two-finger gesture, only swipe or stay on the screen
over a duration will be recognized.

Bug: 163016948
Test: manually test: enable Talback and perform 3-finger swipe gesture
      atest com.android.server.accessibility.magnification
Change-Id: I310cf6e3fb2cb2b5b6fbc6a0ba9f0aa1d219b4df
(cherry picked from commit cb49d47bc6)
Merged-In: I310cf6e3fb2cb2b5b6fbc6a0ba9f0aa1d219b4df
This commit is contained in:
ryanlwlin
2021-01-05 20:51:39 +08:00
committed by Ryan Lin
parent 8ae6cd8c72
commit 0c7aeef03d
12 changed files with 357 additions and 183 deletions

View File

@@ -98,8 +98,7 @@ public final class GesturesObserver implements GestureMatcher.StateChangeListene
}
mProcessMotionEvent = true;
for (int i = 0; i < mGestureMatchers.size(); i++) {
final GestureMatcher matcher =
mGestureMatchers.get(i);
final GestureMatcher matcher = mGestureMatchers.get(i);
matcher.onMotionEvent(event, rawEvent, policyFlags);
if (matcher.getState() == GestureMatcher.STATE_GESTURE_COMPLETED) {
clear();
@@ -128,7 +127,10 @@ public final class GesturesObserver implements GestureMatcher.StateChangeListene
MotionEvent rawEvent, int policyFlags) {
if (state == GestureMatcher.STATE_GESTURE_COMPLETED) {
mListener.onGestureCompleted(gestureId, event, rawEvent, policyFlags);
//Clear the states in onMotionEvent().
// Ideally we clear the states in onMotionEvent(), this case is for hold gestures.
// If we clear before processing up event , then MultiTap matcher cancels the gesture
// due to incorrect state. It ends up listener#onGestureCancelled is called even
// the gesture is detected.
if (!mProcessMotionEvent) {
clear();
}

View File

@@ -33,7 +33,7 @@ import java.lang.annotation.RetentionPolicy;
class MagnificationGestureMatcher {
private static final int GESTURE_BASE = 100;
public static final int GESTURE_TWO_FINGER_DOWN = GESTURE_BASE + 1;
public static final int GESTURE_TWO_FINGERS_DOWN_OR_SWIPE = GESTURE_BASE + 1;
public static final int GESTURE_SWIPE = GESTURE_BASE + 2;
public static final int GESTURE_SINGLE_TAP = GESTURE_BASE + 3;
public static final int GESTURE_SINGLE_TAP_AND_HOLD = GESTURE_BASE + 4;
@@ -41,7 +41,7 @@ class MagnificationGestureMatcher {
public static final int GESTURE_TRIPLE_TAP_AND_HOLD = GESTURE_BASE + 6;
@IntDef(prefix = {"GESTURE_MAGNIFICATION_"}, value = {
GESTURE_TWO_FINGER_DOWN,
GESTURE_TWO_FINGERS_DOWN_OR_SWIPE,
GESTURE_SWIPE
})
@Retention(RetentionPolicy.SOURCE)
@@ -57,8 +57,8 @@ class MagnificationGestureMatcher {
switch (gestureId) {
case GESTURE_SWIPE:
return "GESTURE_SWIPE";
case GESTURE_TWO_FINGER_DOWN:
return "GESTURE_TWO_FINGER_DOWN";
case GESTURE_TWO_FINGERS_DOWN_OR_SWIPE:
return "GESTURE_TWO_FINGERS_DOWN_OR_SWIPE";
case GESTURE_SINGLE_TAP:
return "GESTURE_SINGLE_TAP";
case GESTURE_SINGLE_TAP_AND_HOLD:
@@ -71,6 +71,12 @@ class MagnificationGestureMatcher {
return "none";
}
/**
* @param context
* @return the duration in milliseconds between the first tap's down event and
* the second tap's down event to be considered that the user is going to performing
* panning/scaling gesture.
*/
static int getMagnificationMultiTapTimeout(Context context) {
return ViewConfiguration.getDoubleTapTimeout() + context.getResources().getInteger(
R.integer.config_screen_magnification_multi_tap_adjustment);

View File

@@ -65,7 +65,7 @@ class MagnificationGesturesObserver implements GesturesObserver.Listener {
* the last event before timeout.
*
* @see MagnificationGestureMatcher#GESTURE_SWIPE
* @see MagnificationGestureMatcher#GESTURE_TWO_FINGER_DOWN
* @see MagnificationGestureMatcher#GESTURE_TWO_FINGERS_DOWN_OR_SWIPE
*/
void onGestureCompleted(@GestureId int gestureId, long lastDownEventTime,
List<MotionEventInfo> delayedEventQueue, MotionEvent event);

View File

@@ -48,6 +48,11 @@ class SimpleSwipe extends GestureMatcher {
cancelAfter(mDetectionDurationMillis, event, rawEvent, policyFlags);
}
@Override
protected void onPointerDown(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
cancelGesture(event, rawEvent, policyFlags);
}
@Override
protected void onMove(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
if (gestureMatched(event, rawEvent, policyFlags)) {
@@ -65,7 +70,7 @@ class SimpleSwipe extends GestureMatcher {
}
private boolean gestureMatched(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
return mLastDown != null && (distance(mLastDown, event) >= mSwipeMinDistance);
return mLastDown != null && (distance(mLastDown, event) > mSwipeMinDistance);
}
@Override

View File

@@ -1,74 +0,0 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.accessibility.magnification;
import android.content.Context;
import android.os.Handler;
import android.view.MotionEvent;
import com.android.server.accessibility.gestures.GestureMatcher;
/**
*
* This class is responsible for matching two fingers down gestures. The gesture matching
* result is determined in a duration.
*/
final class TwoFingersDown extends GestureMatcher {
private MotionEvent mLastDown;
private final int mDetectionDurationMillis;
TwoFingersDown(Context context) {
super(MagnificationGestureMatcher.GESTURE_TWO_FINGER_DOWN,
new Handler(context.getMainLooper()), null);
mDetectionDurationMillis = MagnificationGestureMatcher.getMagnificationMultiTapTimeout(
context);
}
@Override
protected void onDown(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
mLastDown = MotionEvent.obtain(event);
cancelAfter(mDetectionDurationMillis, event, rawEvent, policyFlags);
}
@Override
protected void onPointerDown(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
if (mLastDown == null) {
cancelGesture(event, rawEvent, policyFlags);
}
completeGesture(event, rawEvent, policyFlags);
}
@Override
protected void onUp(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
cancelGesture(event, rawEvent, policyFlags);
}
@Override
public void clear() {
if (mLastDown != null) {
mLastDown.recycle();
mLastDown = null;
}
super.clear();
}
@Override
protected String getGestureName() {
return this.getClass().getSimpleName();
}
}

View File

@@ -0,0 +1,123 @@
/*
* Copyright (C) 2021 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.magnification;
import android.annotation.NonNull;
import android.content.Context;
import android.os.Handler;
import android.util.MathUtils;
import android.view.MotionEvent;
import android.view.ViewConfiguration;
import com.android.server.accessibility.gestures.GestureMatcher;
/**
* This class is responsible for detecting that the user is using two fingers to perform
* swiping gestures or just stay pressed on the screen. The gesture matching result is determined
* in a duration.
*/
final class TwoFingersDownOrSwipe extends GestureMatcher {
private final int mDoubleTapTimeout;
private final int mDetectionDurationMillis;
private final int mSwipeMinDistance;
private MotionEvent mFirstPointerDown;
private MotionEvent mSecondPointerDown;
TwoFingersDownOrSwipe(Context context) {
super(MagnificationGestureMatcher.GESTURE_TWO_FINGERS_DOWN_OR_SWIPE,
new Handler(context.getMainLooper()), null);
mDetectionDurationMillis = MagnificationGestureMatcher.getMagnificationMultiTapTimeout(
context);
mDoubleTapTimeout = ViewConfiguration.getDoubleTapTimeout();
mSwipeMinDistance = ViewConfiguration.get(context).getScaledTouchSlop();
}
@Override
protected void onDown(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
mFirstPointerDown = MotionEvent.obtain(event);
cancelAfter(mDetectionDurationMillis, event, rawEvent, policyFlags);
}
@Override
protected void onPointerDown(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
if (mFirstPointerDown == null) {
cancelGesture(event, rawEvent, policyFlags);
}
if (event.getPointerCount() == 2) {
mSecondPointerDown = MotionEvent.obtain(event);
completeAfter(mDoubleTapTimeout, event, rawEvent, policyFlags);
} else {
cancelGesture(event, rawEvent, policyFlags);
}
}
@Override
protected void onMove(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
if (mFirstPointerDown == null || mSecondPointerDown == null) {
return;
}
if (distance(mFirstPointerDown, /* move */ event) > mSwipeMinDistance) {
completeGesture(event, rawEvent, policyFlags);
return;
}
if (distance(mSecondPointerDown, /* move */ event) > mSwipeMinDistance) {
// The second pointer is swiping.
completeGesture(event, rawEvent, policyFlags);
}
}
@Override
protected void onPointerUp(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
cancelGesture(event, rawEvent, policyFlags);
}
@Override
protected void onUp(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
cancelGesture(event, rawEvent, policyFlags);
}
@Override
public void clear() {
if (mFirstPointerDown != null) {
mFirstPointerDown.recycle();
mFirstPointerDown = null;
}
if (mSecondPointerDown != null) {
mSecondPointerDown.recycle();
mSecondPointerDown = null;
}
super.clear();
}
@Override
protected String getGestureName() {
return this.getClass().getSimpleName();
}
private static double distance(@NonNull MotionEvent downEvent, @NonNull MotionEvent moveEvent) {
final int downActionIndex = downEvent.getActionIndex();
final int downPointerId = downEvent.getPointerId(downActionIndex);
final int moveActionIndex = moveEvent.findPointerIndex(downPointerId);
if (moveActionIndex < 0) {
return -1;
}
return MathUtils.dist(downEvent.getX(downActionIndex), downEvent.getY(downActionIndex),
moveEvent.getX(moveActionIndex), moveEvent.getY(moveActionIndex));
}
}

View File

@@ -323,7 +323,7 @@ public class WindowMagnificationGestureHandler extends MagnificationGestureHandl
* manipulate the window magnifier or want to interact with current UI. The rule of leaving
* this state is as follows:
* <ol>
* <li> If {@link MagnificationGestureMatcher#GESTURE_TWO_FINGER_DOWN} is detected,
* <li> If {@link MagnificationGestureMatcher#GESTURE_TWO_FINGERS_DOWN_OR_SWIPE} is detected,
* {@link State} will be transited to {@link PanningScalingGestureState}.</li>
* <li> If other gesture is detected and the last motion event is neither ACTION_UP nor
* ACTION_CANCEL.
@@ -357,7 +357,7 @@ public class WindowMagnificationGestureHandler extends MagnificationGestureHandl
new SimpleSwipe(context),
multiTap,
multiTapAndHold,
new TwoFingersDown(context));
new TwoFingersDownOrSwipe(context));
}
@Override
@@ -399,7 +399,7 @@ public class WindowMagnificationGestureHandler extends MagnificationGestureHandl
Slog.d(mLogTag,
"onGestureDetected : delayedEventQueue = " + delayedEventQueue);
}
if (gestureId == MagnificationGestureMatcher.GESTURE_TWO_FINGER_DOWN
if (gestureId == MagnificationGestureMatcher.GESTURE_TWO_FINGERS_DOWN_OR_SWIPE
&& mWindowMagnificationMgr.pointersInWindow(mDisplayId, motionEvent) > 0) {
transitionTo(mObservePanningScalingState);
} else if (gestureId == MagnificationGestureMatcher.GESTURE_TRIPLE_TAP) {