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

Commit 3b930b1a authored by Jean Chen's avatar Jean Chen Committed by Android (Google) Code Review
Browse files

Merge changes from topic "MultiFingerMultiTap" into main

* changes:
  feat(MultiFingerMultiTap): Implement two finger triple tap feature on FullScreenMagnificationGestureHandler
  feat(MultiFingerMultiTap):Create a new DetectingState and ViewportDraggingState for feature
parents c8ce1cc5 5dcb907b
Loading
Loading
Loading
Loading
+281 −30
Original line number Diff line number Diff line
@@ -69,6 +69,7 @@ import com.android.internal.accessibility.util.AccessibilityStatsLogUtils;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.accessibility.AccessibilityManagerService;
import com.android.server.accessibility.AccessibilityTraceManager;
import com.android.server.accessibility.Flags;
import com.android.server.accessibility.gestures.GestureUtils;

/**
@@ -261,8 +262,12 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
        }

        mDelegatingState = new DelegatingState();
        mDetectingState = new DetectingState(context);
        mViewportDraggingState = new ViewportDraggingState();
        mDetectingState = Flags.enableMagnificationMultipleFingerMultipleTapGesture()
                ? new DetectingStateWithMultiFinger(context)
                : new DetectingState(context);
        mViewportDraggingState = Flags.enableMagnificationMultipleFingerMultipleTapGesture()
                ? new ViewportDraggingStateWithMultiFinger()
                : new ViewportDraggingState();
        mPanningScalingState = new PanningScalingState(context);
        mSinglePanningState = new SinglePanningState(context);
        mFullScreenMagnificationVibrationHelper = fullScreenMagnificationVibrationHelper;
@@ -634,6 +639,62 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
        }
    }

    final class ViewportDraggingStateWithMultiFinger extends ViewportDraggingState {
        // LINT.IfChange(viewport_dragging_state_with_multi_finger)
        @Override
        public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags)
                throws GestureException {
            final int action = event.getActionMasked();
            switch (action) {
                case ACTION_POINTER_DOWN: {
                    clearAndTransitToPanningScalingState();
                }
                break;
                case ACTION_MOVE: {
                    if (event.getPointerCount() > 2) {
                        throw new GestureException("Should have one pointer down.");
                    }
                    final float eventX = event.getX();
                    final float eventY = event.getY();
                    if (mFullScreenMagnificationController.magnificationRegionContains(
                            mDisplayId, eventX, eventY)) {
                        mFullScreenMagnificationController.setCenter(mDisplayId, eventX, eventY,
                                /* animate */ mLastMoveOutsideMagnifiedRegion,
                                AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID);
                        mLastMoveOutsideMagnifiedRegion = false;
                    } else {
                        mLastMoveOutsideMagnifiedRegion = true;
                    }
                }
                break;

                case ACTION_UP:
                case ACTION_CANCEL: {
                    // If mScaleToRecoverAfterDraggingEnd >= 1.0, the dragging state is triggered
                    // by zoom in temporary, and the magnifier needs to recover to original scale
                    // after exiting dragging state.
                    // Otherwise, the magnifier should be disabled.
                    if (mScaleToRecoverAfterDraggingEnd >= 1.0f) {
                        zoomToScale(mScaleToRecoverAfterDraggingEnd, event.getX(),
                                event.getY());
                    } else {
                        zoomOff();
                    }
                    clear();
                    mScaleToRecoverAfterDraggingEnd = Float.NaN;
                    transitionTo(mDetectingState);
                }
                    break;

                case ACTION_DOWN: {
                    throw new GestureException(
                            "Unexpected event type: " + MotionEvent.actionToString(action));
                }
            }
        }
        // LINT.ThenChange(:viewport_dragging_state)
    }

    /**
     * This class handles motion events when the event dispatcher has
     * determined that the user is performing a single-finger drag of the
@@ -643,17 +704,18 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
     * of the finger, and any part of the screen is reachable without lifting the finger.
     * This makes it the preferable mode for tasks like reading text spanning full screen width.
     */
    final class ViewportDraggingState implements State {
    class ViewportDraggingState implements State {

        /**
         * The cached scale for recovering after dragging ends.
         * If the scale >= 1.0, the magnifier needs to recover to scale.
         * Otherwise, the magnifier should be disabled.
         */
        @VisibleForTesting float mScaleToRecoverAfterDraggingEnd = Float.NaN;
        @VisibleForTesting protected float mScaleToRecoverAfterDraggingEnd = Float.NaN;

        private boolean mLastMoveOutsideMagnifiedRegion;
        protected boolean mLastMoveOutsideMagnifiedRegion;

        // LINT.IfChange(viewport_dragging_state)
        @Override
        public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags)
                throws GestureException {
@@ -706,6 +768,7 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
                }
            }
        }
        // LINT.ThenChange(:viewport_dragging_state_with_multi_finger)

        private boolean isAlwaysOnMagnificationEnabled() {
            return mFullScreenMagnificationController.isAlwaysOnMagnificationEnabled();
@@ -732,7 +795,7 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
                    ? mFullScreenMagnificationController.getScale(mDisplayId) : Float.NaN;
        }

        private void clearAndTransitToPanningScalingState() {
        protected void clearAndTransitToPanningScalingState() {
            final float scaleToRecovery = mScaleToRecoverAfterDraggingEnd;
            clear();
            mScaleToRecoverAfterDraggingEnd = scaleToRecovery;
@@ -791,36 +854,220 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
        }
    }

    final class DetectingStateWithMultiFinger extends DetectingState {
        // A flag set to true when two fingers have touched down.
        // Used to indicate what next finger action should be.
        private boolean mIsTwoFingerCountReached = false;
        // A tap counts when two fingers are down and up once.
        private int mCompletedTapCount = 0;
        DetectingStateWithMultiFinger(Context context) {
            super(context);
        }

        // LINT.IfChange(detecting_state_with_multi_finger)
        @Override
        public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
            cacheDelayedMotionEvent(event, rawEvent, policyFlags);
            switch (event.getActionMasked()) {
                case MotionEvent.ACTION_DOWN: {
                    mLastDetectingDownEventTime = event.getDownTime();
                    mHandler.removeMessages(MESSAGE_TRANSITION_TO_DELEGATING_STATE);

                    mFirstPointerDownLocation.set(event.getX(), event.getY());

                    if (!mFullScreenMagnificationController.magnificationRegionContains(
                            mDisplayId, event.getX(), event.getY())) {

                        transitionToDelegatingStateAndClear();

                    } else if (isMultiTapTriggered(2 /* taps */)) {

                        // 3tap and hold
                        afterLongTapTimeoutTransitionToDraggingState(event);

                    } else if (isTapOutOfDistanceSlop()) {

                        transitionToDelegatingStateAndClear();

                    } else if (mDetectSingleFingerTripleTap
                            || mDetectTwoFingerTripleTap
                            // If activated, delay an ACTION_DOWN for mMultiTapMaxDelay
                            // to ensure reachability of
                            // STATE_PANNING_SCALING(triggerable with ACTION_POINTER_DOWN)
                            || isActivated()) {

                        afterMultiTapTimeoutTransitionToDelegatingState();

                    } else {

                        // Delegate pending events without delay
                        transitionToDelegatingStateAndClear();
                    }
                }
                break;
                case ACTION_POINTER_DOWN: {
                    mIsTwoFingerCountReached = mDetectTwoFingerTripleTap
                            && event.getPointerCount() == 2;
                    mHandler.removeMessages(MESSAGE_TRANSITION_TO_DELEGATING_STATE);

                    if (isActivated() && event.getPointerCount() == 2) {
                        storePointerDownLocation(mSecondPointerDownLocation, event);
                        mHandler.sendEmptyMessageDelayed(MESSAGE_TRANSITION_TO_PANNINGSCALING_STATE,
                                ViewConfiguration.getTapTimeout());
                    } else if (mIsTwoFingerCountReached) {
                        // Placing two-finger triple-taps behind isActivated to avoid
                        // blocking panning scaling state
                        if (isMultiFingerMultiTapTriggered(/* targetTapCount= */ 2, event)) {
                            // 3tap and hold
                            afterLongTapTimeoutTransitionToDraggingState(event);
                        } else {
                            afterMultiTapTimeoutTransitionToDelegatingState();
                        }
                    } else {
                        transitionToDelegatingStateAndClear();
                    }
                }
                break;
                case ACTION_POINTER_UP: {
                    // If it is a two-finger gesture, do not transition to the delegating state
                    // to ensure the reachability of
                    // the two-finger triple tap (triggerable with ACTION_MOVE and ACTION_UP)
                    if (!mIsTwoFingerCountReached) {
                        transitionToDelegatingStateAndClear();
                    }
                }
                break;
                case ACTION_MOVE: {
                    if (isFingerDown()
                            && distance(mLastDown, /* move */ event) > mSwipeMinDistance) {
                        // Swipe detected - transition immediately

                        // 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 */) && event.getPointerCount() == 1) {
                            transitionToViewportDraggingStateAndClear(event);
                        } else if (isActivated() && event.getPointerCount() == 2) {
                            if (mIsSinglePanningEnabled
                                    && overscrollState(event, mFirstPointerDownLocation)
                                    == OVERSCROLL_VERTICAL_EDGE) {
                                transitionToDelegatingStateAndClear();
                            }
                            //Primary pointer is swiping, so transit to PanningScalingState
                            transitToPanningScalingStateAndClear();
                        } else if (isMultiFingerMultiTapTriggered(/* targetTapCount= */ 2, event)
                                && event.getPointerCount() == 2) {
                            // Placing two-finger triple-taps behind isActivated to avoid
                            // blocking panning scaling state
                            transitionToViewportDraggingStateAndClear(event);
                        } else if (mIsSinglePanningEnabled
                                && isActivated()
                                && event.getPointerCount() == 1) {
                            if (overscrollState(event, mFirstPointerDownLocation)
                                    == OVERSCROLL_VERTICAL_EDGE) {
                                transitionToDelegatingStateAndClear();
                            }
                            transitToSinglePanningStateAndClear();
                        } else {
                            transitionToDelegatingStateAndClear();
                        }
                    } else if (isActivated() && pointerDownValid(mSecondPointerDownLocation)
                            && distanceClosestPointerToPoint(
                            mSecondPointerDownLocation, /* move */ event) > mSwipeMinDistance) {
                        //Second pointer is swiping, so transit to PanningScalingState
                        transitToPanningScalingStateAndClear();
                    }
                }
                break;
                case ACTION_UP: {

                    mHandler.removeMessages(MESSAGE_ON_TRIPLE_TAP_AND_HOLD);

                    if (!mFullScreenMagnificationController.magnificationRegionContains(
                            mDisplayId, event.getX(), event.getY())) {
                        transitionToDelegatingStateAndClear();

                    } else if (isMultiTapTriggered(3 /* taps */)) {
                        onTripleTap(/* up */ event);

                    } else if (isMultiFingerMultiTapTriggered(/* targetTapCount= */ 3, event)) {
                        onTripleTap(event);

                    } else if (
                            // Possible to be false on: 3tap&drag -> scale -> PTR_UP -> UP
                            isFingerDown()
                            //TODO long tap should never happen here
                            && ((timeBetween(mLastDown, mLastUp) >= mLongTapMinDelay)
                                    || (distance(mLastDown, mLastUp) >= mSwipeMinDistance))
                            // If it is a two-finger but not reach 3 tap, do not transition to the
                            // delegating state to ensure the reachability of the triple tap
                            && mCompletedTapCount == 0) {
                        transitionToDelegatingStateAndClear();

                    }
                }
                break;
            }
        }
        // LINT.ThenChange(:detecting_state)

        @Override
        public void clear() {
            mCompletedTapCount = 0;
            setShortcutTriggered(false);
            removePendingDelayedMessages();
            clearDelayedMotionEvents();
            mFirstPointerDownLocation.set(Float.NaN, Float.NaN);
            mSecondPointerDownLocation.set(Float.NaN, Float.NaN);
        }

        private boolean isMultiFingerMultiTapTriggered(int targetTapCount, MotionEvent event) {
            if (event.getActionMasked() == ACTION_UP && mIsTwoFingerCountReached) {
                mCompletedTapCount++;
                mIsTwoFingerCountReached = false;
            }
            return mDetectTwoFingerTripleTap && mCompletedTapCount == targetTapCount;
        }

        void transitionToDelegatingStateAndClear() {
            mCompletedTapCount = 0;
            transitionTo(mDelegatingState);
            sendDelayedMotionEvents();
            removePendingDelayedMessages();
            mFirstPointerDownLocation.set(Float.NaN, Float.NaN);
            mSecondPointerDownLocation.set(Float.NaN, Float.NaN);
        }
    }

    /**
     * This class handles motion events when the event dispatch has not yet
     * determined what the user is doing. It watches for various tap events.
     */
    final class DetectingState implements State, Handler.Callback {
    class DetectingState implements State, Handler.Callback {

        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;
        protected static final int MESSAGE_ON_TRIPLE_TAP_AND_HOLD = 1;
        protected static final int MESSAGE_TRANSITION_TO_DELEGATING_STATE = 2;
        protected static final int MESSAGE_TRANSITION_TO_PANNINGSCALING_STATE = 3;

        final int mLongTapMinDelay;
        final int mSwipeMinDistance;
        final int mMultiTapMaxDelay;
        final int mMultiTapMaxDistance;

        private MotionEventInfo mDelayedEventQueue;
        MotionEvent mLastDown;
        private MotionEvent mPreLastDown;
        private MotionEvent mLastUp;
        private MotionEvent mPreLastUp;
        private PointF mSecondPointerDownLocation = new PointF(Float.NaN, Float.NaN);
        protected MotionEventInfo mDelayedEventQueue;
        protected MotionEvent mLastDown;
        protected MotionEvent mPreLastDown;
        protected MotionEvent mLastUp;
        protected MotionEvent mPreLastUp;

        private long mLastDetectingDownEventTime;
        protected PointF mFirstPointerDownLocation = new PointF(Float.NaN, Float.NaN);
        protected PointF mSecondPointerDownLocation = new PointF(Float.NaN, Float.NaN);
        protected long mLastDetectingDownEventTime;

        @VisibleForTesting boolean mShortcutTriggered;

        @VisibleForTesting Handler mHandler = new Handler(Looper.getMainLooper(), this);

        private PointF mFirstPointerDownLocation = new PointF(Float.NaN, Float.NaN);

        DetectingState(Context context) {
            mLongTapMinDelay = ViewConfiguration.getLongPressTimeout();
            mMultiTapMaxDelay = ViewConfiguration.getDoubleTapTimeout()
@@ -855,6 +1102,7 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
            return true;
        }

        // LINT.IfChange(detecting_state)
        @Override
        public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
            cacheDelayedMotionEvent(event, rawEvent, policyFlags);
@@ -969,23 +1217,24 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
                break;
            }
        }
        // LINT.ThenChange(:detecting_state_with_multi_finger)

        private void storePointerDownLocation(PointF pointerDownLocation, MotionEvent event) {
        protected void storePointerDownLocation(PointF pointerDownLocation, MotionEvent event) {
            final int index = event.getActionIndex();
            pointerDownLocation.set(event.getX(index), event.getY(index));
        }

        private boolean pointerDownValid(PointF pointerDownLocation) {
        protected boolean pointerDownValid(PointF pointerDownLocation) {
            return !(Float.isNaN(pointerDownLocation.x) && Float.isNaN(
                    pointerDownLocation.y));
        }

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

        private void transitToSinglePanningStateAndClear() {
        protected void transitToSinglePanningStateAndClear() {
            transitionTo(mSinglePanningState);
            clear();
        }
@@ -1016,7 +1265,7 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
            return mLastDown != null;
        }

        private long timeBetween(@Nullable MotionEvent a, @Nullable MotionEvent b) {
        protected long timeBetween(@Nullable MotionEvent a, @Nullable MotionEvent b) {
            if (a == null && b == null) return 0;
            return abs(timeOf(a) - timeOf(b));
        }
@@ -1061,13 +1310,13 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
            mSecondPointerDownLocation.set(Float.NaN, Float.NaN);
        }

        private void removePendingDelayedMessages() {
        protected 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,
        protected void cacheDelayedMotionEvent(MotionEvent event, MotionEvent rawEvent,
                int policyFlags) {
            if (event.getActionMasked() == ACTION_DOWN) {
                mPreLastDown = mLastDown;
@@ -1090,7 +1339,7 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
            }
        }

        private void sendDelayedMotionEvents() {
        protected void sendDelayedMotionEvents() {
            if (mDelayedEventQueue == null) {
                return;
            }
@@ -1112,7 +1361,7 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
            } while (mDelayedEventQueue != null);
        }

        private void clearDelayedMotionEvents() {
        protected void clearDelayedMotionEvents() {
            while (mDelayedEventQueue != null) {
                MotionEventInfo info = mDelayedEventQueue;
                mDelayedEventQueue = info.mNext;
@@ -1136,7 +1385,7 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
         *      1. direct three tap gesture
         *      2. one tap while shortcut triggered (it counts as two taps).
         */
        private void onTripleTap(MotionEvent up) {
        protected void onTripleTap(MotionEvent up) {
            if (DEBUG_DETECTING) {
                Slog.i(mLogTag, "onTripleTap(); delayed: "
                        + MotionEventInfo.toString(mDelayedEventQueue));
@@ -1156,7 +1405,7 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
            clear();
        }

        private boolean isActivated() {
        protected boolean isActivated() {
            return mFullScreenMagnificationController.isActivated(mDisplayId);
        }

@@ -1167,6 +1416,8 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH

            // Only log the 3tap and hold event
            if (!shortcutTriggered) {
                // TODO:(b/309534286): Add metrics for two-finger triple-tap and fix
                //  the log two-finger bug before enabling the flag
                // Triple tap and hold also belongs to triple tap event
                final boolean enabled = !isActivated();
                mMagnificationLogger.logMagnificationTripleTap(enabled);
+93 −0

File changed.

Preview size limit exceeded, changes collapsed.