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 58b61b36af172..c0967db47315c 100644 --- a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java +++ b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java @@ -33,6 +33,7 @@ import static java.util.Arrays.asList; import static java.util.Arrays.copyOfRange; import android.accessibilityservice.MagnificationConfig; +import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UiContext; @@ -41,6 +42,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.graphics.PointF; +import android.graphics.Rect; import android.graphics.Region; import android.os.Handler; import android.os.Looper; @@ -147,6 +149,20 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH private PointerCoords[] mTempPointerCoords; private PointerProperties[] mTempPointerProperties; + + @VisibleForTesting static final int OVERSCROLL_NONE = 0; + @VisibleForTesting static final int OVERSCROLL_LEFT_EDGE = 1; + @VisibleForTesting static final int OVERSCROLL_RIGHT_EDGE = 2; + @VisibleForTesting static final int OVERSCROLL_VERTICAL_EDGE = 3; + + @IntDef({ + OVERSCROLL_NONE, + OVERSCROLL_LEFT_EDGE, + OVERSCROLL_RIGHT_EDGE, + OVERSCROLL_VERTICAL_EDGE + }) + public @interface OverscrollState {} + @VisibleForTesting boolean mIsSinglePanningEnabled; public FullScreenMagnificationGestureHandler(@UiContext Context context, FullScreenMagnificationController fullScreenMagnificationController, @@ -842,8 +858,11 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH transitToPanningScalingStateAndClear(); } else if (mIsSinglePanningEnabled && isActivated() - && event.getPointerCount() == 1 - && !isOverscroll(event)) { + && event.getPointerCount() == 1) { + if (overscrollState(event, mFirstPointerDownLocation) + == OVERSCROLL_VERTICAL_EDGE) { + transitionToDelegatingStateAndClear(); + } transitToSinglePanningStateAndClear(); } else { transitionToDelegatingStateAndClear(); @@ -881,20 +900,6 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH } } - 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(); pointerDownLocation.set(event.getX(index), event.getY(index)); @@ -1172,7 +1177,9 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH } private void zoomOn(float centerX, float centerY) { - if (DEBUG_DETECTING) Slog.i(mLogTag, "zoomOn(" + centerX + ", " + centerY + ")"); + if (DEBUG_DETECTING) { + Slog.i(mLogTag, "zoomOn(" + centerX + ", " + centerY + ")"); + } final float scale = MathUtils.constrain( mFullScreenMagnificationController.getPersistedScale(mDisplayId), @@ -1189,7 +1196,9 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH } private void zoomOff() { - if (DEBUG_DETECTING) Slog.i(mLogTag, "zoomOff()"); + if (DEBUG_DETECTING) { + Slog.i(mLogTag, "zoomOff()"); + } mFullScreenMagnificationController.reset(mDisplayId, /* animate */ true); } @@ -1218,6 +1227,28 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH + '}'; } + private int overscrollState(MotionEvent event, PointF firstPointerDownLocation) { + if (!pointerValid(firstPointerDownLocation)) { + return OVERSCROLL_NONE; + } + float dX = event.getX() - firstPointerDownLocation.x; + float dY = event.getY() - firstPointerDownLocation.y; + if (mFullScreenMagnificationController.isAtLeftEdge(mDisplayId) && dX > 0) { + return OVERSCROLL_LEFT_EDGE; + } else if (mFullScreenMagnificationController.isAtRightEdge(mDisplayId) && dX < 0) { + return OVERSCROLL_RIGHT_EDGE; + } else if (mFullScreenMagnificationController.isAtTopEdge(mDisplayId) && dY > 0 + || mFullScreenMagnificationController.isAtBottomEdge(mDisplayId) && dY < 0) { + return OVERSCROLL_VERTICAL_EDGE; + } + return OVERSCROLL_NONE; + } + + private boolean pointerValid(PointF pointerDownLocation) { + return !(Float.isNaN(pointerDownLocation.x) && Float.isNaN(pointerDownLocation.y)); + } + + private static final class MotionEventInfo { private static final int MAX_POOL_SIZE = 10; @@ -1343,10 +1374,29 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH private final FullScreenMagnificationVibrationHelper mFullScreenMagnificationVibrationHelper; - SinglePanningState(Context context, FullScreenMagnificationVibrationHelper - fullScreenMagnificationVibrationHelper) { + @VisibleForTesting int mOverscrollState; + + // mPivotEdge is the point on the edge of the screen when the magnified view hits the edge + // This point sets the center of magnified view when warp/scale effect is triggered + private final PointF mPivotEdge; + + // mReachedEdgeCoord is the user's pointer location on the screen when the magnified view + // has hit the edge + private final PointF mReachedEdgeCoord; + // mEdgeCooldown value will be set to true when user hits the edge and will be set to false + // once the user moves x distance away from the edge. This is so that vibrating haptic + // doesn't get triggered by slight movements + private boolean mEdgeCooldown; + + SinglePanningState( + Context context, + FullScreenMagnificationVibrationHelper fullScreenMagnificationVibrationHelper) { mScrollGestureDetector = new GestureDetector(context, this, Handler.getMain()); mFullScreenMagnificationVibrationHelper = fullScreenMagnificationVibrationHelper; + mOverscrollState = OVERSCROLL_NONE; + mPivotEdge = new PointF(Float.NaN, Float.NaN); + mReachedEdgeCoord = new PointF(Float.NaN, Float.NaN); + mEdgeCooldown = false; } @Override @@ -1355,6 +1405,16 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH switch (action) { case ACTION_UP: case ACTION_CANCEL: + if (mOverscrollState == OVERSCROLL_LEFT_EDGE + || mOverscrollState == OVERSCROLL_RIGHT_EDGE) { + mFullScreenMagnificationController.setScaleAndCenter( + mDisplayId, + mFullScreenMagnificationController.getPersistedScale(mDisplayId), + mPivotEdge.x, + mPivotEdge.y, + true, + AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID); + } clear(); transitionTo(mDetectingState); break; @@ -1382,11 +1442,22 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH + " isAtEdge: " + mFullScreenMagnificationController.isAtEdge(mDisplayId)); } - // TODO: b/280812104 Dispatch events before Delegation if (mFullScreenMagnificationController.isAtEdge(mDisplayId)) { + playEdgeVibration(second); + setPivotEdge(); + } + if (mOverscrollState == OVERSCROLL_NONE) { + mOverscrollState = overscrollState(second, new PointF(first.getX(), first.getY())); + } else if (mOverscrollState == OVERSCROLL_VERTICAL_EDGE) { clear(); transitionTo(mDelegatingState); - vibrateIfNeeded(); + } else { + boolean reset = warpEffectReset(second); + if (reset) { + mFullScreenMagnificationController.reset(mDisplayId, /* animate */ true); + clear(); + transitionTo(mDetectingState); + } } return /* event consumed: */ true; } @@ -1405,7 +1476,116 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH return "SinglePanningState{" + "isEdgeOfView=" + mFullScreenMagnificationController.isAtEdge(mDisplayId) + + "overscrollStatus=" + + mOverscrollState + "}"; } + + private void playEdgeVibration(MotionEvent event) { + if (mOverscrollState == OVERSCROLL_NONE) { + vibrateIfNeeded(event); + } + } + + private void setPivotEdge() { + if (!pointerValid(mPivotEdge)) { + Rect bounds = new Rect(); + mFullScreenMagnificationController.getMagnificationBounds(mDisplayId, bounds); + if (mOverscrollState == OVERSCROLL_LEFT_EDGE) { + mPivotEdge.set( + bounds.left, + mFullScreenMagnificationController.getCenterY(mDisplayId)); + } else if (mOverscrollState == OVERSCROLL_RIGHT_EDGE) { + mPivotEdge.set( + bounds.right, + mFullScreenMagnificationController.getCenterY(mDisplayId)); + } + } + } + + private boolean warpEffectReset(MotionEvent second) { + float scale = calculateOverscrollScale(second); + if (scale < 0) return false; + mFullScreenMagnificationController.setScaleAndCenter( + /* displayId= */ mDisplayId, + /* scale= */ scale, + /* centerX= */ mPivotEdge.x, + /* centerY= */ mPivotEdge.y, + /* animate= */ true, + /* id= */ AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID); + if (scale == 1.0f) { + return true; + } + return false; + } + + private float calculateOverscrollScale(MotionEvent second) { + // if at left and overshootDistX is negative or if at right and overshootDistX is + // positive then user is not in overscroll state anymore overscroll state. Reset + // overscroll values by clearing + float overshootDistX = second.getX() - mReachedEdgeCoord.x; + if ((mOverscrollState == OVERSCROLL_LEFT_EDGE && overshootDistX < 0) + || (mOverscrollState == OVERSCROLL_RIGHT_EDGE && overshootDistX > 0)) { + clear(); + return -1.0f; + } + float overshootDistY = second.getY() - mReachedEdgeCoord.y; + float overshootDist = (float) (Math.hypot(abs(overshootDistX), abs(overshootDistY))); + Rect bounds = new Rect(); + mFullScreenMagnificationController.getMagnificationBounds(mDisplayId, bounds); + float overShootFraction = overshootDist / (float) bounds.width(); + float minDist = 0.05f * bounds.width(); + if (mEdgeCooldown && (overshootDist > minDist)) { + mEdgeCooldown = false; + } + float scale = (1 - overShootFraction) * getSensitivityScale(); + scale = + MathUtils.constrain( + /* amount= */ scale, + /* low= */ 1.0f, + /* high= */ mFullScreenMagnificationController.getPersistedScale( + mDisplayId)); + return scale; + } + + private float getSensitivityScale() { + float magnificationScale = + mFullScreenMagnificationController.getPersistedScale(mDisplayId); + float sensitivityFactor = 0.0f; + if (magnificationScale < 1.7f) { + sensitivityFactor = 1.0f; + } else if (magnificationScale < 2.0f) { + sensitivityFactor = 1.0f; + } else if (magnificationScale < 2.2f) { + sensitivityFactor = 0.95f; + } else if (magnificationScale < 2.5f) { + sensitivityFactor = 1.1f; + } else if (magnificationScale < 2.7f) { + sensitivityFactor = 1.3f; + } else if (magnificationScale < 3.0f) { + sensitivityFactor = 1.0f; + } else { + sensitivityFactor = 1.0f; + } + return magnificationScale * sensitivityFactor; + } + + private void vibrateIfNeeded(MotionEvent event) { + if ((mFullScreenMagnificationController.isAtLeftEdge(mDisplayId) + || mFullScreenMagnificationController.isAtRightEdge(mDisplayId)) + && !mEdgeCooldown) { + mFullScreenMagnificationVibrationHelper.vibrateIfSettingEnabled(); + mReachedEdgeCoord.set(event.getX(), event.getY()); + mEdgeCooldown = true; + } + } + + @Override + public void clear() { + mOverscrollState = OVERSCROLL_NONE; + mPivotEdge.set(Float.NaN, Float.NaN); + mReachedEdgeCoord.set(Float.NaN, Float.NaN); + mEdgeCooldown = false; + } } } 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 4d3bd92551d74..19fb2c9b05dc4 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 @@ -589,51 +589,91 @@ public class FullScreenMagnificationGestureHandlerTest { } @Test - public void testScroll_zoomedStateAndAtEdge_delegatingState() { - goFromStateIdleTo(STATE_ACTIVATED); + @FlakyTest + public void testScroll_singleHorizontalPanningAndAtEdge_leftEdgeOverscroll() { + goFromStateIdleTo(STATE_SINGLE_PANNING); + float centerY = + (INITIAL_MAGNIFICATION_BOUNDS.top + INITIAL_MAGNIFICATION_BOUNDS.bottom) / 2.0f; mFullScreenMagnificationController.setCenter( - DISPLAY_0, - INITIAL_MAGNIFICATION_BOUNDS.left, - INITIAL_MAGNIFICATION_BOUNDS.top / 2, - false, - 1); + DISPLAY_0, INITIAL_MAGNIFICATION_BOUNDS.left, centerY, 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(); + PointF edgeCoords = new PointF(initCoords.x, initCoords.y); + edgeCoords.offset(swipeMinDistance, 0); - swipeAndHold(initCoords, endCoords); + swipeAndHold(initCoords, edgeCoords); - assertTrue(mMgh.mCurrentState == mMgh.mDelegatingState); + assertTrue(mMgh.mCurrentState == mMgh.mSinglePanningState); + assertTrue(mMgh.mSinglePanningState.mOverscrollState == mMgh.OVERSCROLL_LEFT_EDGE); assertTrue(isZoomed()); } @Test @FlakyTest - public void testScroll_singlePanningAndAtEdge_delegatingState() { + public void testScroll_singleHorizontalPanningAndAtEdge_rightEdgeOverscroll() { goFromStateIdleTo(STATE_SINGLE_PANNING); + float centerY = + (INITIAL_MAGNIFICATION_BOUNDS.top + INITIAL_MAGNIFICATION_BOUNDS.bottom) / 2.0f; mFullScreenMagnificationController.setCenter( - DISPLAY_0, - INITIAL_MAGNIFICATION_BOUNDS.left, - INITIAL_MAGNIFICATION_BOUNDS.top / 2, - false, - 1); + DISPLAY_0, INITIAL_MAGNIFICATION_BOUNDS.right, centerY, 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(); + PointF edgeCoords = new PointF(initCoords.x, initCoords.y); + edgeCoords.offset(-swipeMinDistance, 0); - swipeAndHold(initCoords, endCoords); + swipeAndHold(initCoords, edgeCoords); - assertTrue(mMgh.mCurrentState == mMgh.mDelegatingState); + assertTrue(mMgh.mCurrentState == mMgh.mSinglePanningState); + assertTrue(mMgh.mSinglePanningState.mOverscrollState == mMgh.OVERSCROLL_RIGHT_EDGE); + assertTrue(isZoomed()); + } + + @Test + @FlakyTest + public void testScroll_singleVerticalPanningAndAtEdge_verticalOverscroll() { + goFromStateIdleTo(STATE_SINGLE_PANNING); + float centerX = + (INITIAL_MAGNIFICATION_BOUNDS.right + INITIAL_MAGNIFICATION_BOUNDS.left) / 2.0f; + mFullScreenMagnificationController.setCenter( + DISPLAY_0, centerX, INITIAL_MAGNIFICATION_BOUNDS.top, false, 1); + final float swipeMinDistance = ViewConfiguration.get(mContext).getScaledTouchSlop() + 1; + PointF initCoords = + new PointF( + mFullScreenMagnificationController.getCenterX(DISPLAY_0), + mFullScreenMagnificationController.getCenterY(DISPLAY_0)); + PointF edgeCoords = new PointF(initCoords.x, initCoords.y); + edgeCoords.offset(0, swipeMinDistance); + + swipeAndHold(initCoords, edgeCoords); + + assertTrue(mMgh.mSinglePanningState.mOverscrollState == mMgh.OVERSCROLL_VERTICAL_EDGE); + assertTrue(isZoomed()); + } + + @Test + public void testScroll_singlePanningAndAtEdge_noOverscroll() { + goFromStateIdleTo(STATE_SINGLE_PANNING); + float centerY = + (INITIAL_MAGNIFICATION_BOUNDS.top + INITIAL_MAGNIFICATION_BOUNDS.bottom) / 2.0f; + mFullScreenMagnificationController.setCenter( + DISPLAY_0, INITIAL_MAGNIFICATION_BOUNDS.left, centerY, false, 1); + final float swipeMinDistance = ViewConfiguration.get(mContext).getScaledTouchSlop() + 1; + PointF initCoords = + new PointF( + mFullScreenMagnificationController.getCenterX(DISPLAY_0), + mFullScreenMagnificationController.getCenterY(DISPLAY_0)); + PointF edgeCoords = new PointF(initCoords.x, initCoords.y); + edgeCoords.offset(-swipeMinDistance, 0); + + swipeAndHold(initCoords, edgeCoords); + + assertTrue(mMgh.mSinglePanningState.mOverscrollState == mMgh.OVERSCROLL_NONE); assertTrue(isZoomed()); } @@ -1105,6 +1145,11 @@ public class FullScreenMagnificationGestureHandlerTest { send(upEvent()); } + private void swipe(PointF start, PointF end) { + swipeAndHold(start, end); + send(upEvent(end.x, end.y)); + } + private void swipeAndHold() { send(downEvent()); send(moveEvent(DEFAULT_X * 2, DEFAULT_Y * 2));