From 9ec06e12290bf8bd145c44b45c3019e801638629 Mon Sep 17 00:00:00 2001 From: ryanlwlin Date: Wed, 29 Jul 2020 20:49:13 +0800 Subject: [PATCH] Add callback for transition animation To support transition animation while switching magnification mode, We add the callback to know when the animation is finished. Bug: 161669184 Test: FullScreenMagnificationControllerTest Change-Id: I002e0a818a38d5928d98741284c68f6236745b6f --- .../FullScreenMagnificationController.java | 123 +++++++++++++++--- ...FullScreenMagnificationControllerTest.java | 100 +++++++++++++- 2 files changed, 205 insertions(+), 18 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 44c4bf4836a0d..b6f2a47dd5e29 100644 --- a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java +++ b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java @@ -16,8 +16,10 @@ package com.android.server.accessibility.magnification; +import android.animation.Animator; import android.animation.ValueAnimator; import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -61,6 +63,8 @@ public class FullScreenMagnificationController { private static final boolean DEBUG = false; private static final String LOG_TAG = "FullScreenMagnificationController"; + private static final Runnable STUB_RUNNABLE = () -> { + }; public static final float MIN_SCALE = 1.0f; public static final float MAX_SCALE = 8.0f; @@ -292,7 +296,7 @@ public class FullScreenMagnificationController { // Adjust the current spec's offsets if necessary. if (updateCurrentSpecWithOffsetsLocked( mCurrentMagnificationSpec.offsetX, mCurrentMagnificationSpec.offsetY)) { - sendSpecToAnimation(mCurrentMagnificationSpec, false); + sendSpecToAnimation(mCurrentMagnificationSpec, null); } onMagnificationChangedLocked(); } @@ -300,17 +304,18 @@ public class FullScreenMagnificationController { } } - void sendSpecToAnimation(MagnificationSpec spec, boolean animate) { + void sendSpecToAnimation(MagnificationSpec spec, Runnable endCallback) { if (DEBUG) { Slog.i(LOG_TAG, - "sendSpecToAnimation(spec = " + spec + ", animate = " + animate + ")"); + "sendSpecToAnimation(spec = " + spec + ", endCallback = " + endCallback + + ")"); } if (Thread.currentThread().getId() == mMainThreadId) { - mSpecAnimationBridge.updateSentSpecMainThread(spec, animate); + mSpecAnimationBridge.updateSentSpecMainThread(spec, endCallback); } else { final Message m = PooledLambda.obtainMessage( SpecAnimationBridge::updateSentSpecMainThread, - mSpecAnimationBridge, spec, animate); + mSpecAnimationBridge, spec, endCallback); mControllerCtx.getHandler().sendMessage(m); } } @@ -410,6 +415,11 @@ public class FullScreenMagnificationController { @GuardedBy("mLock") boolean reset(boolean animate) { + return reset(transformToStubRunnable(animate)); + } + + @GuardedBy("mLock") + boolean reset(Runnable endCallback) { if (!mRegistered) { return false; } @@ -420,7 +430,7 @@ public class FullScreenMagnificationController { onMagnificationChangedLocked(); } mIdOfLastServiceToMagnify = INVALID_ID; - sendSpecToAnimation(spec, animate); + sendSpecToAnimation(spec, endCallback); return changed; } @@ -448,24 +458,24 @@ public class FullScreenMagnificationController { final float centerX = normPivotX + offsetX; final float centerY = normPivotY + offsetY; mIdOfLastServiceToMagnify = id; - return setScaleAndCenter(scale, centerX, centerY, animate, id); + return setScaleAndCenter(scale, centerX, centerY, transformToStubRunnable(animate), id); } @GuardedBy("mLock") boolean setScaleAndCenter(float scale, float centerX, float centerY, - boolean animate, int id) { + Runnable endCallback, int id) { if (!mRegistered) { return false; } if (DEBUG) { Slog.i(LOG_TAG, "setScaleAndCenterLocked(scale = " + scale + ", centerX = " + centerX - + ", centerY = " + centerY + ", animate = " + animate + + ", centerY = " + centerY + ", endCallback = " + endCallback + ", id = " + id + ")"); } final boolean changed = updateMagnificationSpecLocked(scale, centerX, centerY); - sendSpecToAnimation(mCurrentMagnificationSpec, animate); + sendSpecToAnimation(mCurrentMagnificationSpec, endCallback); if (isMagnifying() && (id != INVALID_ID)) { mIdOfLastServiceToMagnify = id; } @@ -531,7 +541,7 @@ public class FullScreenMagnificationController { if (id != INVALID_ID) { mIdOfLastServiceToMagnify = id; } - sendSpecToAnimation(mCurrentMagnificationSpec, false); + sendSpecToAnimation(mCurrentMagnificationSpec, null); } boolean updateCurrentSpecWithOffsetsLocked(float nonNormOffsetX, float nonNormOffsetY) { @@ -865,12 +875,26 @@ public class FullScreenMagnificationController { * the spec did not change */ public boolean reset(int displayId, boolean animate) { + return reset(displayId, animate ? STUB_RUNNABLE : null); + } + + /** + * Resets the magnification scale and center, optionally animating the + * transition. + * + * @param displayId The logical display id. + * @param endCallback Called when the animation is ended or the spec did not change. + * {@code null} to transition immediately + * @return {@code true} if the magnification spec changed, {@code false} if + * the spec did not change + */ + public boolean reset(int displayId, Runnable endCallback) { synchronized (mLock) { final DisplayMagnification display = mDisplays.get(displayId); if (display == null) { return false; } - return display.reset(animate); + return display.reset(endCallback); } } @@ -921,7 +945,8 @@ public class FullScreenMagnificationController { if (display == null) { return false; } - return display.setScaleAndCenter(Float.NaN, centerX, centerY, animate, id); + return display.setScaleAndCenter(Float.NaN, centerX, centerY, + animate ? STUB_RUNNABLE : null, id); } } @@ -944,12 +969,35 @@ public class FullScreenMagnificationController { */ public boolean setScaleAndCenter(int displayId, float scale, float centerX, float centerY, boolean animate, int id) { + return setScaleAndCenter(displayId, scale, centerX, centerY, + transformToStubRunnable(animate), id); + } + + /** + * Sets the scale and center of the magnified region, optionally + * animating the transition. If animation is disabled, the transition + * is immediate. + * + * @param displayId The logical display id. + * @param scale the target scale, or {@link Float#NaN} to leave unchanged + * @param centerX the screen-relative X coordinate around which to + * center and scale, or {@link Float#NaN} to leave unchanged + * @param centerY the screen-relative Y coordinate around which to + * center and scale, or {@link Float#NaN} to leave unchanged + * @param endCallback called when the transition is finished successfully or the spec did not + * change. {@code null} to transition immediately. + * @param id the ID of the service requesting the change + * @return {@code true} if the magnification spec changed, {@code false} if + * the spec did not change + */ + public boolean setScaleAndCenter(int displayId, float scale, float centerX, float centerY, + Runnable endCallback, int id) { synchronized (mLock) { final DisplayMagnification display = mDisplays.get(displayId); if (display == null) { return false; } - return display.setScaleAndCenter(scale, centerX, centerY, animate, id); + return display.setScaleAndCenter(scale, centerX, centerY, endCallback, id); } } @@ -1160,7 +1208,8 @@ public class FullScreenMagnificationController { * Class responsible for animating spec on the main thread and sending spec * updates to the window manager. */ - private static class SpecAnimationBridge implements ValueAnimator.AnimatorUpdateListener { + private static class SpecAnimationBridge implements ValueAnimator.AnimatorUpdateListener, + Animator.AnimatorListener { private final ControllerContext mControllerCtx; /** @@ -1180,6 +1229,8 @@ public class FullScreenMagnificationController { */ private final ValueAnimator mValueAnimator; + // Called when the callee wants animating and the sent spec matches the target spec. + private Runnable mEndCallback; private final Object mLock; private final int mDisplayId; @@ -1197,6 +1248,7 @@ public class FullScreenMagnificationController { mValueAnimator.setInterpolator(new DecelerateInterpolator(2.5f)); mValueAnimator.setFloatValues(0.0f, 1.0f); mValueAnimator.addUpdateListener(this); + mValueAnimator.addListener(this); } /** @@ -1216,24 +1268,36 @@ public class FullScreenMagnificationController { } } - public void updateSentSpecMainThread(MagnificationSpec spec, boolean animate) { + void updateSentSpecMainThread(MagnificationSpec spec, Runnable endCallback) { if (mValueAnimator.isRunning()) { + // Avoid AnimationEnd Callback. + mEndCallback = null; mValueAnimator.cancel(); } + mEndCallback = endCallback; // If the current and sent specs don't match, update the sent spec. synchronized (mLock) { final boolean changed = !mSentMagnificationSpec.equals(spec); if (changed) { - if (animate) { + if (mEndCallback != null) { animateMagnificationSpecLocked(spec); } else { setMagnificationSpecLocked(spec); } + } else { + sendEndCallbackMainThread(); } } } + private void sendEndCallbackMainThread() { + if (mEndCallback != null) { + mEndCallback.run(); + mEndCallback = null; + } + } + @GuardedBy("mLock") private void setMagnificationSpecLocked(MagnificationSpec spec) { if (mEnabled) { @@ -1270,6 +1334,26 @@ public class FullScreenMagnificationController { } } } + + @Override + public void onAnimationStart(Animator animation) { + + } + + @Override + public void onAnimationEnd(Animator animation) { + sendEndCallbackMainThread(); + } + + @Override + public void onAnimationCancel(Animator animation) { + + } + + @Override + public void onAnimationRepeat(Animator animation) { + + } } private static class ScreenStateObserver extends BroadcastReceiver { @@ -1395,4 +1479,9 @@ public class FullScreenMagnificationController { return mAnimationDuration; } } + + @Nullable + private static Runnable transformToStubRunnable(boolean animate) { + return animate ? STUB_RUNNABLE : null; + } } diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java index a9f2e4a50dedc..57bfbf33d6804 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java @@ -27,6 +27,7 @@ import static org.mockito.Matchers.anyObject; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -95,12 +96,15 @@ public class FullScreenMagnificationControllerTest { ValueAnimator mMockValueAnimator; ValueAnimator.AnimatorUpdateListener mTargetAnimationListener; + ValueAnimator.AnimatorListener mStateListener; FullScreenMagnificationController mFullScreenMagnificationController; + Runnable mEndCallback; @Before public void setUp() { Looper looper = InstrumentationRegistry.getContext().getMainLooper(); + mEndCallback = Mockito.mock(Runnable.class); // Pretending ID of the Thread associated with looper as main thread ID in controller when(mMockContext.getMainLooper()).thenReturn(looper); when(mMockControllerCtx.getContext()).thenReturn(mMockContext); @@ -319,6 +323,7 @@ public class FullScreenMagnificationControllerTest { for (int i = 0; i < DISPLAY_COUNT; i++) { setScaleAndCenter_animated_stateChangesAndAnimationHappens(i); resetMockWindowManager(); + Mockito.reset(mEndCallback); } } @@ -331,7 +336,7 @@ public class FullScreenMagnificationControllerTest { MagnificationSpec endSpec = getMagnificationSpec(scale, offsets); assertTrue(mFullScreenMagnificationController.setScaleAndCenter(displayId, scale, - newCenter.x, newCenter.y, true, SERVICE_ID_1)); + newCenter.x, newCenter.y, mEndCallback, SERVICE_ID_1)); mMessageCapturingHandler.sendAllMessages(); assertEquals(newCenter.x, mFullScreenMagnificationController.getCenterX(displayId), 0.5); @@ -358,7 +363,33 @@ public class FullScreenMagnificationControllerTest { Mockito.reset(mMockWindowManager); when(mMockValueAnimator.getAnimatedFraction()).thenReturn(1.0f); mTargetAnimationListener.onAnimationUpdate(mMockValueAnimator); + mStateListener.onAnimationEnd(mMockValueAnimator); verify(mMockWindowManager).setMagnificationSpec(eq(displayId), argThat(closeTo(endSpec))); + verify(mEndCallback).run(); + } + + @Test + public void testSetScaleAndCenterWithAnimation_sameSpec_noAnimationButInvokeEndCallback() { + for (int i = 0; i < DISPLAY_COUNT; i++) { + setScaleAndCenter_sameSpec_noAnimationButInvokeEndCallback(i); + Mockito.reset(mEndCallback); + } + } + + private void setScaleAndCenter_sameSpec_noAnimationButInvokeEndCallback(int displayId) { + register(displayId); + final PointF center = INITIAL_BOUNDS_LOWER_RIGHT_2X_CENTER; + final float targetScale = 2.0f; + assertTrue(mFullScreenMagnificationController.setScaleAndCenter(displayId, + targetScale, center.x, center.y, false, SERVICE_ID_1)); + mMessageCapturingHandler.sendAllMessages(); + + assertFalse(mFullScreenMagnificationController.setScaleAndCenter(displayId, + targetScale, center.x, center.y, mEndCallback, SERVICE_ID_1)); + mMessageCapturingHandler.sendAllMessages(); + + verify(mMockValueAnimator, never()).start(); + verify(mEndCallback).run(); } @Test @@ -638,6 +669,69 @@ public class FullScreenMagnificationControllerTest { assertFalse(mFullScreenMagnificationController.resetIfNeeded(displayId, false)); } + @Test + public void testReset_notMagnifying_noStateChangeButInvokeCallback() { + for (int i = 0; i < DISPLAY_COUNT; i++) { + reset_notMagnifying_noStateChangeButInvokeCallback(i); + Mockito.reset(mEndCallback); + } + } + + private void reset_notMagnifying_noStateChangeButInvokeCallback(int displayId) { + register(displayId); + + assertFalse(mFullScreenMagnificationController.reset(displayId, mEndCallback)); + mMessageCapturingHandler.sendAllMessages(); + + verify(mMockAms, never()).notifyMagnificationChanged(eq(displayId), + any(Region.class), anyFloat(), anyFloat(), anyFloat()); + verify(mEndCallback).run(); + } + + @Test + public void testReset_Magnifying_resetsMagnificationAndInvokeLastEndCallback() { + for (int i = 0; i < DISPLAY_COUNT; i++) { + reset_Magnifying_resetsMagnificationAndInvokeLastEndCallback(i); + } + } + + private void reset_Magnifying_resetsMagnificationAndInvokeLastEndCallback(int displayId) { + register(displayId); + float scale = 2.5f; + PointF firstCenter = INITIAL_BOUNDS_LOWER_RIGHT_2X_CENTER; + assertTrue(mFullScreenMagnificationController.setScaleAndCenter(displayId, + scale, firstCenter.x, firstCenter.y, mEndCallback, SERVICE_ID_1)); + mMessageCapturingHandler.sendAllMessages(); + Mockito.reset(mMockValueAnimator); + // Stubs the logic after the animation is started. + doAnswer(invocation -> { + mStateListener.onAnimationEnd(mMockValueAnimator); + return null; + }).when(mMockValueAnimator).cancel(); + when(mMockValueAnimator.isRunning()).thenReturn(true); + // Intermediate point + float fraction = 0.33f; + when(mMockValueAnimator.getAnimatedFraction()).thenReturn(fraction); + mTargetAnimationListener.onAnimationUpdate(mMockValueAnimator); + Runnable lastEndCallback = Mockito.mock(Runnable.class); + + assertTrue(mFullScreenMagnificationController.reset(displayId, lastEndCallback)); + mMessageCapturingHandler.sendAllMessages(); + + // Verify expected actions. + verify(mEndCallback, never()).run(); + verify(mMockValueAnimator).start(); + verify(mMockValueAnimator).cancel(); + + // Fast-forward the animation to the end. + when(mMockValueAnimator.getAnimatedFraction()).thenReturn(1.0f); + mTargetAnimationListener.onAnimationUpdate(mMockValueAnimator); + mStateListener.onAnimationEnd(mMockValueAnimator); + + assertFalse(mFullScreenMagnificationController.isMagnifying(DISPLAY_0)); + verify(lastEndCallback).run(); + } + @Test public void testTurnScreenOff_resetsMagnification() { register(DISPLAY_0); @@ -1043,6 +1137,10 @@ public class FullScreenMagnificationControllerTest { ArgumentCaptor.forClass(ValueAnimator.AnimatorUpdateListener.class); verify(mMockValueAnimator).addUpdateListener(listenerArgumentCaptor.capture()); mTargetAnimationListener = listenerArgumentCaptor.getValue(); + ArgumentCaptor animatorListenerArgumentCaptor = + ArgumentCaptor.forClass(ValueAnimator.AnimatorListener.class); + verify(mMockValueAnimator).addListener(animatorListenerArgumentCaptor.capture()); + mStateListener = animatorListenerArgumentCaptor.getValue(); Mockito.reset(mMockValueAnimator); // Ignore other initialization }