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
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -69,7 +69,7 @@ public class MagnificationGesturesObserverTest {
|
||||
mContext = InstrumentationRegistry.getContext();
|
||||
mInstrumentation = InstrumentationRegistry.getInstrumentation();
|
||||
mObserver = new MagnificationGesturesObserver(mCallback, new SimpleSwipe(mContext),
|
||||
new TwoFingersDown(mContext));
|
||||
new TwoFingersDownOrSwipe(mContext));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -77,9 +77,7 @@ public class MagnificationGesturesObserverTest {
|
||||
final MotionEvent moveEvent = TouchEventGenerator.moveEvent(Display.DEFAULT_DISPLAY,
|
||||
DEFAULT_X , DEFAULT_Y);
|
||||
|
||||
mInstrumentation.runOnMainSync(() -> {
|
||||
mObserver.onMotionEvent(moveEvent, moveEvent, 0);
|
||||
});
|
||||
mObserver.onMotionEvent(moveEvent, moveEvent, 0);
|
||||
|
||||
verify(mCallback).onGestureCancelled(eq(0L),
|
||||
mEventInfoArgumentCaptor.capture(), argThat(new MotionEventMatcher(moveEvent)));
|
||||
@@ -92,9 +90,7 @@ public class MagnificationGesturesObserverTest {
|
||||
final MotionEvent downEvent = TouchEventGenerator.downEvent(Display.DEFAULT_DISPLAY,
|
||||
DEFAULT_X , DEFAULT_Y);
|
||||
|
||||
mInstrumentation.runOnMainSync(() -> {
|
||||
mObserver.onMotionEvent(downEvent, downEvent, 0);
|
||||
});
|
||||
mObserver.onMotionEvent(downEvent, downEvent, 0);
|
||||
|
||||
verify(mCallback).onGestureCancelled(eq(0L),
|
||||
mEventInfoArgumentCaptor.capture(), argThat(new MotionEventMatcher(downEvent)));
|
||||
@@ -108,9 +104,7 @@ public class MagnificationGesturesObserverTest {
|
||||
final int timeoutMillis = MagnificationGestureMatcher.getMagnificationMultiTapTimeout(
|
||||
mContext) + 100;
|
||||
|
||||
mInstrumentation.runOnMainSync(() -> {
|
||||
mObserver.onMotionEvent(downEvent, downEvent, 0);
|
||||
});
|
||||
mObserver.onMotionEvent(downEvent, downEvent, 0);
|
||||
|
||||
verify(mCallback, timeout(timeoutMillis)).onGestureCancelled(eq(downEvent.getDownTime()),
|
||||
mEventInfoArgumentCaptor.capture(), argThat(new MotionEventMatcher(downEvent)));
|
||||
@@ -121,14 +115,12 @@ public class MagnificationGesturesObserverTest {
|
||||
public void sendEventsOfSwiping_onGestureCompleted() {
|
||||
final MotionEvent downEvent = TouchEventGenerator.downEvent(Display.DEFAULT_DISPLAY,
|
||||
DEFAULT_X, DEFAULT_Y);
|
||||
final float swipeDistance = ViewConfiguration.get(mContext).getScaledTouchSlop();
|
||||
final float swipeDistance = ViewConfiguration.get(mContext).getScaledTouchSlop() + 1;
|
||||
final MotionEvent moveEvent = TouchEventGenerator.moveEvent(Display.DEFAULT_DISPLAY,
|
||||
DEFAULT_X + swipeDistance, DEFAULT_Y + swipeDistance);
|
||||
|
||||
mInstrumentation.runOnMainSync(() -> {
|
||||
mObserver.onMotionEvent(downEvent, downEvent, 0);
|
||||
mObserver.onMotionEvent(moveEvent, moveEvent, 0);
|
||||
});
|
||||
mObserver.onMotionEvent(downEvent, downEvent, 0);
|
||||
mObserver.onMotionEvent(moveEvent, moveEvent, 0);
|
||||
|
||||
verify(mCallback).onGestureCompleted(eq(MagnificationGestureMatcher.GESTURE_SWIPE),
|
||||
eq(downEvent.getDownTime()), mEventInfoArgumentCaptor.capture(),
|
||||
|
||||
@@ -78,7 +78,7 @@ public class SimpleSwipeTest {
|
||||
|
||||
@Test
|
||||
public void sendSwipeEvent_onGestureCompleted() {
|
||||
final float swipeDistance = ViewConfiguration.get(mContext).getScaledTouchSlop();
|
||||
final float swipeDistance = ViewConfiguration.get(mContext).getScaledTouchSlop() + 1;
|
||||
final MotionEvent downEvent = TouchEventGenerator.downEvent(Display.DEFAULT_DISPLAY,
|
||||
DEFAULT_X, DEFAULT_Y);
|
||||
final MotionEvent moveEvent = TouchEventGenerator.moveEvent(Display.DEFAULT_DISPLAY,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
* 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.
|
||||
@@ -16,14 +16,20 @@
|
||||
|
||||
package com.android.server.accessibility.magnification;
|
||||
|
||||
import static com.android.server.accessibility.utils.TouchEventGenerator.movePointer;
|
||||
import static com.android.server.accessibility.utils.TouchEventGenerator.twoPointersDownEvents;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.after;
|
||||
import static org.mockito.Mockito.timeout;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.PointF;
|
||||
import android.view.Display;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.ViewConfiguration;
|
||||
|
||||
import androidx.test.InstrumentationRegistry;
|
||||
|
||||
@@ -35,18 +41,20 @@ import org.junit.Test;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Tests for {@link TwoFingersDown}.
|
||||
* Tests for {@link TwoFingersDownOrSwipe}.
|
||||
*/
|
||||
public class TwoFingersDownTest {
|
||||
public class TwoFingersDownOrSwipeTest {
|
||||
|
||||
private static final float DEFAULT_X = 100f;
|
||||
private static final float DEFAULT_Y = 100f;
|
||||
|
||||
private static Context sContext;
|
||||
private static float sSwipeMinDistance;
|
||||
private static int sTimeoutMillis;
|
||||
private static Context sContext;
|
||||
|
||||
private Context mContext;
|
||||
private GesturesObserver mGesturesObserver;
|
||||
@Mock
|
||||
private GesturesObserver.Listener mListener;
|
||||
@@ -56,13 +64,13 @@ public class TwoFingersDownTest {
|
||||
sContext = InstrumentationRegistry.getContext();
|
||||
sTimeoutMillis = MagnificationGestureMatcher.getMagnificationMultiTapTimeout(
|
||||
sContext) + 100;
|
||||
sSwipeMinDistance = ViewConfiguration.get(sContext).getScaledTouchSlop() + 1;
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
mContext = InstrumentationRegistry.getContext();
|
||||
MockitoAnnotations.initMocks(this);
|
||||
mGesturesObserver = new GesturesObserver(mListener, new TwoFingersDown(mContext));
|
||||
mGesturesObserver = new GesturesObserver(mListener, new TwoFingersDownOrSwipe(sContext));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -78,24 +86,16 @@ public class TwoFingersDownTest {
|
||||
|
||||
@Test
|
||||
public void sendTwoFingerDownEvent_onGestureCompleted() {
|
||||
final MotionEvent downEvent = TouchEventGenerator.downEvent(Display.DEFAULT_DISPLAY,
|
||||
DEFAULT_X, DEFAULT_Y);
|
||||
final MotionEvent.PointerCoords defPointerCoords = new MotionEvent.PointerCoords();
|
||||
defPointerCoords.x = DEFAULT_X;
|
||||
defPointerCoords.y = DEFAULT_Y;
|
||||
final MotionEvent.PointerCoords secondPointerCoords = new MotionEvent.PointerCoords();
|
||||
secondPointerCoords.x = DEFAULT_X + 10;
|
||||
secondPointerCoords.y = DEFAULT_Y + 10;
|
||||
final List<MotionEvent> downEvents = twoPointersDownEvents(Display.DEFAULT_DISPLAY,
|
||||
new PointF(DEFAULT_X, DEFAULT_Y), new PointF(DEFAULT_X + 10, DEFAULT_Y + 10));
|
||||
|
||||
final MotionEvent twoPointersDownEvent = TouchEventGenerator.twoPointersDownEvent(
|
||||
Display.DEFAULT_DISPLAY, defPointerCoords, secondPointerCoords);
|
||||
|
||||
mGesturesObserver.onMotionEvent(downEvent, downEvent, 0);
|
||||
mGesturesObserver.onMotionEvent(twoPointersDownEvent, twoPointersDownEvent, 0);
|
||||
for (MotionEvent event : downEvents) {
|
||||
mGesturesObserver.onMotionEvent(event, event, 0);
|
||||
}
|
||||
|
||||
verify(mListener, timeout(sTimeoutMillis)).onGestureCompleted(
|
||||
MagnificationGestureMatcher.GESTURE_TWO_FINGER_DOWN, twoPointersDownEvent,
|
||||
twoPointersDownEvent, 0);
|
||||
MagnificationGestureMatcher.GESTURE_TWO_FINGERS_DOWN_OR_SWIPE, downEvents.get(1),
|
||||
downEvents.get(1), 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -108,7 +108,39 @@ public class TwoFingersDownTest {
|
||||
mGesturesObserver.onMotionEvent(downEvent, downEvent, 0);
|
||||
mGesturesObserver.onMotionEvent(upEvent, upEvent, 0);
|
||||
|
||||
verify(mListener, timeout(sTimeoutMillis)).onGestureCancelled(any(MotionEvent.class),
|
||||
any(MotionEvent.class), eq(0));
|
||||
verify(mListener, after(ViewConfiguration.getDoubleTapTimeout())).onGestureCancelled(
|
||||
any(MotionEvent.class), any(MotionEvent.class), eq(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void firstPointerMove_twoPointersDown_onGestureCompleted() {
|
||||
final List<MotionEvent> downEvents = twoPointersDownEvents(Display.DEFAULT_DISPLAY,
|
||||
new PointF(DEFAULT_X, DEFAULT_Y), new PointF(DEFAULT_X + 10, DEFAULT_Y + 10));
|
||||
for (MotionEvent event : downEvents) {
|
||||
mGesturesObserver.onMotionEvent(event, event, 0);
|
||||
}
|
||||
final MotionEvent moveEvent = movePointer(downEvents.get(1), 0, sSwipeMinDistance, 0);
|
||||
|
||||
mGesturesObserver.onMotionEvent(moveEvent, moveEvent, 0);
|
||||
|
||||
verify(mListener).onGestureCompleted(
|
||||
MagnificationGestureMatcher.GESTURE_TWO_FINGERS_DOWN_OR_SWIPE, moveEvent,
|
||||
moveEvent, 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void secondPointerMove_twoPointersDown_onGestureCompleted() {
|
||||
final List<MotionEvent> downEvents = twoPointersDownEvents(Display.DEFAULT_DISPLAY,
|
||||
new PointF(DEFAULT_X, DEFAULT_Y), new PointF(DEFAULT_X + 10, DEFAULT_Y + 10));
|
||||
for (MotionEvent event : downEvents) {
|
||||
mGesturesObserver.onMotionEvent(event, event, 0);
|
||||
}
|
||||
final MotionEvent moveEvent = movePointer(downEvents.get(1), 1, sSwipeMinDistance, 0);
|
||||
|
||||
mGesturesObserver.onMotionEvent(moveEvent, moveEvent, 0);
|
||||
|
||||
verify(mListener).onGestureCompleted(
|
||||
MagnificationGestureMatcher.GESTURE_TWO_FINGERS_DOWN_OR_SWIPE, moveEvent,
|
||||
moveEvent, 0);
|
||||
}
|
||||
}
|
||||
@@ -24,13 +24,15 @@ import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.PointF;
|
||||
import android.graphics.Rect;
|
||||
import android.os.RemoteException;
|
||||
import android.util.DebugUtils;
|
||||
import android.view.InputDevice;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.ViewConfiguration;
|
||||
|
||||
import androidx.test.InstrumentationRegistry;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
import androidx.test.runner.AndroidJUnit4;
|
||||
|
||||
import com.android.server.accessibility.EventStreamTransformation;
|
||||
@@ -43,6 +45,7 @@ import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.IntConsumer;
|
||||
|
||||
/**
|
||||
@@ -75,7 +78,7 @@ public class WindowMagnificationGestureHandlerTest {
|
||||
@Before
|
||||
public void setUp() throws RemoteException {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
mContext = InstrumentationRegistry.getContext();
|
||||
mContext = InstrumentationRegistry.getInstrumentation().getContext();
|
||||
mWindowMagnificationManager = new WindowMagnificationManager(mContext, 0,
|
||||
mock(WindowMagnificationManager.Callback.class));
|
||||
mMockConnection = new MockWindowMagnificationConnection();
|
||||
@@ -100,8 +103,8 @@ public class WindowMagnificationGestureHandlerTest {
|
||||
* Covers following paths to get to and back between each state and {@link #STATE_IDLE}.
|
||||
* <p>
|
||||
* <br> IDLE -> SHOW_MAGNIFIER [label="a11y\nbtn"]
|
||||
* <br> SHOW_MAGNIFIER -> TWO_FINGER_DOWN [label="2hold"]
|
||||
* <br> TWO_FINGER_DOWN -> SHOW_MAGNIFIER [label="release"]
|
||||
* <br> SHOW_MAGNIFIER -> TWO_FINGERS_DOWN [label="2hold"]
|
||||
* <br> TWO_FINGERS_DOWN -> SHOW_MAGNIFIER [label="release"]
|
||||
* <br> SHOW_MAGNIFIER -> IDLE [label="a11y\nbtn"]
|
||||
* <br> IDLE -> SHOW_MAGNIFIER_TRIPLE_TAP [label="3tap"]
|
||||
* <br> SHOW_MAGNIFIER_TRIPLE_TAP -> IDLE [label="3tap"]
|
||||
@@ -112,18 +115,16 @@ public class WindowMagnificationGestureHandlerTest {
|
||||
*/
|
||||
@Test
|
||||
public void testEachState_isReachableAndRecoverable() {
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
|
||||
forEachState(state -> {
|
||||
goFromStateIdleTo(state);
|
||||
assertIn(state);
|
||||
returnToNormalFrom(state);
|
||||
try {
|
||||
assertIn(STATE_IDLE);
|
||||
} catch (AssertionError e) {
|
||||
throw new AssertionError("Failed while testing state " + stateToString(state),
|
||||
e);
|
||||
}
|
||||
});
|
||||
forEachState(state -> {
|
||||
goFromStateIdleTo(state);
|
||||
assertIn(state);
|
||||
returnToNormalFrom(state);
|
||||
try {
|
||||
assertIn(STATE_IDLE);
|
||||
} catch (AssertionError e) {
|
||||
throw new AssertionError("Failed while testing state " + stateToString(state),
|
||||
e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -209,10 +210,19 @@ public class WindowMagnificationGestureHandlerTest {
|
||||
case STATE_TWO_FINGERS_DOWN: {
|
||||
goFromStateIdleTo(STATE_SHOW_MAGNIFIER_SHORTCUT);
|
||||
final Rect frame = mMockConnection.getMirrorWindowFrame();
|
||||
send(downEvent(frame.centerX(), frame.centerY()));
|
||||
//Second finger is outside the window.
|
||||
send(twoPointerDownEvent(new float[]{frame.centerX(), frame.centerX() + 10},
|
||||
new float[]{frame.centerY(), frame.centerY() + 10}));
|
||||
final PointF firstPointerDown = new PointF(frame.centerX(), frame.centerY());
|
||||
// The second finger is outside the window.
|
||||
final PointF secondPointerDown = new PointF(frame.right + 10,
|
||||
frame.bottom + 10);
|
||||
final List<MotionEvent> motionEvents =
|
||||
TouchEventGenerator.twoPointersDownEvents(DISPLAY_0,
|
||||
firstPointerDown, secondPointerDown);
|
||||
for (MotionEvent downEvent: motionEvents) {
|
||||
send(downEvent);
|
||||
}
|
||||
// Wait for two-finger down gesture completed.
|
||||
Thread.sleep(ViewConfiguration.getDoubleTapTimeout());
|
||||
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
|
||||
}
|
||||
break;
|
||||
case STATE_SHOW_MAGNIFIER_TRIPLE_TAP: {
|
||||
@@ -301,16 +311,6 @@ public class WindowMagnificationGestureHandlerTest {
|
||||
send(upEvent(DEFAULT_TAP_X, DEFAULT_TAP_Y));
|
||||
}
|
||||
|
||||
private MotionEvent twoPointerDownEvent(float[] x, float[] y) {
|
||||
final MotionEvent.PointerCoords defPointerCoords = new MotionEvent.PointerCoords();
|
||||
defPointerCoords.x = x[0];
|
||||
defPointerCoords.y = y[0];
|
||||
final MotionEvent.PointerCoords pointerCoords = new MotionEvent.PointerCoords();
|
||||
pointerCoords.x = x[1];
|
||||
pointerCoords.y = y[1];
|
||||
return TouchEventGenerator.twoPointersDownEvent(DISPLAY_0, defPointerCoords, pointerCoords);
|
||||
}
|
||||
|
||||
private String stateDump() {
|
||||
return "\nCurrent state dump:\n" + mWindowMagnificationGestureHandler.mCurrentState;
|
||||
}
|
||||
|
||||
@@ -19,16 +19,19 @@ package com.android.server.accessibility.utils;
|
||||
import static android.view.MotionEvent.ACTION_DOWN;
|
||||
import static android.view.MotionEvent.ACTION_MOVE;
|
||||
import static android.view.MotionEvent.ACTION_POINTER_DOWN;
|
||||
import static android.view.MotionEvent.ACTION_POINTER_UP;
|
||||
import static android.view.MotionEvent.ACTION_UP;
|
||||
import static android.view.MotionEvent.PointerCoords;
|
||||
|
||||
import android.graphics.PointF;
|
||||
import android.os.SystemClock;
|
||||
import android.view.InputDevice;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* generates {@link MotionEvent} with source {@link InputDevice#SOURCE_TOUCHSCREEN}
|
||||
*
|
||||
*/
|
||||
public class TouchEventGenerator {
|
||||
|
||||
@@ -39,44 +42,68 @@ public class TouchEventGenerator {
|
||||
public static MotionEvent moveEvent(int displayId, float x, float y) {
|
||||
return generateSingleTouchEvent(displayId, ACTION_MOVE, x, y);
|
||||
}
|
||||
|
||||
public static MotionEvent upEvent(int displayId, float x, float y) {
|
||||
return generateSingleTouchEvent(displayId, ACTION_UP, x, y);
|
||||
}
|
||||
|
||||
public static MotionEvent twoPointersDownEvent(int displayId, PointerCoords defPointerCoords,
|
||||
PointerCoords pointerCoords) {
|
||||
return generateTwoPointersEvent(displayId, ACTION_POINTER_DOWN, defPointerCoords,
|
||||
pointerCoords);
|
||||
}
|
||||
|
||||
private static MotionEvent generateSingleTouchEvent(int displayId, int action, float x,
|
||||
float y) {
|
||||
final long downTime = SystemClock.uptimeMillis();
|
||||
final MotionEvent ev = MotionEvent.obtain(downTime, downTime,
|
||||
action, x, y, 0);
|
||||
ev.setDisplayId(displayId);
|
||||
ev.setSource(InputDevice.SOURCE_TOUCHSCREEN);
|
||||
return ev;
|
||||
return generateMultiplePointersEvent(displayId, action, new PointF(x, y));
|
||||
}
|
||||
|
||||
private static MotionEvent generateTwoPointersEvent(int displayId, int action,
|
||||
PointerCoords defPointerCoords, PointerCoords pointerCoords) {
|
||||
final long downTime = SystemClock.uptimeMillis();
|
||||
MotionEvent.PointerProperties defPointerProperties = new MotionEvent.PointerProperties();
|
||||
defPointerProperties.id = 0;
|
||||
defPointerProperties.toolType = MotionEvent.TOOL_TYPE_FINGER;
|
||||
MotionEvent.PointerProperties pointerProperties = new MotionEvent.PointerProperties();
|
||||
pointerProperties.id = 1;
|
||||
pointerProperties.toolType = MotionEvent.TOOL_TYPE_FINGER;
|
||||
/**
|
||||
* Creates a list of {@link MotionEvent} with given pointers location.
|
||||
*
|
||||
* @param displayId the display id
|
||||
* @param pointF1 location on the screen of the second pointer.
|
||||
* @param pointF2 location on the screen of the second pointer.
|
||||
* @return a list of {@link MotionEvent} with {@link MotionEvent#ACTION_DOWN} and {@link
|
||||
* MotionEvent#ACTION_POINTER_DOWN}.
|
||||
*/
|
||||
public static List<MotionEvent> twoPointersDownEvents(int displayId, PointF pointF1,
|
||||
PointF pointF2) {
|
||||
final List<MotionEvent> downEvents = new ArrayList<>();
|
||||
final MotionEvent downEvent = generateMultiplePointersEvent(displayId,
|
||||
MotionEvent.ACTION_DOWN, pointF1);
|
||||
downEvents.add(downEvent);
|
||||
|
||||
final int actionIndex = 1 << MotionEvent.ACTION_POINTER_INDEX_SHIFT;
|
||||
final int action = ACTION_POINTER_DOWN | actionIndex;
|
||||
|
||||
final MotionEvent twoPointersDownEvent = generateMultiplePointersEvent(displayId, action,
|
||||
pointF1, pointF2);
|
||||
downEvents.add(twoPointersDownEvent);
|
||||
return downEvents;
|
||||
}
|
||||
|
||||
private static MotionEvent generateMultiplePointersEvent(int displayId, int action,
|
||||
PointF... pointFs) {
|
||||
final int length = pointFs.length;
|
||||
final MotionEvent.PointerCoords[] pointerCoordsArray =
|
||||
new MotionEvent.PointerCoords[length];
|
||||
final MotionEvent.PointerProperties[] pointerPropertiesArray =
|
||||
new MotionEvent.PointerProperties[length];
|
||||
for (int i = 0; i < length; i++) {
|
||||
MotionEvent.PointerCoords pointerCoords = new MotionEvent.PointerCoords();
|
||||
pointerCoords.x = pointFs[i].x;
|
||||
pointerCoords.y = pointFs[i].y;
|
||||
pointerCoordsArray[i] = pointerCoords;
|
||||
|
||||
MotionEvent.PointerProperties pointerProperties = new MotionEvent.PointerProperties();
|
||||
pointerProperties.id = i;
|
||||
pointerProperties.toolType = MotionEvent.TOOL_TYPE_FINGER;
|
||||
pointerPropertiesArray[i] = pointerProperties;
|
||||
}
|
||||
|
||||
final long downTime = SystemClock.uptimeMillis();
|
||||
final MotionEvent ev = MotionEvent.obtain(
|
||||
/* downTime */ downTime,
|
||||
/* eventTime */ downTime,
|
||||
/* action */ action,
|
||||
/* pointerCount */ 2,
|
||||
/* pointerProperties */ new MotionEvent.PointerProperties[] {
|
||||
defPointerProperties, pointerProperties},
|
||||
/* pointerCoords */ new PointerCoords[] { defPointerCoords, pointerCoords },
|
||||
/* pointerCount */ length,
|
||||
/* pointerProperties */ pointerPropertiesArray,
|
||||
/* pointerCoords */ pointerCoordsArray,
|
||||
/* metaState */ 0,
|
||||
/* buttonState */ 0,
|
||||
/* xPrecision */ 1.0f,
|
||||
@@ -88,4 +115,65 @@ public class TouchEventGenerator {
|
||||
ev.setDisplayId(displayId);
|
||||
return ev;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a move event that moves the pointer of the original event with given index.
|
||||
* The original event should not be up event and we don't support
|
||||
* {@link MotionEvent#ACTION_POINTER_UP} now.
|
||||
*
|
||||
* @param originalEvent the move or down event
|
||||
* @param pointerIndex the index of the pointer we want to move.
|
||||
* @param offsetX the offset in X coordinate.
|
||||
* @param offsetY the offset in Y coordinate.
|
||||
* @return a motion event with move action.
|
||||
*/
|
||||
public static MotionEvent movePointer(MotionEvent originalEvent, int pointerIndex,
|
||||
float offsetX, float offsetY) {
|
||||
if (originalEvent.getActionMasked() == ACTION_UP) {
|
||||
throw new IllegalArgumentException("No pointer is on the screen");
|
||||
}
|
||||
|
||||
if (originalEvent.getActionMasked() == ACTION_POINTER_UP) {
|
||||
throw new IllegalArgumentException("unsupported yet,please implement it first");
|
||||
}
|
||||
|
||||
final int pointerCount = originalEvent.getPointerCount();
|
||||
if (pointerIndex >= pointerCount) {
|
||||
throw new IllegalArgumentException(
|
||||
pointerIndex + "is not available with pointer count" + pointerCount);
|
||||
}
|
||||
final int action = MotionEvent.ACTION_MOVE;
|
||||
final MotionEvent.PointerProperties[] pp = new MotionEvent.PointerProperties[pointerCount];
|
||||
for (int i = 0; i < pointerCount; i++) {
|
||||
MotionEvent.PointerProperties pointerProperty = new MotionEvent.PointerProperties();
|
||||
originalEvent.getPointerProperties(i, pointerProperty);
|
||||
pp[i] = pointerProperty;
|
||||
}
|
||||
|
||||
final MotionEvent.PointerCoords[] pc = new MotionEvent.PointerCoords[pointerCount];
|
||||
for (int i = 0; i < pointerCount; i++) {
|
||||
MotionEvent.PointerCoords pointerCoord = new MotionEvent.PointerCoords();
|
||||
originalEvent.getPointerCoords(i, pointerCoord);
|
||||
pc[i] = pointerCoord;
|
||||
}
|
||||
pc[pointerIndex].x += offsetX;
|
||||
pc[pointerIndex].y += offsetY;
|
||||
final MotionEvent ev = MotionEvent.obtain(
|
||||
/* downTime */ originalEvent.getDownTime(),
|
||||
/* eventTime */ SystemClock.uptimeMillis(),
|
||||
/* action */ action,
|
||||
/* pointerCount */ 2,
|
||||
/* pointerProperties */ pp,
|
||||
/* pointerCoords */ pc,
|
||||
/* metaState */ 0,
|
||||
/* buttonState */ 0,
|
||||
/* xPrecision */ 1.0f,
|
||||
/* yPrecision */ 1.0f,
|
||||
/* deviceId */ 0,
|
||||
/* edgeFlags */ 0,
|
||||
/* source */ originalEvent.getSource(),
|
||||
/* flags */ originalEvent.getFlags());
|
||||
ev.setDisplayId(originalEvent.getDisplayId());
|
||||
return ev;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user