Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 2047cd4b authored by ryanlwlin's avatar ryanlwlin
Browse files

Fix the multi-fingers gesture conflict with TouchExplorer

TouchExplorer supports multi-finger gestures from R. However,
FullScreenMagnificationGestureHandler has higher priority of
receiving motion events than TouchExplorer. When the screen is
zoomed in, two pointersdown gesture makes it transiting to
PanningScalingState.

To fix it, We post a tap tmeout to transit to PanningScalingState
when receiving two pointers down. In this duration, any pointers
action will make it transiting to DelegatingState. We also add
a condition that is if the movement of any fingers exceeed the
touchSlope, it will transit to PanningScalingState.

Bug: 159508732
Test: atest FullScreenMagnificationGestureHandlerTest
atest MagnificationGestureHandlerTest

Change-Id: Ic72c0da68a6a4f1714da8d05f743d6218793a5da
parent e658c76b
Loading
Loading
Loading
Loading
+48 −4
Original line number Diff line number Diff line
@@ -26,6 +26,7 @@ import static android.view.MotionEvent.ACTION_UP;

import static com.android.internal.accessibility.util.AccessibilityStatsLogUtils.logMagnificationTripleTap;
import static com.android.server.accessibility.gestures.GestureUtils.distance;
import static com.android.server.accessibility.gestures.GestureUtils.distanceClosestPointerToPoint;

import static java.lang.Math.abs;
import static java.util.Arrays.asList;
@@ -37,6 +38,7 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.PointF;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
@@ -615,6 +617,7 @@ class FullScreenMagnificationGestureHandler extends MagnificationGestureHandler

        private static final int MESSAGE_ON_TRIPLE_TAP_AND_HOLD = 1;
        private static final int MESSAGE_TRANSITION_TO_DELEGATING_STATE = 2;
        private static final int MESSAGE_TRANSITION_TO_PANNINGSCALING_STATE = 3;

        final int mLongTapMinDelay;
        final int mSwipeMinDistance;
@@ -626,6 +629,7 @@ class FullScreenMagnificationGestureHandler extends MagnificationGestureHandler
        private MotionEvent mPreLastDown;
        private MotionEvent mLastUp;
        private MotionEvent mPreLastUp;
        private PointF mSecondPointerDownLocation = new PointF(Float.NaN, Float.NaN);

        private long mLastDetectingDownEventTime;

@@ -656,6 +660,10 @@ class FullScreenMagnificationGestureHandler extends MagnificationGestureHandler
                    transitionToDelegatingStateAndClear();
                }
                break;
                case MESSAGE_TRANSITION_TO_PANNINGSCALING_STATE: {
                    transitToPanningScalingStateAndClear();
                }
                break;
                default: {
                    throw new IllegalArgumentException("Unknown message type: " + type);
                }
@@ -702,14 +710,20 @@ class FullScreenMagnificationGestureHandler extends MagnificationGestureHandler
                }
                break;
                case ACTION_POINTER_DOWN: {
                    if (mMagnificationController.isMagnifying(mDisplayId)) {
                        transitionTo(mPanningScalingState);
                        clear();
                    if (mMagnificationController.isMagnifying(mDisplayId)
                            && event.getPointerCount() == 2) {
                        storeSecondPointerDownLocation(event);
                        mHandler.sendEmptyMessageDelayed(MESSAGE_TRANSITION_TO_PANNINGSCALING_STATE,
                                ViewConfiguration.getTapTimeout());
                    } else {
                        transitionToDelegatingStateAndClear();
                    }
                }
                break;
                case ACTION_POINTER_UP: {
                    transitionToDelegatingStateAndClear();
                }
                break;
                case ACTION_MOVE: {
                    if (isFingerDown()
                            && distance(mLastDown, /* move */ event) > mSwipeMinDistance) {
@@ -719,11 +733,19 @@ class FullScreenMagnificationGestureHandler extends MagnificationGestureHandler
                        // For convenience, viewport dragging takes precedence
                        // over insta-delegating on 3tap&swipe
                        // (which is a rare combo to be used aside from magnification)
                        if (isMultiTapTriggered(2 /* taps */)) {
                        if (isMultiTapTriggered(2 /* taps */) && event.getPointerCount() == 1) {
                            transitionToViewportDraggingStateAndClear(event);
                        } else if (isMagnifying() && event.getPointerCount() == 2) {
                            //Primary pointer is swiping, so transit to PanningScalingState
                            transitToPanningScalingStateAndClear();
                        } else {
                            transitionToDelegatingStateAndClear();
                        }
                    } else if (isMagnifying() && secondPointerDownValid()
                            && distanceClosestPointerToPoint(
                            mSecondPointerDownLocation, /* move */ event) > mSwipeMinDistance) {
                        //Second pointer is swiping, so transit to PanningScalingState
                        transitToPanningScalingStateAndClear();
                    }
                }
                break;
@@ -755,6 +777,21 @@ class FullScreenMagnificationGestureHandler extends MagnificationGestureHandler
            }
        }

        private void storeSecondPointerDownLocation(MotionEvent event) {
            final int index = event.getActionIndex();
            mSecondPointerDownLocation.set(event.getX(index), event.getY(index));
        }

        private boolean secondPointerDownValid() {
            return !(Float.isNaN(mSecondPointerDownLocation.x) && Float.isNaN(
                    mSecondPointerDownLocation.y));
        }

        private void transitToPanningScalingStateAndClear() {
            transitionTo(mPanningScalingState);
            clear();
        }

        public boolean isMultiTapTriggered(int numTaps) {

            // Shortcut acts as the 2 initial taps
@@ -822,11 +859,13 @@ class FullScreenMagnificationGestureHandler extends MagnificationGestureHandler
            setShortcutTriggered(false);
            removePendingDelayedMessages();
            clearDelayedMotionEvents();
            mSecondPointerDownLocation.set(Float.NaN, Float.NaN);
        }

        private void removePendingDelayedMessages() {
            mHandler.removeMessages(MESSAGE_ON_TRIPLE_TAP_AND_HOLD);
            mHandler.removeMessages(MESSAGE_TRANSITION_TO_DELEGATING_STATE);
            mHandler.removeMessages(MESSAGE_TRANSITION_TO_PANNINGSCALING_STATE);
        }

        private void cacheDelayedMotionEvent(MotionEvent event, MotionEvent rawEvent,
@@ -890,6 +929,7 @@ class FullScreenMagnificationGestureHandler extends MagnificationGestureHandler
            transitionTo(mDelegatingState);
            sendDelayedMotionEvents();
            removePendingDelayedMessages();
            mSecondPointerDownLocation.set(Float.NaN, Float.NaN);
        }

        private void onTripleTap(MotionEvent up) {
@@ -907,6 +947,10 @@ class FullScreenMagnificationGestureHandler extends MagnificationGestureHandler
            }
        }

        private boolean isMagnifying() {
            return mMagnificationController.isMagnifying(mDisplayId);
        }

        void transitionToViewportDraggingStateAndClear(MotionEvent down) {

            if (DEBUG_DETECTING) Slog.i(LOG_TAG, "onTripleTapAndHold()");
+22 −0
Original line number Diff line number Diff line
package com.android.server.accessibility.gestures;

import android.graphics.PointF;
import android.util.MathUtils;
import android.view.MotionEvent;

@@ -38,6 +39,27 @@ public final class GestureUtils {
        return MathUtils.dist(first.getX(), first.getY(), second.getX(), second.getY());
    }

    /**
     * Returns the minimum distance between {@code pointerDown} and each pointer of
     * {@link MotionEvent}.
     *
     * @param pointerDown The action pointer location of the {@link MotionEvent} with
     *     {@link MotionEvent#ACTION_DOWN} or {@link MotionEvent#ACTION_POINTER_DOWN}
     * @param moveEvent The {@link MotionEvent} with {@link MotionEvent#ACTION_MOVE}
     * @return the movement of the pointer.
     */
    public static double distanceClosestPointerToPoint(PointF pointerDown, MotionEvent moveEvent) {
        float movement = Float.MAX_VALUE;
        for (int i = 0; i < moveEvent.getPointerCount(); i++) {
            final float moveDelta = MathUtils.dist(pointerDown.x, pointerDown.y, moveEvent.getX(i),
                    moveEvent.getY(i));
            if (movement > moveDelta) {
                movement = moveDelta;
            }
        }
        return movement;
    }

    public static boolean isTimedOut(MotionEvent firstUp, MotionEvent secondUp, int timeout) {
        final long deltaTime = secondUp.getEventTime() - firstUp.getEventTime();
        return (deltaTime >= timeout);
+164 −28
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ 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 com.android.server.testutils.TestUtils.strictMock;

@@ -38,11 +39,13 @@ import static org.mockito.Mockito.when;
import android.animation.ValueAnimator;
import android.annotation.NonNull;
import android.content.Context;
import android.graphics.PointF;
import android.os.Handler;
import android.os.Message;
import android.util.DebugUtils;
import android.view.InputDevice;
import android.view.MotionEvent;
import android.view.ViewConfiguration;

import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
@@ -56,6 +59,9 @@ import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;


import java.util.ArrayList;
import java.util.List;
import java.util.function.IntConsumer;

/**
@@ -106,6 +112,7 @@ public class FullScreenMagnificationGestureHandlerTest {
    // Co-prime x and y, to potentially catch x-y-swapped errors
    public static final float DEFAULT_X = 301;
    public static final float DEFAULT_Y = 299;
    public static final PointF DEFAULT_POINT = new PointF(DEFAULT_X, DEFAULT_Y);

    private static final int DISPLAY_0 = 0;

@@ -327,6 +334,107 @@ public class FullScreenMagnificationGestureHandlerTest {
        });
    }

    @Test
    public void testTwoFingersOneTap_zoomedState_dispatchMotionEvents() {
        goFromStateIdleTo(STATE_ZOOMED);
        final EventCaptor eventCaptor = new EventCaptor();
        mMgh.setNext(eventCaptor);

        send(downEvent());
        send(pointerEvent(ACTION_POINTER_DOWN, DEFAULT_X * 2, DEFAULT_Y));
        send(pointerEvent(ACTION_POINTER_UP, DEFAULT_X * 2, DEFAULT_Y));
        send(upEvent());

        assertIn(STATE_ZOOMED);
        final List<Integer> expectedActions = new ArrayList();
        expectedActions.add(Integer.valueOf(ACTION_DOWN));
        expectedActions.add(Integer.valueOf(ACTION_POINTER_DOWN));
        expectedActions.add(Integer.valueOf(ACTION_POINTER_UP));
        expectedActions.add(Integer.valueOf(ACTION_UP));
        assertActionsInOrder(eventCaptor.mEvents, expectedActions);

        returnToNormalFrom(STATE_ZOOMED);
    }

    @Test
    public void testThreeFingersOneTap_zoomedState_dispatchMotionEvents() {
        goFromStateIdleTo(STATE_ZOOMED);
        final EventCaptor eventCaptor = new EventCaptor();
        mMgh.setNext(eventCaptor);
        PointF pointer1 = DEFAULT_POINT;
        PointF pointer2 = new PointF(DEFAULT_X * 1.5f, DEFAULT_Y);
        PointF pointer3 = new PointF(DEFAULT_X * 2, DEFAULT_Y);

        send(downEvent());
        send(pointerEvent(ACTION_POINTER_DOWN, new PointF[] {pointer1, pointer2}));
        send(pointerEvent(ACTION_POINTER_DOWN, new PointF[] {pointer1, pointer2, pointer3}));
        send(pointerEvent(ACTION_POINTER_UP, new PointF[] {pointer1, pointer2, pointer3}));
        send(pointerEvent(ACTION_POINTER_UP, new PointF[] {pointer1, pointer2, pointer3}));
        send(upEvent());

        assertIn(STATE_ZOOMED);
        final List<Integer> expectedActions = new ArrayList();
        expectedActions.add(Integer.valueOf(ACTION_DOWN));
        expectedActions.add(Integer.valueOf(ACTION_POINTER_DOWN));
        expectedActions.add(Integer.valueOf(ACTION_POINTER_DOWN));
        expectedActions.add(Integer.valueOf(ACTION_POINTER_UP));
        expectedActions.add(Integer.valueOf(ACTION_POINTER_UP));
        expectedActions.add(Integer.valueOf(ACTION_UP));
        assertActionsInOrder(eventCaptor.mEvents, expectedActions);

        returnToNormalFrom(STATE_ZOOMED);
    }

    @Test
    public void testFirstFingerSwipe_TwoPinterDownAndZoomedState_panningState() {
        goFromStateIdleTo(STATE_ZOOMED);
        PointF pointer1 = DEFAULT_POINT;
        PointF pointer2 = new PointF(DEFAULT_X * 1.5f, DEFAULT_Y);

        send(downEvent());
        send(pointerEvent(ACTION_POINTER_DOWN, new PointF[] {pointer1, pointer2}));
        //The minimum movement to transit to panningState.
        final float sWipeMinDistance = ViewConfiguration.get(mContext).getScaledTouchSlop();
        pointer1.offset(sWipeMinDistance + 1, 0);
        send(pointerEvent(ACTION_MOVE, new PointF[] {pointer1, pointer2}));
        assertIn(STATE_PANNING);

        assertIn(STATE_PANNING);
        returnToNormalFrom(STATE_PANNING);
    }

    @Test
    public void testSecondFingerSwipe_TwoPinterDownAndZoomedState_panningState() {
        goFromStateIdleTo(STATE_ZOOMED);
        PointF pointer1 = DEFAULT_POINT;
        PointF pointer2 = new PointF(DEFAULT_X * 1.5f, DEFAULT_Y);

        send(downEvent());
        send(pointerEvent(ACTION_POINTER_DOWN, new PointF[] {pointer1, pointer2}));
        //The minimum movement to transit to panningState.
        final float sWipeMinDistance = ViewConfiguration.get(mContext).getScaledTouchSlop();
        pointer2.offset(sWipeMinDistance + 1, 0);
        send(pointerEvent(ACTION_MOVE, new PointF[] {pointer1, pointer2}));
        assertIn(STATE_PANNING);

        assertIn(STATE_PANNING);
        returnToNormalFrom(STATE_PANNING);
    }

    private void assertActionsInOrder(List<MotionEvent> actualEvents,
            List<Integer> expectedActions) {
        assertTrue(actualEvents.size() == expectedActions.size());
        final int size = actualEvents.size();
        for (int i = 0; i < size; i++) {
            final int expectedAction = expectedActions.get(i);
            final int actualAction = actualEvents.get(i).getActionMasked();
            assertTrue(String.format(
                    "%dth action %s is not matched, actual events : %s, ", i,
                    MotionEvent.actionToString(expectedAction), actualEvents),
                    actualAction == expectedAction);
        }
    }

    private void assertZoomsImmediatelyOnSwipeFrom(int state) {
        goFromStateIdleTo(state);
        swipeAndHold();
@@ -467,6 +575,7 @@ public class FullScreenMagnificationGestureHandlerTest {
                    goFromStateIdleTo(STATE_ZOOMED);
                    send(downEvent());
                    send(pointerEvent(ACTION_POINTER_DOWN, DEFAULT_X * 2, DEFAULT_Y));
                    fastForward(ViewConfiguration.getTapTimeout());
                } break;
                case STATE_SCALING_AND_PANNING: {
                    goFromStateIdleTo(STATE_PANNING);
@@ -619,29 +728,37 @@ public class FullScreenMagnificationGestureHandlerTest {
                MotionEvent.ACTION_UP, x, y, 0));
    }


    private MotionEvent pointerEvent(int action, float x, float y) {
        MotionEvent.PointerProperties defPointerProperties = new MotionEvent.PointerProperties();
        defPointerProperties.id = 0;
        defPointerProperties.toolType = MotionEvent.TOOL_TYPE_FINGER;
        return pointerEvent(action, new PointF[] {DEFAULT_POINT, new PointF(x, y)});
    }

    private MotionEvent pointerEvent(int action, PointF[] pointersPosition) {
        final MotionEvent.PointerProperties[] PointerPropertiesArray =
                new MotionEvent.PointerProperties[pointersPosition.length];
        for (int i = 0; i < pointersPosition.length; i++) {
            MotionEvent.PointerProperties pointerProperties = new MotionEvent.PointerProperties();
        pointerProperties.id = 1;
            pointerProperties.id = i;
            pointerProperties.toolType = MotionEvent.TOOL_TYPE_FINGER;
            PointerPropertiesArray[i] = pointerProperties;
        }

        MotionEvent.PointerCoords defPointerCoords = new MotionEvent.PointerCoords();
        defPointerCoords.x = DEFAULT_X;
        defPointerCoords.y = DEFAULT_Y;
        final MotionEvent.PointerCoords[] pointerCoordsArray =
                new MotionEvent.PointerCoords[pointersPosition.length];
        for (int i = 0; i < pointersPosition.length; i++) {
            MotionEvent.PointerCoords pointerCoords = new MotionEvent.PointerCoords();
        pointerCoords.x = x;
        pointerCoords.y = y;
            pointerCoords.x = pointersPosition[i].x;
            pointerCoords.y = pointersPosition[i].y;
            pointerCoordsArray[i] = pointerCoords;
        }

        return MotionEvent.obtain(
                /* downTime */ mClock.now(),
                /* eventTime */ mClock.now(),
                /* action */ action,
            /* pointerCount */ 2,
            /* pointerProperties */ new MotionEvent.PointerProperties[] {
                    defPointerProperties, pointerProperties},
            /* pointerCoords */ new MotionEvent.PointerCoords[] { defPointerCoords, pointerCoords },
                /* pointerCount */ pointersPosition.length,
                /* pointerProperties */ PointerPropertiesArray,
                /* pointerCoords */ pointerCoordsArray,
                /* metaState */ 0,
                /* buttonState */ 0,
                /* xPrecision */ 1.0f,
@@ -652,7 +769,26 @@ public class FullScreenMagnificationGestureHandlerTest {
                /* flags */ 0);
    }


    private String stateDump() {
        return "\nCurrent state dump:\n" + mMgh + "\n" + mHandler.getPendingMessages();
    }

    private class EventCaptor implements EventStreamTransformation {
        List<MotionEvent> mEvents = new ArrayList<>();

        @Override
        public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
            mEvents.add(event.copy());
        }

        @Override
        public void setNext(EventStreamTransformation next) {
        }

        @Override
        public EventStreamTransformation getNext() {
            return null;
        }
    }
}