From cb49d47bc6c30fb6c58fb3e7d077252fdd9641c1 Mon Sep 17 00:00:00 2001 From: ryanlwlin Date: Tue, 5 Jan 2021 20:51:39 +0800 Subject: [PATCH] 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 --- .../magnification/GesturesObserver.java | 8 +- .../MagnificationGestureMatcher.java | 14 +- .../MagnificationGesturesObserver.java | 2 +- .../magnification/SimpleSwipe.java | 7 +- .../magnification/TwoFingersDown.java | 74 --------- .../magnification/TwoFingersDownOrSwipe.java | 123 +++++++++++++++ .../WindowMagnificationGestureHandler.java | 6 +- .../MagnificationGesturesObserverTest.java | 22 +-- .../magnification/SimpleSwipeTest.java | 2 +- ...st.java => TwoFingersDownOrSwipeTest.java} | 80 +++++++--- ...WindowMagnificationGestureHandlerTest.java | 60 ++++---- .../utils/TouchEventGenerator.java | 142 ++++++++++++++---- 12 files changed, 357 insertions(+), 183 deletions(-) delete mode 100644 services/accessibility/java/com/android/server/accessibility/magnification/TwoFingersDown.java create mode 100644 services/accessibility/java/com/android/server/accessibility/magnification/TwoFingersDownOrSwipe.java rename services/tests/servicestests/src/com/android/server/accessibility/magnification/{TwoFingersDownTest.java => TwoFingersDownOrSwipeTest.java} (51%) diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/GesturesObserver.java b/services/accessibility/java/com/android/server/accessibility/magnification/GesturesObserver.java index feed18d438c77..3d8f5173d25a5 100644 --- a/services/accessibility/java/com/android/server/accessibility/magnification/GesturesObserver.java +++ b/services/accessibility/java/com/android/server/accessibility/magnification/GesturesObserver.java @@ -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(); } diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationGestureMatcher.java b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationGestureMatcher.java index 7a4d9e34b6572..570e0ce5490de 100644 --- a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationGestureMatcher.java +++ b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationGestureMatcher.java @@ -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); diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationGesturesObserver.java b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationGesturesObserver.java index a209086ba475d..085c343ff6317 100644 --- a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationGesturesObserver.java +++ b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationGesturesObserver.java @@ -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 delayedEventQueue, MotionEvent event); diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/SimpleSwipe.java b/services/accessibility/java/com/android/server/accessibility/magnification/SimpleSwipe.java index cd5061fa31638..fa15ac1c0e4fd 100644 --- a/services/accessibility/java/com/android/server/accessibility/magnification/SimpleSwipe.java +++ b/services/accessibility/java/com/android/server/accessibility/magnification/SimpleSwipe.java @@ -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 diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/TwoFingersDown.java b/services/accessibility/java/com/android/server/accessibility/magnification/TwoFingersDown.java deleted file mode 100644 index 173a5b82e0039..0000000000000 --- a/services/accessibility/java/com/android/server/accessibility/magnification/TwoFingersDown.java +++ /dev/null @@ -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(); - } -} diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/TwoFingersDownOrSwipe.java b/services/accessibility/java/com/android/server/accessibility/magnification/TwoFingersDownOrSwipe.java new file mode 100644 index 0000000000000..1742bd46d8655 --- /dev/null +++ b/services/accessibility/java/com/android/server/accessibility/magnification/TwoFingersDownOrSwipe.java @@ -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)); + } +} diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationGestureHandler.java b/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationGestureHandler.java index 55a911eea8210..fa3406217fa81 100644 --- a/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationGestureHandler.java +++ b/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationGestureHandler.java @@ -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: *
    - *
  1. If {@link MagnificationGestureMatcher#GESTURE_TWO_FINGER_DOWN} is detected, + *
  2. If {@link MagnificationGestureMatcher#GESTURE_TWO_FINGERS_DOWN_OR_SWIPE} is detected, * {@link State} will be transited to {@link PanningScalingGestureState}.
  3. *
  4. 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) { diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationGesturesObserverTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationGesturesObserverTest.java index 895fb17575046..5dbf837b08b20 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationGesturesObserverTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationGesturesObserverTest.java @@ -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(), diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/SimpleSwipeTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/SimpleSwipeTest.java index 01631bf21a637..0ca631e4ed62e 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/SimpleSwipeTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/SimpleSwipeTest.java @@ -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, diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/TwoFingersDownTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/TwoFingersDownOrSwipeTest.java similarity index 51% rename from services/tests/servicestests/src/com/android/server/accessibility/magnification/TwoFingersDownTest.java rename to services/tests/servicestests/src/com/android/server/accessibility/magnification/TwoFingersDownOrSwipeTest.java index ed8dc4e470de8..162d2a9d98af0 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/TwoFingersDownTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/TwoFingersDownOrSwipeTest.java @@ -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 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 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 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); } } diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationGestureHandlerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationGestureHandlerTest.java index 4b7ebbc29b463..b9498d641ed77 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationGestureHandlerTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationGestureHandlerTest.java @@ -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}. *

    *
    IDLE -> SHOW_MAGNIFIER [label="a11y\nbtn"] - *
    SHOW_MAGNIFIER -> TWO_FINGER_DOWN [label="2hold"] - *
    TWO_FINGER_DOWN -> SHOW_MAGNIFIER [label="release"] + *
    SHOW_MAGNIFIER -> TWO_FINGERS_DOWN [label="2hold"] + *
    TWO_FINGERS_DOWN -> SHOW_MAGNIFIER [label="release"] *
    SHOW_MAGNIFIER -> IDLE [label="a11y\nbtn"] *
    IDLE -> SHOW_MAGNIFIER_TRIPLE_TAP [label="3tap"] *
    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 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; } diff --git a/services/tests/servicestests/src/com/android/server/accessibility/utils/TouchEventGenerator.java b/services/tests/servicestests/src/com/android/server/accessibility/utils/TouchEventGenerator.java index a05881f78892c..fbcde533aa9f1 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/utils/TouchEventGenerator.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/utils/TouchEventGenerator.java @@ -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 twoPointersDownEvents(int displayId, PointF pointF1, + PointF pointF2) { + final List 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; + } }