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

Commit 2d4f78e3 authored by ktanojo's avatar ktanojo
Browse files

Create SinglePanningState that will handle single pointer/finger panning

In magnification feature user will be able to pan with one
pointer/finger and only once the user is at the edge it will transition
to DetectingState which will determine whether the user has overscrolled
or not. If it has overscrolled (user is trying to pan further in the
direction of the edge) transition to delegatingState else return back to
SinglePanningState

Bug: 278270879, 279498927
Test: atest FullScreenMagnficationGestureHandlerTest
Change-Id: Ie706ce00a22d9e9adf833c3b86bca686a13d27ce
(cherry picked from commit 30943ffaedbd85232cf6f9b6b12ca2c5219b8212)
parent b9064cdc
Loading
Loading
Loading
Loading
+106 −0
Original line number Original line Diff line number Diff line
@@ -245,6 +245,31 @@ public class FullScreenMagnificationController implements
            return mCurrentMagnificationSpec.offsetY;
            return mCurrentMagnificationSpec.offsetY;
        }
        }


        @GuardedBy("mLock")
        boolean isAtEdge() {
            return isAtLeftEdge() || isAtRightEdge() || isAtTopEdge() || isAtBottomEdge();
        }

        @GuardedBy("mLock")
        boolean isAtLeftEdge() {
            return getOffsetX() == getMaxOffsetXLocked();
        }

        @GuardedBy("mLock")
        boolean isAtRightEdge() {
            return getOffsetX() == getMinOffsetXLocked();
        }

        @GuardedBy("mLock")
        boolean isAtTopEdge() {
            return getOffsetY() == getMaxOffsetYLocked();
        }

        @GuardedBy("mLock")
        boolean isAtBottomEdge() {
            return getOffsetY() == getMinOffsetYLocked();
        }

        @GuardedBy("mLock")
        @GuardedBy("mLock")
        float getCenterX() {
        float getCenterX() {
            return (mMagnificationBounds.width() / 2.0f
            return (mMagnificationBounds.width() / 2.0f
@@ -1085,6 +1110,87 @@ public class FullScreenMagnificationController implements
        }
        }
    }
    }


    /**
     * Returns whether the user is at one of the edges (left, right, top, bottom)
     * of the magnification viewport
     *
     * @param displayId
     * @return if user is at the edge of the view
     */
    public boolean isAtEdge(int displayId) {
        synchronized (mLock) {
            final DisplayMagnification display = mDisplays.get(displayId);
            if (display == null) {
                return false;
            }
            return display.isAtEdge();
        }
    }

    /**
     * Returns whether the user is at the left edge of the viewport
     *
     * @param displayId
     * @return if user is at left edge of view
     */
    public boolean isAtLeftEdge(int displayId) {
        synchronized (mLock) {
            final DisplayMagnification display = mDisplays.get(displayId);
            if (display == null) {
                return false;
            }
            return display.isAtLeftEdge();
        }
    }

    /**
     * Returns whether the user is at the right edge of the viewport
     *
     * @param displayId
     * @return if user is at right edge of view
     */
    public boolean isAtRightEdge(int displayId) {
        synchronized (mLock) {
            final DisplayMagnification display = mDisplays.get(displayId);
            if (display == null) {
                return false;
            }
            return display.isAtRightEdge();
        }
    }

    /**
     * Returns whether the user is at the top edge of the viewport
     *
     * @param displayId
     * @return if user is at top edge of view
     */
    public boolean isAtTopEdge(int displayId) {
        synchronized (mLock) {
            final DisplayMagnification display = mDisplays.get(displayId);
            if (display == null) {
                return false;
            }
            return display.isAtTopEdge();
        }
    }

    /**
     * Returns whether the user is at the bottom edge of the viewport
     *
     * @param displayId
     * @return if user is at bottom edge of view
     */
    public boolean isAtBottomEdge(int displayId) {
        synchronized (mLock) {
            final DisplayMagnification display = mDisplays.get(displayId);
            if (display == null) {
                return false;
            }
            return display.isAtBottomEdge();
        }
    }

    /**
    /**
     * Returns the Y offset of the magnification viewport. If an animation
     * Returns the Y offset of the magnification viewport. If an animation
     * is in progress, this reflects the end state of the animation.
     * is in progress, this reflects the end state of the animation.
+107 −14
Original line number Original line Diff line number Diff line
@@ -137,6 +137,7 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
    @VisibleForTesting final DetectingState mDetectingState;
    @VisibleForTesting final DetectingState mDetectingState;
    @VisibleForTesting final PanningScalingState mPanningScalingState;
    @VisibleForTesting final PanningScalingState mPanningScalingState;
    @VisibleForTesting final ViewportDraggingState mViewportDraggingState;
    @VisibleForTesting final ViewportDraggingState mViewportDraggingState;
    @VisibleForTesting final SinglePanningState mSinglePanningState;


    private final ScreenStateReceiver mScreenStateReceiver;
    private final ScreenStateReceiver mScreenStateReceiver;
    private final WindowMagnificationPromptController mPromptController;
    private final WindowMagnificationPromptController mPromptController;
@@ -146,7 +147,7 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH


    private PointerCoords[] mTempPointerCoords;
    private PointerCoords[] mTempPointerCoords;
    private PointerProperties[] mTempPointerProperties;
    private PointerProperties[] mTempPointerProperties;

    @VisibleForTesting boolean mIsSinglePanningEnabled;
    public FullScreenMagnificationGestureHandler(@UiContext Context context,
    public FullScreenMagnificationGestureHandler(@UiContext Context context,
            FullScreenMagnificationController fullScreenMagnificationController,
            FullScreenMagnificationController fullScreenMagnificationController,
            AccessibilityTraceManager trace,
            AccessibilityTraceManager trace,
@@ -202,6 +203,8 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
        mDetectingState = new DetectingState(context);
        mDetectingState = new DetectingState(context);
        mViewportDraggingState = new ViewportDraggingState();
        mViewportDraggingState = new ViewportDraggingState();
        mPanningScalingState = new PanningScalingState(context);
        mPanningScalingState = new PanningScalingState(context);
        mSinglePanningState = new SinglePanningState(context);
        setSinglePanningEnabled(false);


        if (mDetectShortcutTrigger) {
        if (mDetectShortcutTrigger) {
            mScreenStateReceiver = new ScreenStateReceiver(context, this);
            mScreenStateReceiver = new ScreenStateReceiver(context, this);
@@ -213,6 +216,11 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
        transitionTo(mDetectingState);
        transitionTo(mDetectingState);
    }
    }


    @VisibleForTesting
    void setSinglePanningEnabled(boolean isEnabled) {
        mIsSinglePanningEnabled = isEnabled;
    }

    @Override
    @Override
    void onMotionEventInternal(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
    void onMotionEventInternal(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
        handleEventWith(mCurrentState, event, rawEvent, policyFlags);
        handleEventWith(mCurrentState, event, rawEvent, policyFlags);
@@ -223,6 +231,7 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
        // To keep InputEventConsistencyVerifiers within GestureDetectors happy
        // To keep InputEventConsistencyVerifiers within GestureDetectors happy
        mPanningScalingState.mScrollGestureDetector.onTouchEvent(event);
        mPanningScalingState.mScrollGestureDetector.onTouchEvent(event);
        mPanningScalingState.mScaleGestureDetector.onTouchEvent(event);
        mPanningScalingState.mScaleGestureDetector.onTouchEvent(event);
        mSinglePanningState.mScrollGestureDetector.onTouchEvent(event);


        try {
        try {
            stateHandler.onMotionEvent(event, rawEvent, policyFlags);
            stateHandler.onMotionEvent(event, rawEvent, policyFlags);
@@ -669,7 +678,6 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH


        @Override
        @Override
        public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
        public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {

            // Ensures that the state at the end of delegation is consistent with the last delegated
            // Ensures that the state at the end of delegation is consistent with the last delegated
            // UP/DOWN event in queue: still delegating if pointer is down, detecting otherwise
            // UP/DOWN event in queue: still delegating if pointer is down, detecting otherwise
            switch (event.getActionMasked()) {
            switch (event.getActionMasked()) {
@@ -726,6 +734,8 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH


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


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

        DetectingState(Context context) {
        DetectingState(Context context) {
            mLongTapMinDelay = ViewConfiguration.getLongPressTimeout();
            mLongTapMinDelay = ViewConfiguration.getLongPressTimeout();
            mMultiTapMaxDelay = ViewConfiguration.getDoubleTapTimeout()
            mMultiTapMaxDelay = ViewConfiguration.getDoubleTapTimeout()
@@ -765,10 +775,11 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
            cacheDelayedMotionEvent(event, rawEvent, policyFlags);
            cacheDelayedMotionEvent(event, rawEvent, policyFlags);
            switch (event.getActionMasked()) {
            switch (event.getActionMasked()) {
                case MotionEvent.ACTION_DOWN: {
                case MotionEvent.ACTION_DOWN: {

                    mLastDetectingDownEventTime = event.getDownTime();
                    mLastDetectingDownEventTime = event.getDownTime();
                    mHandler.removeMessages(MESSAGE_TRANSITION_TO_DELEGATING_STATE);
                    mHandler.removeMessages(MESSAGE_TRANSITION_TO_DELEGATING_STATE);


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

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


@@ -800,7 +811,7 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
                break;
                break;
                case ACTION_POINTER_DOWN: {
                case ACTION_POINTER_DOWN: {
                    if (isActivated() && event.getPointerCount() == 2) {
                    if (isActivated() && event.getPointerCount() == 2) {
                        storeSecondPointerDownLocation(event);
                        storePointerDownLocation(mSecondPointerDownLocation, event);
                        mHandler.sendEmptyMessageDelayed(MESSAGE_TRANSITION_TO_PANNINGSCALING_STATE,
                        mHandler.sendEmptyMessageDelayed(MESSAGE_TRANSITION_TO_PANNINGSCALING_STATE,
                                ViewConfiguration.getTapTimeout());
                                ViewConfiguration.getTapTimeout());
                    } else {
                    } else {
@@ -815,7 +826,6 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
                case ACTION_MOVE: {
                case ACTION_MOVE: {
                    if (isFingerDown()
                    if (isFingerDown()
                            && distance(mLastDown, /* move */ event) > mSwipeMinDistance) {
                            && distance(mLastDown, /* move */ event) > mSwipeMinDistance) {

                        // Swipe detected - transition immediately
                        // Swipe detected - transition immediately


                        // For convenience, viewport dragging takes precedence
                        // For convenience, viewport dragging takes precedence
@@ -826,10 +836,15 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
                        } else if (isActivated() && event.getPointerCount() == 2) {
                        } else if (isActivated() && event.getPointerCount() == 2) {
                            //Primary pointer is swiping, so transit to PanningScalingState
                            //Primary pointer is swiping, so transit to PanningScalingState
                            transitToPanningScalingStateAndClear();
                            transitToPanningScalingStateAndClear();
                        } else if (mIsSinglePanningEnabled
                                && isActivated()
                                && event.getPointerCount() == 1
                                && !isOverscroll(event)) {
                            transitToSinglePanningStateAndClear();
                        } else {
                        } else {
                            transitionToDelegatingStateAndClear();
                            transitionToDelegatingStateAndClear();
                        }
                        }
                    } else if (isActivated() && secondPointerDownValid()
                    } else if (isActivated() && pointerDownValid(mSecondPointerDownLocation)
                            && distanceClosestPointerToPoint(
                            && distanceClosestPointerToPoint(
                            mSecondPointerDownLocation, /* move */ event) > mSwipeMinDistance) {
                            mSecondPointerDownLocation, /* move */ event) > mSwipeMinDistance) {
                        //Second pointer is swiping, so transit to PanningScalingState
                        //Second pointer is swiping, so transit to PanningScalingState
@@ -843,11 +858,9 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH


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

                        transitionToDelegatingStateAndClear();
                        transitionToDelegatingStateAndClear();


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

                        onTripleTap(/* up */ event);
                        onTripleTap(/* up */ event);


                    } else if (
                    } else if (
@@ -856,7 +869,6 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
                            //TODO long tap should never happen here
                            //TODO long tap should never happen here
                            && ((timeBetween(mLastDown, mLastUp) >= mLongTapMinDelay)
                            && ((timeBetween(mLastDown, mLastUp) >= mLongTapMinDelay)
                                    || (distance(mLastDown, mLastUp) >= mSwipeMinDistance))) {
                                    || (distance(mLastDown, mLastUp) >= mSwipeMinDistance))) {

                        transitionToDelegatingStateAndClear();
                        transitionToDelegatingStateAndClear();


                    }
                    }
@@ -865,14 +877,28 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
            }
            }
        }
        }


        private void storeSecondPointerDownLocation(MotionEvent event) {
        private boolean isOverscroll(MotionEvent event) {
            if (!pointerDownValid(mFirstPointerDownLocation)) {
                return false;
            }
            float dX = event.getX() - mFirstPointerDownLocation.x;
            float dY = event.getY() - mFirstPointerDownLocation.y;
            boolean didOverscroll =
                    mFullScreenMagnificationController.isAtLeftEdge(mDisplayId) && dX > 0
                    || mFullScreenMagnificationController.isAtRightEdge(mDisplayId) && dX < 0
                    || mFullScreenMagnificationController.isAtTopEdge(mDisplayId) && dY > 0
                    || mFullScreenMagnificationController.isAtBottomEdge(mDisplayId) && dY < 0;
            return didOverscroll;
        }

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


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


        private void transitToPanningScalingStateAndClear() {
        private void transitToPanningScalingStateAndClear() {
@@ -880,6 +906,11 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
            clear();
            clear();
        }
        }


        private void transitToSinglePanningStateAndClear() {
            transitionTo(mSinglePanningState);
            clear();
        }

        public boolean isMultiTapTriggered(int numTaps) {
        public boolean isMultiTapTriggered(int numTaps) {


            // Shortcut acts as the 2 initial taps
            // Shortcut acts as the 2 initial taps
@@ -947,6 +978,7 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
            setShortcutTriggered(false);
            setShortcutTriggered(false);
            removePendingDelayedMessages();
            removePendingDelayedMessages();
            clearDelayedMotionEvents();
            clearDelayedMotionEvents();
            mFirstPointerDownLocation.set(Float.NaN, Float.NaN);
            mSecondPointerDownLocation.set(Float.NaN, Float.NaN);
            mSecondPointerDownLocation.set(Float.NaN, Float.NaN);
        }
        }


@@ -1165,12 +1197,14 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
                + ", mDelegatingState=" + mDelegatingState
                + ", mDelegatingState=" + mDelegatingState
                + ", mMagnifiedInteractionState=" + mPanningScalingState
                + ", mMagnifiedInteractionState=" + mPanningScalingState
                + ", mViewportDraggingState=" + mViewportDraggingState
                + ", mViewportDraggingState=" + mViewportDraggingState
                + ", mSinglePanningState=" + mSinglePanningState
                + ", mDetectTripleTap=" + mDetectTripleTap
                + ", mDetectTripleTap=" + mDetectTripleTap
                + ", mDetectShortcutTrigger=" + mDetectShortcutTrigger
                + ", mDetectShortcutTrigger=" + mDetectShortcutTrigger
                + ", mCurrentState=" + State.nameOf(mCurrentState)
                + ", mCurrentState=" + State.nameOf(mCurrentState)
                + ", mPreviousState=" + State.nameOf(mPreviousState)
                + ", mPreviousState=" + State.nameOf(mPreviousState)
                + ", mMagnificationController=" + mFullScreenMagnificationController
                + ", mMagnificationController=" + mFullScreenMagnificationController
                + ", mDisplayId=" + mDisplayId
                + ", mDisplayId=" + mDisplayId
                + ", mIsSinglePanningEnabled=" + mIsSinglePanningEnabled
                + '}';
                + '}';
    }
    }


@@ -1285,8 +1319,67 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
     * Indicates an error with a gesture handler or state.
     * Indicates an error with a gesture handler or state.
     */
     */
    private static class GestureException extends Exception {
    private static class GestureException extends Exception {

        GestureException(String message) {
        GestureException(String message) {
            super(message);
            super(message);
        }
        }
    }
    }

    final class SinglePanningState extends SimpleOnGestureListener implements State {
        private final GestureDetector mScrollGestureDetector;
        private MotionEventInfo mEvent;

        SinglePanningState(Context context) {
            mScrollGestureDetector = new GestureDetector(context, this, Handler.getMain());
        }

        @Override
        public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
            int action = event.getActionMasked();
            switch (action) {
                case ACTION_UP:
                case ACTION_CANCEL:
                    clear();
                    transitionTo(mDetectingState);
                    break;
            }
        }

        @Override
        public boolean onScroll(
                MotionEvent first, MotionEvent second, float distanceX, float distanceY) {
            if (mCurrentState != mSinglePanningState) {
                return true;
            }
            mFullScreenMagnificationController.offsetMagnifiedRegion(
                    mDisplayId,
                    distanceX,
                    distanceY,
                    AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID);
            if (DEBUG_PANNING_SCALING) {
                Slog.i(
                        mLogTag,
                        "SinglePanningState Panned content by scrollX: "
                                + distanceX
                                + " scrollY: "
                                + distanceY
                                + " isAtEdge: "
                                + mFullScreenMagnificationController.isAtEdge(mDisplayId));
            }
            // TODO: b/280812104 Dispatch events before Delegation
            if (mFullScreenMagnificationController.isAtEdge(mDisplayId)) {
                clear();
                transitionTo(mDelegatingState);
            }
            return /* event consumed: */ true;
        }

        @Override
        public String toString() {
            return "SinglePanningState{"
                    + "isEdgeOfView="
                    + mFullScreenMagnificationController.isAtEdge(mDisplayId)
                    + "}";
        }
    }
}
}
+111 −5
Original line number Original line Diff line number Diff line
@@ -33,6 +33,7 @@ import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.times;
@@ -42,6 +43,8 @@ import static org.mockito.Mockito.when;
import android.animation.ValueAnimator;
import android.animation.ValueAnimator;
import android.annotation.NonNull;
import android.annotation.NonNull;
import android.graphics.PointF;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.Region;
import android.os.Handler;
import android.os.Handler;
import android.os.Message;
import android.os.Message;
import android.os.UserHandle;
import android.os.UserHandle;
@@ -67,11 +70,13 @@ import com.android.server.wm.WindowManagerInternal;


import org.junit.After;
import org.junit.After;
import org.junit.Before;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.MockitoAnnotations;
import org.mockito.stubbing.Answer;


import java.util.ArrayList;
import java.util.ArrayList;
import java.util.List;
import java.util.List;
@@ -123,9 +128,10 @@ public class FullScreenMagnificationGestureHandlerTest {
    public static final int STATE_SHORTCUT_TRIGGERED_ZOOMED_TMP = 8;
    public static final int STATE_SHORTCUT_TRIGGERED_ZOOMED_TMP = 8;
    public static final int STATE_PANNING = 9;
    public static final int STATE_PANNING = 9;
    public static final int STATE_SCALING_AND_PANNING = 10;
    public static final int STATE_SCALING_AND_PANNING = 10;
    public static final int STATE_SINGLE_PANNING = 11;


    public static final int FIRST_STATE = STATE_IDLE;
    public static final int FIRST_STATE = STATE_IDLE;
    public static final int LAST_STATE = STATE_SCALING_AND_PANNING;
    public static final int LAST_STATE = STATE_SINGLE_PANNING;


    // Co-prime x and y, to potentially catch x-y-swapped errors
    // Co-prime x and y, to potentially catch x-y-swapped errors
    public static final float DEFAULT_X = 301;
    public static final float DEFAULT_X = 301;
@@ -155,6 +161,10 @@ public class FullScreenMagnificationGestureHandlerTest {


    private float mOriginalMagnificationPersistedScale;
    private float mOriginalMagnificationPersistedScale;


    static final Rect INITIAL_MAGNIFICATION_BOUNDS = new Rect(0, 0, 800, 800);

    static final Region INITIAL_MAGNIFICATION_REGION = new Region(INITIAL_MAGNIFICATION_BOUNDS);

    @Before
    @Before
    public void setUp() {
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        MockitoAnnotations.initMocks(this);
@@ -187,6 +197,14 @@ public class FullScreenMagnificationGestureHandlerTest {
                    return true;
                    return true;
                }
                }
        };
        };

        doAnswer((Answer<Void>) invocationOnMock -> {
            Object[] args = invocationOnMock.getArguments();
            Region regionArg = (Region) args[1];
            regionArg.set(new Rect(INITIAL_MAGNIFICATION_BOUNDS));
            return null;
        }).when(mockWindowManager).getMagnificationRegion(anyInt(), any(Region.class));

        mFullScreenMagnificationController.register(DISPLAY_0);
        mFullScreenMagnificationController.register(DISPLAY_0);
        mFullScreenMagnificationController.setAlwaysOnMagnificationEnabled(true);
        mFullScreenMagnificationController.setAlwaysOnMagnificationEnabled(true);
        mClock = new OffsettableClock.Stopped();
        mClock = new OffsettableClock.Stopped();
@@ -214,6 +232,7 @@ public class FullScreenMagnificationGestureHandlerTest {
                mContext, mFullScreenMagnificationController, mMockTraceManager, mMockCallback,
                mContext, mFullScreenMagnificationController, mMockTraceManager, mMockCallback,
                detectTripleTap, detectShortcutTrigger,
                detectTripleTap, detectShortcutTrigger,
                mWindowMagnificationPromptController, DISPLAY_0);
                mWindowMagnificationPromptController, DISPLAY_0);
        h.setSinglePanningEnabled(true);
        mHandler = new TestHandler(h.mDetectingState, mClock) {
        mHandler = new TestHandler(h.mDetectingState, mClock) {
            @Override
            @Override
            protected String messageToString(Message m) {
            protected String messageToString(Message m) {
@@ -239,6 +258,7 @@ public class FullScreenMagnificationGestureHandlerTest {
     * {@link #returnToNormalFrom} (for navigating back to {@link #STATE_IDLE})
     * {@link #returnToNormalFrom} (for navigating back to {@link #STATE_IDLE})
     */
     */
    @Test
    @Test
    @Ignore("b/291925580")
    public void testEachState_isReachableAndRecoverable() {
    public void testEachState_isReachableAndRecoverable() {
        forEachState(state -> {
        forEachState(state -> {
            goFromStateIdleTo(state);
            goFromStateIdleTo(state);
@@ -525,6 +545,75 @@ public class FullScreenMagnificationGestureHandlerTest {
        verify(mWindowMagnificationPromptController).showNotificationIfNeeded();
        verify(mWindowMagnificationPromptController).showNotificationIfNeeded();
    }
    }


    @Test
    public void testActionUpNotAtEdge_singlePanningState_detectingState() {
        goFromStateIdleTo(STATE_SINGLE_PANNING);

        send(upEvent());

        check(mMgh.mCurrentState == mMgh.mDetectingState, STATE_IDLE);
        assertTrue(isZoomed());
    }

    @Test
    public void testScroll_SinglePanningDisabled_delegatingState() {
        mMgh.setSinglePanningEnabled(false);

        goFromStateIdleTo(STATE_ACTIVATED);
        allowEventDelegation();
        swipeAndHold();

        assertTrue(mMgh.mCurrentState == mMgh.mDelegatingState);
    }

    @Test
    public void testScroll_zoomedStateAndAtEdge_delegatingState() {
        goFromStateIdleTo(STATE_ACTIVATED);
        mFullScreenMagnificationController.setCenter(
                DISPLAY_0,
                INITIAL_MAGNIFICATION_BOUNDS.left,
                INITIAL_MAGNIFICATION_BOUNDS.top / 2,
                false,
                1);
        final float swipeMinDistance = ViewConfiguration.get(mContext).getScaledTouchSlop() + 1;
        PointF initCoords =
                new PointF(
                        mFullScreenMagnificationController.getCenterX(DISPLAY_0),
                        mFullScreenMagnificationController.getCenterY(DISPLAY_0));
        PointF endCoords = new PointF(initCoords.x, initCoords.y);
        endCoords.offset(swipeMinDistance, 0);
        allowEventDelegation();

        swipeAndHold(initCoords, endCoords);

        assertTrue(mMgh.mCurrentState == mMgh.mDelegatingState);
        assertTrue(isZoomed());
    }

    @Test
    public void testScroll_singlePanningAndAtEdge_delegatingState() {
        goFromStateIdleTo(STATE_SINGLE_PANNING);
        mFullScreenMagnificationController.setCenter(
                DISPLAY_0,
                INITIAL_MAGNIFICATION_BOUNDS.left,
                INITIAL_MAGNIFICATION_BOUNDS.top / 2,
                false,
                1);
        final float swipeMinDistance = ViewConfiguration.get(mContext).getScaledTouchSlop() + 1;
        PointF initCoords =
                new PointF(
                        mFullScreenMagnificationController.getCenterX(DISPLAY_0),
                        mFullScreenMagnificationController.getCenterY(DISPLAY_0));
        PointF endCoords = new PointF(initCoords.x, initCoords.y);
        endCoords.offset(swipeMinDistance, 0);
        allowEventDelegation();

        swipeAndHold(initCoords, endCoords);

        assertTrue(mMgh.mCurrentState == mMgh.mDelegatingState);
        assertTrue(isZoomed());
    }

    @Test
    @Test
    public void testShortcutTriggered_invokeShowWindowPromptAction() {
    public void testShortcutTriggered_invokeShowWindowPromptAction() {
        goFromStateIdleTo(STATE_SHORTCUT_TRIGGERED);
        goFromStateIdleTo(STATE_SHORTCUT_TRIGGERED);
@@ -740,6 +829,10 @@ public class FullScreenMagnificationGestureHandlerTest {
                        state);
                        state);
                check(mMgh.mPanningScalingState.mScaling, state);
                check(mMgh.mPanningScalingState.mScaling, state);
            } break;
            } break;
            case STATE_SINGLE_PANNING: {
                check(isZoomed(), state);
                check(mMgh.mCurrentState == mMgh.mSinglePanningState, state);
            } break;
            default: throw new IllegalArgumentException("Illegal state: " + state);
            default: throw new IllegalArgumentException("Illegal state: " + state);
        }
        }
    }
    }
@@ -803,6 +896,10 @@ public class FullScreenMagnificationGestureHandlerTest {
                    send(pointerEvent(ACTION_MOVE, DEFAULT_X * 2, DEFAULT_Y * 4));
                    send(pointerEvent(ACTION_MOVE, DEFAULT_X * 2, DEFAULT_Y * 4));
                    send(pointerEvent(ACTION_MOVE, DEFAULT_X * 2, DEFAULT_Y * 5));
                    send(pointerEvent(ACTION_MOVE, DEFAULT_X * 2, DEFAULT_Y * 5));
                } break;
                } break;
                case STATE_SINGLE_PANNING: {
                    goFromStateIdleTo(STATE_ACTIVATED);
                    swipeAndHold();
                } break;
                default:
                default:
                    throw new IllegalArgumentException("Illegal state: " + state);
                    throw new IllegalArgumentException("Illegal state: " + state);
            }
            }
@@ -859,6 +956,10 @@ public class FullScreenMagnificationGestureHandlerTest {
            case STATE_SCALING_AND_PANNING: {
            case STATE_SCALING_AND_PANNING: {
                returnToNormalFrom(STATE_PANNING);
                returnToNormalFrom(STATE_PANNING);
            } break;
            } break;
            case STATE_SINGLE_PANNING: {
                send(upEvent());
                returnToNormalFrom(STATE_ACTIVATED);
            } break;
            default: throw new IllegalArgumentException("Illegal state: " + state);
            default: throw new IllegalArgumentException("Illegal state: " + state);
        }
        }
    }
    }
@@ -906,6 +1007,11 @@ public class FullScreenMagnificationGestureHandlerTest {
        send(moveEvent(DEFAULT_X * 2, DEFAULT_Y * 2));
        send(moveEvent(DEFAULT_X * 2, DEFAULT_Y * 2));
    }
    }


    private void swipeAndHold(PointF start, PointF end) {
        send(downEvent(start.x, start.y));
        send(moveEvent(end.x, end.y));
    }

    private void longTap() {
    private void longTap() {
        send(downEvent());
        send(downEvent());
        fastForward(2000);
        fastForward(2000);