diff options
| author | 2023-04-20 22:06:49 +0000 | |
|---|---|---|
| committer | 2023-07-19 16:18:14 +0000 | |
| commit | 2d4f78e3fdc72e37b65e374fcc981cbc3b24a34b (patch) | |
| tree | a2f9805060145c3afb315e9108fe2de1a10f8621 | |
| parent | b9064cdc483213199f78babc810e313568d9ccef (diff) | |
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)
3 files changed, 324 insertions, 19 deletions
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java index 1482d078fa8c..3f3fa3419117 100644 --- a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java +++ b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java @@ -246,6 +246,31 @@ public class FullScreenMagnificationController implements } @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") float getCenterX() { return (mMagnificationBounds.width() / 2.0f + mMagnificationBounds.left - getOffsetX()) / getScale(); @@ -1086,6 +1111,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 * is in progress, this reflects the end state of the animation. * diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java index 038847e2a759..4aebbf11c7af 100644 --- a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java +++ b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java @@ -137,6 +137,7 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH @VisibleForTesting final DetectingState mDetectingState; @VisibleForTesting final PanningScalingState mPanningScalingState; @VisibleForTesting final ViewportDraggingState mViewportDraggingState; + @VisibleForTesting final SinglePanningState mSinglePanningState; private final ScreenStateReceiver mScreenStateReceiver; private final WindowMagnificationPromptController mPromptController; @@ -146,7 +147,7 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH private PointerCoords[] mTempPointerCoords; private PointerProperties[] mTempPointerProperties; - + @VisibleForTesting boolean mIsSinglePanningEnabled; public FullScreenMagnificationGestureHandler(@UiContext Context context, FullScreenMagnificationController fullScreenMagnificationController, AccessibilityTraceManager trace, @@ -202,6 +203,8 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH mDetectingState = new DetectingState(context); mViewportDraggingState = new ViewportDraggingState(); mPanningScalingState = new PanningScalingState(context); + mSinglePanningState = new SinglePanningState(context); + setSinglePanningEnabled(false); if (mDetectShortcutTrigger) { mScreenStateReceiver = new ScreenStateReceiver(context, this); @@ -213,6 +216,11 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH transitionTo(mDetectingState); } + @VisibleForTesting + void setSinglePanningEnabled(boolean isEnabled) { + mIsSinglePanningEnabled = isEnabled; + } + @Override void onMotionEventInternal(MotionEvent event, MotionEvent rawEvent, int policyFlags) { handleEventWith(mCurrentState, event, rawEvent, policyFlags); @@ -223,6 +231,7 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH // To keep InputEventConsistencyVerifiers within GestureDetectors happy mPanningScalingState.mScrollGestureDetector.onTouchEvent(event); mPanningScalingState.mScaleGestureDetector.onTouchEvent(event); + mSinglePanningState.mScrollGestureDetector.onTouchEvent(event); try { stateHandler.onMotionEvent(event, rawEvent, policyFlags); @@ -669,7 +678,6 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH @Override public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) { - // 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 switch (event.getActionMasked()) { @@ -726,6 +734,8 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH @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() @@ -765,10 +775,11 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH 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())) { @@ -800,7 +811,7 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH break; case ACTION_POINTER_DOWN: { if (isActivated() && event.getPointerCount() == 2) { - storeSecondPointerDownLocation(event); + storePointerDownLocation(mSecondPointerDownLocation, event); mHandler.sendEmptyMessageDelayed(MESSAGE_TRANSITION_TO_PANNINGSCALING_STATE, ViewConfiguration.getTapTimeout()); } else { @@ -815,7 +826,6 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH case ACTION_MOVE: { if (isFingerDown() && distance(mLastDown, /* move */ event) > mSwipeMinDistance) { - // Swipe detected - transition immediately // For convenience, viewport dragging takes precedence @@ -826,10 +836,15 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH } else if (isActivated() && event.getPointerCount() == 2) { //Primary pointer is swiping, so transit to PanningScalingState transitToPanningScalingStateAndClear(); + } else if (mIsSinglePanningEnabled + && isActivated() + && event.getPointerCount() == 1 + && !isOverscroll(event)) { + transitToSinglePanningStateAndClear(); } else { transitionToDelegatingStateAndClear(); } - } else if (isActivated() && secondPointerDownValid() + } else if (isActivated() && pointerDownValid(mSecondPointerDownLocation) && distanceClosestPointerToPoint( mSecondPointerDownLocation, /* move */ event) > mSwipeMinDistance) { //Second pointer is swiping, so transit to PanningScalingState @@ -843,11 +858,9 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH if (!mFullScreenMagnificationController.magnificationRegionContains( mDisplayId, event.getX(), event.getY())) { - transitionToDelegatingStateAndClear(); } else if (isMultiTapTriggered(3 /* taps */)) { - onTripleTap(/* up */ event); } else if ( @@ -856,7 +869,6 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH //TODO long tap should never happen here && ((timeBetween(mLastDown, mLastUp) >= mLongTapMinDelay) || (distance(mLastDown, mLastUp) >= mSwipeMinDistance))) { - 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(); - mSecondPointerDownLocation.set(event.getX(index), event.getY(index)); + pointerDownLocation.set(event.getX(index), event.getY(index)); } - private boolean secondPointerDownValid() { - return !(Float.isNaN(mSecondPointerDownLocation.x) && Float.isNaN( - mSecondPointerDownLocation.y)); + private boolean pointerDownValid(PointF pointerDownLocation) { + return !(Float.isNaN(pointerDownLocation.x) && Float.isNaN( + pointerDownLocation.y)); } private void transitToPanningScalingStateAndClear() { @@ -880,6 +906,11 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH clear(); } + private void transitToSinglePanningStateAndClear() { + transitionTo(mSinglePanningState); + clear(); + } + public boolean isMultiTapTriggered(int numTaps) { // Shortcut acts as the 2 initial taps @@ -947,6 +978,7 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH setShortcutTriggered(false); removePendingDelayedMessages(); clearDelayedMotionEvents(); + mFirstPointerDownLocation.set(Float.NaN, Float.NaN); mSecondPointerDownLocation.set(Float.NaN, Float.NaN); } @@ -1165,12 +1197,14 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH + ", mDelegatingState=" + mDelegatingState + ", mMagnifiedInteractionState=" + mPanningScalingState + ", mViewportDraggingState=" + mViewportDraggingState + + ", mSinglePanningState=" + mSinglePanningState + ", mDetectTripleTap=" + mDetectTripleTap + ", mDetectShortcutTrigger=" + mDetectShortcutTrigger + ", mCurrentState=" + State.nameOf(mCurrentState) + ", mPreviousState=" + State.nameOf(mPreviousState) + ", mMagnificationController=" + mFullScreenMagnificationController + ", mDisplayId=" + mDisplayId + + ", mIsSinglePanningEnabled=" + mIsSinglePanningEnabled + '}'; } @@ -1285,8 +1319,67 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH * Indicates an error with a gesture handler or state. */ private static class GestureException extends Exception { + GestureException(String 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) + + "}"; + } + } } diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java index 32d0c98d4481..989aee06a1df 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java @@ -33,6 +33,7 @@ import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyInt; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; @@ -42,6 +43,8 @@ import static org.mockito.Mockito.when; import android.animation.ValueAnimator; import android.annotation.NonNull; import android.graphics.PointF; +import android.graphics.Rect; +import android.graphics.Region; import android.os.Handler; import android.os.Message; import android.os.UserHandle; @@ -67,11 +70,13 @@ import com.android.server.wm.WindowManagerInternal; import org.junit.After; import org.junit.Before; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.mockito.stubbing.Answer; import java.util.ArrayList; 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_PANNING = 9; 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 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 public static final float DEFAULT_X = 301; @@ -155,6 +161,10 @@ public class FullScreenMagnificationGestureHandlerTest { 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 public void setUp() { MockitoAnnotations.initMocks(this); @@ -182,11 +192,19 @@ public class FullScreenMagnificationGestureHandlerTest { new MagnificationScaleProvider(mContext), () -> null, ConcurrentUtils.DIRECT_EXECUTOR) { - @Override - public boolean magnificationRegionContains(int displayId, float x, float y) { - return true; - } + @Override + public boolean magnificationRegionContains(int displayId, float x, float y) { + 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.setAlwaysOnMagnificationEnabled(true); mClock = new OffsettableClock.Stopped(); @@ -214,6 +232,7 @@ public class FullScreenMagnificationGestureHandlerTest { mContext, mFullScreenMagnificationController, mMockTraceManager, mMockCallback, detectTripleTap, detectShortcutTrigger, mWindowMagnificationPromptController, DISPLAY_0); + h.setSinglePanningEnabled(true); mHandler = new TestHandler(h.mDetectingState, mClock) { @Override protected String messageToString(Message m) { @@ -239,6 +258,7 @@ public class FullScreenMagnificationGestureHandlerTest { * {@link #returnToNormalFrom} (for navigating back to {@link #STATE_IDLE}) */ @Test + @Ignore("b/291925580") public void testEachState_isReachableAndRecoverable() { forEachState(state -> { goFromStateIdleTo(state); @@ -526,6 +546,75 @@ public class FullScreenMagnificationGestureHandlerTest { } @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 public void testShortcutTriggered_invokeShowWindowPromptAction() { goFromStateIdleTo(STATE_SHORTCUT_TRIGGERED); @@ -740,6 +829,10 @@ public class FullScreenMagnificationGestureHandlerTest { state); check(mMgh.mPanningScalingState.mScaling, state); } break; + case STATE_SINGLE_PANNING: { + check(isZoomed(), state); + check(mMgh.mCurrentState == mMgh.mSinglePanningState, state); + } break; 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 * 5)); } break; + case STATE_SINGLE_PANNING: { + goFromStateIdleTo(STATE_ACTIVATED); + swipeAndHold(); + } break; default: throw new IllegalArgumentException("Illegal state: " + state); } @@ -859,6 +956,10 @@ public class FullScreenMagnificationGestureHandlerTest { case STATE_SCALING_AND_PANNING: { returnToNormalFrom(STATE_PANNING); } break; + case STATE_SINGLE_PANNING: { + send(upEvent()); + returnToNormalFrom(STATE_ACTIVATED); + } break; default: throw new IllegalArgumentException("Illegal state: " + state); } } @@ -906,6 +1007,11 @@ public class FullScreenMagnificationGestureHandlerTest { 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() { send(downEvent()); fastForward(2000); |