Merge "Fix touchexploration multi-finger gesture conflict" into sc-dev

This commit is contained in:
Ryan Lin
2021-03-02 05:06:47 +00:00
committed by Android (Google) Code Review
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;
}
}