Loading services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java +106 −0 Original line number Diff line number Diff line Loading @@ -245,6 +245,31 @@ public class FullScreenMagnificationController implements 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") float getCenterX() { return (mMagnificationBounds.width() / 2.0f Loading Loading @@ -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 * is in progress, this reflects the end state of the animation. Loading services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java +107 −14 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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, Loading Loading @@ -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); Loading @@ -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); Loading @@ -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); Loading Loading @@ -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()) { Loading Loading @@ -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() Loading Loading @@ -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())) { Loading Loading @@ -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 { Loading @@ -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 Loading @@ -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 Loading @@ -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 ( Loading @@ -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(); } Loading @@ -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() { Loading @@ -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 Loading Loading @@ -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); } Loading Loading @@ -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 + '}'; } Loading Loading @@ -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) + "}"; } } } services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java +111 −5 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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; Loading Loading @@ -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; Loading Loading @@ -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); Loading Loading @@ -187,6 +197,14 @@ public class FullScreenMagnificationGestureHandlerTest { 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(); Loading Loading @@ -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) { Loading @@ -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); Loading Loading @@ -525,6 +545,75 @@ public class FullScreenMagnificationGestureHandlerTest { 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 public void testShortcutTriggered_invokeShowWindowPromptAction() { goFromStateIdleTo(STATE_SHORTCUT_TRIGGERED); Loading Loading @@ -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); } } Loading Loading @@ -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); } Loading Loading @@ -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); } } Loading Loading @@ -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); Loading Loading
services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java +106 −0 Original line number Diff line number Diff line Loading @@ -245,6 +245,31 @@ public class FullScreenMagnificationController implements 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") float getCenterX() { return (mMagnificationBounds.width() / 2.0f Loading Loading @@ -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 * is in progress, this reflects the end state of the animation. Loading
services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java +107 −14 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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, Loading Loading @@ -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); Loading @@ -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); Loading @@ -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); Loading Loading @@ -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()) { Loading Loading @@ -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() Loading Loading @@ -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())) { Loading Loading @@ -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 { Loading @@ -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 Loading @@ -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 Loading @@ -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 ( Loading @@ -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(); } Loading @@ -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() { Loading @@ -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 Loading Loading @@ -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); } Loading Loading @@ -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 + '}'; } Loading Loading @@ -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) + "}"; } } }
services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java +111 −5 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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; Loading Loading @@ -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; Loading Loading @@ -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); Loading Loading @@ -187,6 +197,14 @@ public class FullScreenMagnificationGestureHandlerTest { 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(); Loading Loading @@ -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) { Loading @@ -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); Loading Loading @@ -525,6 +545,75 @@ public class FullScreenMagnificationGestureHandlerTest { 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 public void testShortcutTriggered_invokeShowWindowPromptAction() { goFromStateIdleTo(STATE_SHORTCUT_TRIGGERED); Loading Loading @@ -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); } } Loading Loading @@ -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); } Loading Loading @@ -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); } } Loading Loading @@ -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); Loading