diff options
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); |