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:
ryanlwlin
2021-01-05 20:51:39 +08:00
parent 664a31e67c
commit cb49d47bc6
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) {

View File

@@ -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(),

View File

@@ -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,

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -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;
}
}