From 205b1478cc602cddb148eaceab6d54428f08a8c3 Mon Sep 17 00:00:00 2001 From: Tyler Freeman Date: Wed, 11 May 2022 18:50:23 +0000 Subject: [PATCH] feat(magnification): move mag button when window reaches the screen edge Wait to move it until drag is done, if they are dragging by the handle. But if it is moving via two-finger drag or triple-tap, update the drag handle immediately in real-time. Add tests to verify applyTapExcludeRegion() respects the drag handle position. This required fixing a bug in earlier code where it was adding extra padding around the drag handle. Now we only exclude the tap area of the handle, no padding. Bug: b/205904754 Test: unit and manual: 1. Drag the magnification window to the right edge of the screen. The button should swap to the left. 2. Tap the button. It should bounce the frame. 3. Drag back to the center. The button should return to the right. Change-Id: I04af438ad62359ac31ba12aee0a1739e25068937 --- .../res/layout/window_magnifier_view.xml | 3 +- packages/SystemUI/res/values/dimens.xml | 3 + .../accessibility/WindowMagnification.java | 12 ++- .../WindowMagnificationController.java | 77 ++++++++++++--- ...wMagnificationAnimationControllerTest.java | 13 ++- .../WindowMagnificationControllerTest.java | 96 ++++++++++++++++++- 6 files changed, 180 insertions(+), 24 deletions(-) diff --git a/packages/SystemUI/res/layout/window_magnifier_view.xml b/packages/SystemUI/res/layout/window_magnifier_view.xml index ae16e5c919c42..7c755e5465898 100644 --- a/packages/SystemUI/res/layout/window_magnifier_view.xml +++ b/packages/SystemUI/res/layout/window_magnifier_view.xml @@ -77,8 +77,7 @@ android:layout_height="@dimen/magnification_drag_view_size" android:layout_margin="@dimen/magnification_inner_border_margin" android:layout_gravity="right|bottom" - android:paddingEnd="@dimen/magnifier_drag_handle_padding" - android:paddingBottom="@dimen/magnifier_drag_handle_padding" + android:padding="@dimen/magnifier_drag_handle_padding" android:scaleType="center" android:importantForAccessibility="no" android:src="@drawable/ic_move_magnification"/> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 207879c127d19..fa0d6769ca48f 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1036,6 +1036,9 @@ 3dp 300dp + + 32dp 48dp diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java index 6e14ddc671a32..d7fead16e2694 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java @@ -28,6 +28,7 @@ import android.hardware.display.DisplayManager; import android.os.Handler; import android.view.Display; import android.view.SurfaceControl; +import android.view.WindowManagerGlobal; import android.view.accessibility.AccessibilityManager; import android.view.accessibility.IRemoteMagnificationAnimationCallback; import android.view.accessibility.IWindowMagnificationConnection; @@ -89,9 +90,14 @@ public class WindowMagnification extends CoreStartable implements WindowMagnifie TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY, /* options */ null); return new WindowMagnificationController( windowContext, - mHandler, new WindowMagnificationAnimationController(windowContext), - new SfVsyncFrameCallbackProvider(), null, - new SurfaceControl.Transaction(), mWindowMagnifierCallback, mSysUiState); + mHandler, + new WindowMagnificationAnimationController(windowContext), + new SfVsyncFrameCallbackProvider(), + null, + new SurfaceControl.Transaction(), + mWindowMagnifierCallback, + mSysUiState, + WindowManagerGlobal::getWindowSession); } } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java index a1428f3120c8e..1eedae6f01b0d 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java @@ -57,11 +57,11 @@ import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.View; import android.view.WindowManager; -import android.view.WindowManagerGlobal; import android.view.WindowMetrics; import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; import android.view.accessibility.IRemoteMagnificationAnimationCallback; +import android.widget.FrameLayout; import androidx.core.math.MathUtils; @@ -75,6 +75,7 @@ import java.io.PrintWriter; import java.text.NumberFormat; import java.util.Collections; import java.util.Locale; +import java.util.function.Supplier; /** * Class to handle adding and removing a window magnification. @@ -92,6 +93,7 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold private static final Range A11Y_ACTION_SCALE_RANGE = new Range<>(2.0f, 8.0f); private static final float A11Y_CHANGE_SCALE_DIFFERENCE = 1.0f; private static final float ANIMATION_BOUNCE_EFFECT_SCALE = 1.05f; + private final Context mContext; private final Resources mResources; private final Handler mHandler; @@ -163,8 +165,13 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold private SurfaceView mMirrorSurfaceView; private int mMirrorSurfaceMargin; private int mBorderDragSize; - private int mDragViewSize; private int mOuterBorderSize; + + /** + * How far from the right edge of the screen you need to drag the window before the button + * repositions to the other side. + */ + private int mButtonRepositionThresholdFromEdge; // The boundary of magnification frame. private final Rect mMagnificationFrameBoundary = new Rect(); // The top Y of the system gesture rect at the bottom. Set to -1 if it is invalid. @@ -172,6 +179,7 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold private int mMinWindowSize; private final WindowMagnificationAnimationController mAnimationController; + private final Supplier mGlobalWindowSessionSupplier; private final SfVsyncFrameCallbackProvider mSfVsyncFrameProvider; private final MagnificationGestureDetector mGestureDetector; private final int mBounceEffectDuration; @@ -182,18 +190,25 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold private SysUiState mSysUiState; // Set it to true when the view is overlapped with the gesture insets at the bottom. private boolean mOverlapWithGestureInsets; + private boolean mIsDragging; @Nullable private MirrorWindowControl mMirrorWindowControl; - WindowMagnificationController(@UiContext Context context, @NonNull Handler handler, + WindowMagnificationController( + @UiContext Context context, + @NonNull Handler handler, @NonNull WindowMagnificationAnimationController animationController, SfVsyncFrameCallbackProvider sfVsyncFrameProvider, - MirrorWindowControl mirrorWindowControl, SurfaceControl.Transaction transaction, - @NonNull WindowMagnifierCallback callback, SysUiState sysUiState) { + MirrorWindowControl mirrorWindowControl, + SurfaceControl.Transaction transaction, + @NonNull WindowMagnifierCallback callback, + SysUiState sysUiState, + @NonNull Supplier globalWindowSessionSupplier) { mContext = context; mHandler = handler; mAnimationController = animationController; + mGlobalWindowSessionSupplier = globalWindowSessionSupplier; mAnimationController.setWindowMagnificationController(this); mSfVsyncFrameProvider = sfVsyncFrameProvider; mWindowMagnifierCallback = callback; @@ -249,8 +264,8 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold }; mMirrorSurfaceViewLayoutChangeListener = - (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) - -> applyTapExcludeRegion(); + (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> + mMirrorView.post(this::applyTapExcludeRegion); mMirrorViewGeometryVsyncCallback = l -> { @@ -283,10 +298,11 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold R.dimen.magnification_mirror_surface_margin); mBorderDragSize = mResources.getDimensionPixelSize( R.dimen.magnification_border_drag_size); - mDragViewSize = mResources.getDimensionPixelSize( - R.dimen.magnification_drag_view_size); mOuterBorderSize = mResources.getDimensionPixelSize( R.dimen.magnification_outer_border_margin); + mButtonRepositionThresholdFromEdge = + mResources.getDimensionPixelSize( + R.dimen.magnification_button_reposition_threshold_from_edge); mMinWindowSize = mResources.getDimensionPixelSize( com.android.internal.R.dimen.accessibility_window_magnifier_min_size); } @@ -551,10 +567,13 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold } private void applyTapExcludeRegion() { + // Sometimes this can get posted and run after deleteWindowMagnification() is called. + if (mMirrorView == null) return; + final Region tapExcludeRegion = calculateTapExclude(); final IWindow window = IWindow.Stub.asInterface(mMirrorView.getWindowToken()); try { - IWindowSession session = WindowManagerGlobal.getWindowSession(); + IWindowSession session = mGlobalWindowSessionSupplier.get(); session.updateTapExcludeRegion(window, tapExcludeRegion); } catch (RemoteException e) { } @@ -564,9 +583,9 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold Region regionInsideDragBorder = new Region(mBorderDragSize, mBorderDragSize, mMirrorView.getWidth() - mBorderDragSize, mMirrorView.getHeight() - mBorderDragSize); - Rect dragArea = new Rect(mMirrorView.getWidth() - mDragViewSize - mBorderDragSize, - mMirrorView.getHeight() - mDragViewSize - mBorderDragSize, - mMirrorView.getWidth(), mMirrorView.getHeight()); + Rect dragArea = new Rect(); + mDragView.getHitRect(dragArea); + regionInsideDragBorder.op(dragArea, Region.Op.DIFFERENCE); return regionInsideDragBorder; } @@ -713,6 +732,12 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold mMirrorView.setTranslationX(translationX); mMirrorView.setTranslationY(translationY); mWm.updateViewLayout(mMirrorView, params); + + // If they are not dragging the handle, we can move the drag handle immediately without + // disruption. But if they are dragging it, we avoid moving until the end of the drag. + if (!mIsDragging) { + mMirrorView.post(this::maybeRepositionButton); + } } @Override @@ -1060,14 +1085,40 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold @Override public boolean onStart(float x, float y) { + mIsDragging = true; return true; } @Override public boolean onFinish(float x, float y) { + maybeRepositionButton(); + mIsDragging = false; return false; } + /** Moves the button to the opposite edge if the frame is against the edge of the screen. */ + private void maybeRepositionButton() { + if (mMirrorView == null) return; + + final float screenEdgeX = mWindowBounds.right - mButtonRepositionThresholdFromEdge; + final FrameLayout.LayoutParams layoutParams = + (FrameLayout.LayoutParams) mDragView.getLayoutParams(); + + mMirrorView.getBoundsOnScreen(mTmpRect); + + final int newGravity; + if (mTmpRect.right >= screenEdgeX) { + newGravity = Gravity.BOTTOM | Gravity.LEFT; + } else { + newGravity = Gravity.BOTTOM | Gravity.RIGHT; + } + if (newGravity != layoutParams.gravity) { + layoutParams.gravity = newGravity; + mDragView.setLayoutParams(layoutParams); + mDragView.post(this::applyTapExcludeRegion); + } + } + private void animateBounceEffect() { final ObjectAnimator scaleAnimator = ObjectAnimator.ofPropertyValuesHolder(mMirrorView, PropertyValuesHolder.ofFloat(View.SCALE_X, 1, mBounceEffectAnimationScale, 1), diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java index 21c3d6ea0660a..08503308209b4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java @@ -38,6 +38,7 @@ import android.testing.AndroidTestingRunner; import android.view.SurfaceControl; import android.view.View; import android.view.WindowManager; +import android.view.WindowManagerGlobal; import android.view.accessibility.IRemoteMagnificationAnimationCallback; import android.view.animation.AccelerateInterpolator; @@ -761,8 +762,16 @@ public class WindowMagnificationAnimationControllerTest extends SysuiTestCase { SfVsyncFrameCallbackProvider sfVsyncFrameProvider, MirrorWindowControl mirrorWindowControl, SurfaceControl.Transaction transaction, WindowMagnifierCallback callback, SysUiState sysUiState) { - super(context, handler, animationController, sfVsyncFrameProvider, mirrorWindowControl, - transaction, callback, sysUiState); + super( + context, + handler, + animationController, + sfVsyncFrameProvider, + mirrorWindowControl, + transaction, + callback, + sysUiState, + WindowManagerGlobal::getWindowSession); mSpyController = Mockito.mock(WindowMagnificationController.class); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java index b7d345965d36a..3d77d64a7988f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java @@ -28,6 +28,7 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.hasItems; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; @@ -53,18 +54,23 @@ import android.content.res.Resources; import android.graphics.Insets; import android.graphics.PointF; import android.graphics.Rect; +import android.graphics.Region; +import android.graphics.RegionIterator; import android.os.Handler; +import android.os.RemoteException; import android.os.SystemClock; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.testing.TestableResources; import android.text.TextUtils; import android.view.Display; +import android.view.IWindowSession; import android.view.Surface; import android.view.SurfaceControl; import android.view.View; import android.view.WindowInsets; import android.view.WindowManager; +import android.view.WindowManagerGlobal; import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.IRemoteMagnificationAnimationCallback; @@ -120,9 +126,10 @@ public class WindowMagnificationControllerTest extends SysuiTestCase { private WindowMagnificationController mWindowMagnificationController; private Instrumentation mInstrumentation; private final ValueAnimator mValueAnimator = ValueAnimator.ofFloat(0, 1.0f).setDuration(0); + private IWindowSession mWindowSessionSpy; @Before - public void setUp() { + public void setUp() throws RemoteException { MockitoAnnotations.initMocks(this); mContext = Mockito.spy(getContext()); mHandler = new FakeHandler(TestableLooper.get(this).getLooper()); @@ -130,6 +137,8 @@ public class WindowMagnificationControllerTest extends SysuiTestCase { final WindowManager wm = mContext.getSystemService(WindowManager.class); mWindowManager = spy(new TestableWindowManager(wm)); + mWindowSessionSpy = spy(WindowManagerGlobal.getWindowSession()); + mContext.addMockSystemService(Context.WINDOW_SERVICE, mWindowManager); doAnswer(invocation -> { FrameCallback callback = invocation.getArgument(0); @@ -142,9 +151,17 @@ public class WindowMagnificationControllerTest extends SysuiTestCase { mResources = getContext().getOrCreateTestableResources().getResources(); mWindowMagnificationAnimationController = new WindowMagnificationAnimationController( mContext, mValueAnimator); - mWindowMagnificationController = new WindowMagnificationController(mContext, - mHandler, mWindowMagnificationAnimationController, mSfVsyncFrameProvider, - mMirrorWindowControl, mTransaction, mWindowMagnifierCallback, mSysUiState); + mWindowMagnificationController = + new WindowMagnificationController( + mContext, + mHandler, + mWindowMagnificationAnimationController, + mSfVsyncFrameProvider, + mMirrorWindowControl, + mTransaction, + mWindowMagnifierCallback, + mSysUiState, + () -> mWindowSessionSpy); verify(mMirrorWindowControl).setWindowDelegate( any(MirrorWindowControl.MirrorWindowDelegate.class)); @@ -715,6 +732,77 @@ public class WindowMagnificationControllerTest extends SysuiTestCase { ReferenceTestUtils.waitForCondition(() -> hasMagnificationOverlapFlag()); } + @Test + public void moveWindowMagnificationToRightEdge_dragHandleMovesToLeftAndUpdatesTapExcludeRegion() + throws RemoteException { + final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds(); + setSystemGestureInsets(); + mInstrumentation.runOnMainSync( + () -> { + mWindowMagnificationController.enableWindowMagnificationInternal( + Float.NaN, Float.NaN, Float.NaN); + }); + + mInstrumentation.runOnMainSync( + () -> { + mWindowMagnificationController.moveWindowMagnifier(bounds.width(), 0); + }); + + // Wait for Region updated. + waitForIdleSync(); + + final ArgumentCaptor tapExcludeRegionCapturer = + ArgumentCaptor.forClass(Region.class); + verify(mWindowSessionSpy, times(2)) + .updateTapExcludeRegion(any(), tapExcludeRegionCapturer.capture()); + Region tapExcludeRegion = tapExcludeRegionCapturer.getValue(); + RegionIterator iterator = new RegionIterator(tapExcludeRegion); + + final Rect topRect = new Rect(); + final Rect bottomRect = new Rect(); + assertTrue(iterator.next(topRect)); + assertTrue(iterator.next(bottomRect)); + assertFalse(iterator.next(new Rect())); + + assertEquals(topRect.right, bottomRect.right); + assertNotEquals(topRect.left, bottomRect.left); + } + + @Test + public void moveWindowMagnificationToLeftEdge_dragHandleMovesToRightAndUpdatesTapExcludeRegion() + throws RemoteException { + final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds(); + setSystemGestureInsets(); + mInstrumentation.runOnMainSync( + () -> { + mWindowMagnificationController.enableWindowMagnificationInternal( + Float.NaN, Float.NaN, Float.NaN); + }); + + mInstrumentation.runOnMainSync( + () -> { + mWindowMagnificationController.moveWindowMagnifier(-bounds.width(), 0); + }); + + // Wait for Region updated. + waitForIdleSync(); + + final ArgumentCaptor tapExcludeRegionCapturer = + ArgumentCaptor.forClass(Region.class); + verify(mWindowSessionSpy).updateTapExcludeRegion(any(), tapExcludeRegionCapturer.capture()); + Region tapExcludeRegion = tapExcludeRegionCapturer.getValue(); + RegionIterator iterator = new RegionIterator(tapExcludeRegion); + + final Rect topRect = new Rect(); + final Rect bottomRect = new Rect(); + assertTrue(iterator.next(topRect)); + assertTrue(iterator.next(bottomRect)); + assertFalse(iterator.next(new Rect())); + + assertEquals(topRect.left, bottomRect.left); + assertNotEquals(topRect.right, bottomRect.right); + } + @Test public void setMinimumWindowSize_enabled_expectedWindowSize() { final int minimumWindowSize = mResources.getDimensionPixelSize(