diff options
5 files changed, 241 insertions, 45 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/FullscreenMagnificationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/FullscreenMagnificationController.java index af8149f59a16..e37dade22ecf 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/FullscreenMagnificationController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/FullscreenMagnificationController.java @@ -18,6 +18,10 @@ package com.android.systemui.accessibility; import static android.view.WindowManager.LayoutParams; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; import android.annotation.UiContext; import android.content.Context; import android.graphics.PixelFormat; @@ -30,11 +34,17 @@ import android.view.SurfaceControlViewHost; import android.view.View; import android.view.WindowManager; import android.view.accessibility.AccessibilityManager; +import android.view.animation.AccelerateDecelerateInterpolator; +import android.view.animation.Interpolator; +import androidx.annotation.NonNull; import androidx.annotation.UiThread; +import com.android.internal.annotations.VisibleForTesting; +import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.res.R; +import java.util.concurrent.Executor; import java.util.function.Supplier; class FullscreenMagnificationController { @@ -43,32 +53,80 @@ class FullscreenMagnificationController { private final AccessibilityManager mAccessibilityManager; private final WindowManager mWindowManager; private Supplier<SurfaceControlViewHost> mScvhSupplier; - private SurfaceControlViewHost mSurfaceControlViewHost; + private SurfaceControlViewHost mSurfaceControlViewHost = null; + private SurfaceControl mBorderSurfaceControl = null; private Rect mWindowBounds; private SurfaceControl.Transaction mTransaction; private View mFullscreenBorder = null; private int mBorderOffset; private final int mDisplayId; private static final Region sEmptyRegion = new Region(); + private ValueAnimator mShowHideBorderAnimator; + private Executor mExecutor; FullscreenMagnificationController( @UiContext Context context, + Executor executor, AccessibilityManager accessibilityManager, WindowManager windowManager, Supplier<SurfaceControlViewHost> scvhSupplier) { + this(context, executor, accessibilityManager, windowManager, scvhSupplier, + new SurfaceControl.Transaction(), createNullTargetObjectAnimator(context)); + } + + @VisibleForTesting + FullscreenMagnificationController( + @UiContext Context context, + @Main Executor executor, + AccessibilityManager accessibilityManager, + WindowManager windowManager, + Supplier<SurfaceControlViewHost> scvhSupplier, + SurfaceControl.Transaction transaction, + ValueAnimator valueAnimator) { mContext = context; + mExecutor = executor; mAccessibilityManager = accessibilityManager; mWindowManager = windowManager; mWindowBounds = mWindowManager.getCurrentWindowMetrics().getBounds(); - mTransaction = new SurfaceControl.Transaction(); + mTransaction = transaction; mScvhSupplier = scvhSupplier; mBorderOffset = mContext.getResources().getDimensionPixelSize( R.dimen.magnifier_border_width_fullscreen_with_offset) - mContext.getResources().getDimensionPixelSize( R.dimen.magnifier_border_width_fullscreen); mDisplayId = mContext.getDisplayId(); + mShowHideBorderAnimator = valueAnimator; + mShowHideBorderAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(@NonNull Animator animation, boolean isReverse) { + if (isReverse) { + // The animation was played in reverse, which means we are hiding the border. + // We would like to perform clean up after the border is fully hidden. + cleanUpBorder(); + } + } + }); } + private static ValueAnimator createNullTargetObjectAnimator(Context context) { + final ValueAnimator valueAnimator = + ObjectAnimator.ofFloat(/* target= */ null, View.ALPHA, 0f, 1f); + Interpolator interpolator = new AccelerateDecelerateInterpolator(); + final long longAnimationDuration = context.getResources().getInteger( + com.android.internal.R.integer.config_longAnimTime); + + valueAnimator.setInterpolator(interpolator); + valueAnimator.setDuration(longAnimationDuration); + return valueAnimator; + } + + /** + * In {@link com.android.server.accessibility.magnification.FullScreenMagnificationController + * .DisplayMagnification#setActivated(boolean)}, onFullScreenMagnificationActivationState is + * only called when there is an activation status change. Therefore, we could assume that we + * won't be calling "create border" when another creating border animation is running or + * "remove border" when another removing border animation is running. + */ @UiThread void onFullscreenMagnificationActivationChanged(boolean activated) { if (activated) { @@ -78,8 +136,16 @@ class FullscreenMagnificationController { } } + /** + * This method should only be called when fullscreen magnification is changed from activated + * to inactivated. + */ @UiThread private void removeFullscreenMagnificationBorder() { + mShowHideBorderAnimator.reverse(); + } + + private void cleanUpBorder() { if (mSurfaceControlViewHost != null) { mSurfaceControlViewHost.release(); mSurfaceControlViewHost = null; @@ -91,31 +157,54 @@ class FullscreenMagnificationController { } /** - * Since the device corners are not perfectly rounded, we would like to create a thick stroke, - * and set negative offset to the border view to fill up the spaces between the border and the - * device corners. + * This method should only be called when fullscreen magnification is changed from inactivated + * to activated. */ @UiThread private void createFullscreenMagnificationBorder() { - mFullscreenBorder = LayoutInflater.from(mContext) - .inflate(R.layout.fullscreen_magnification_border, null); - mSurfaceControlViewHost = mScvhSupplier.get(); - mSurfaceControlViewHost.setView(mFullscreenBorder, getBorderLayoutParams()); - - SurfaceControl surfaceControl = mSurfaceControlViewHost - .getSurfacePackage().getSurfaceControl(); + if (mSurfaceControlViewHost == null) { + // Create the view only if it does not exist yet. If we are trying to enable fullscreen + // magnification before it was fully disabled, we use the previous view instead of + // creating a new one. + mFullscreenBorder = LayoutInflater.from(mContext) + .inflate(R.layout.fullscreen_magnification_border, null); + // Set the initial border view alpha manually so we won't show the border accidentally + // after we apply show() to the SurfaceControl and before the animation starts to run. + mFullscreenBorder.setAlpha(0f); + mShowHideBorderAnimator.setTarget(mFullscreenBorder); + mSurfaceControlViewHost = mScvhSupplier.get(); + mSurfaceControlViewHost.setView(mFullscreenBorder, getBorderLayoutParams()); + mBorderSurfaceControl = mSurfaceControlViewHost.getSurfacePackage().getSurfaceControl(); + } mTransaction - .setPosition(surfaceControl, -mBorderOffset, -mBorderOffset) - .setLayer(surfaceControl, Integer.MAX_VALUE) - .show(surfaceControl) + .addTransactionCommittedListener( + mExecutor, + () -> { + if (mShowHideBorderAnimator.isRunning()) { + // Since the method is only called when there is an activation + // status change, the running animator is hiding the border. + mShowHideBorderAnimator.reverse(); + } else { + mShowHideBorderAnimator.start(); + } + }) + .setPosition(mBorderSurfaceControl, -mBorderOffset, -mBorderOffset) + .setLayer(mBorderSurfaceControl, Integer.MAX_VALUE) + .show(mBorderSurfaceControl) .apply(); - mAccessibilityManager.attachAccessibilityOverlayToDisplay(mDisplayId, surfaceControl); + mAccessibilityManager.attachAccessibilityOverlayToDisplay( + mDisplayId, mBorderSurfaceControl); applyTouchableRegion(); } + /** + * Since the device corners are not perfectly rounded, we would like to create a thick stroke, + * and set negative offset to the border view to fill up the spaces between the border and the + * device corners. + */ private LayoutParams getBorderLayoutParams() { LayoutParams params = new LayoutParams( mWindowBounds.width() + 2 * mBorderOffset, diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/Magnification.java b/packages/SystemUI/src/com/android/systemui/accessibility/Magnification.java index 70165f377a7c..177d933f2d9f 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/Magnification.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/Magnification.java @@ -54,6 +54,7 @@ import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.util.settings.SecureSettings; import java.io.PrintWriter; +import java.util.concurrent.Executor; import java.util.function.Supplier; import javax.inject.Inject; @@ -71,6 +72,7 @@ public class Magnification implements CoreStartable, CommandQueue.Callbacks { private final ModeSwitchesController mModeSwitchesController; private final Context mContext; private final Handler mHandler; + private final Executor mExecutor; private final AccessibilityManager mAccessibilityManager; private final CommandQueue mCommandQueue; private final OverviewProxyService mOverviewProxyService; @@ -139,12 +141,13 @@ public class Magnification implements CoreStartable, CommandQueue.Callbacks { DisplayIdIndexSupplier<FullscreenMagnificationController> { private final Context mContext; + private final Executor mExecutor; - FullscreenMagnificationControllerSupplier(Context context, Handler handler, - DisplayManager displayManager, SysUiState sysUiState, - SecureSettings secureSettings) { + FullscreenMagnificationControllerSupplier(Context context, DisplayManager displayManager, + Executor executor) { super(displayManager); mContext = context; + mExecutor = executor; } @Override @@ -156,6 +159,7 @@ public class Magnification implements CoreStartable, CommandQueue.Callbacks { windowContext.setTheme(com.android.systemui.res.R.style.Theme_SystemUI); return new FullscreenMagnificationController( windowContext, + mExecutor, windowContext.getSystemService(AccessibilityManager.class), windowContext.getSystemService(WindowManager.class), scvhSupplier); @@ -200,13 +204,14 @@ public class Magnification implements CoreStartable, CommandQueue.Callbacks { DisplayIdIndexSupplier<MagnificationSettingsController> mMagnificationSettingsSupplier; @Inject - public Magnification(Context context, @Main Handler mainHandler, + public Magnification(Context context, @Main Handler mainHandler, @Main Executor executor, CommandQueue commandQueue, ModeSwitchesController modeSwitchesController, SysUiState sysUiState, OverviewProxyService overviewProxyService, SecureSettings secureSettings, DisplayTracker displayTracker, DisplayManager displayManager, AccessibilityLogger a11yLogger) { mContext = context; mHandler = mainHandler; + mExecutor = executor; mAccessibilityManager = mContext.getSystemService(AccessibilityManager.class); mCommandQueue = commandQueue; mModeSwitchesController = modeSwitchesController; @@ -218,7 +223,7 @@ public class Magnification implements CoreStartable, CommandQueue.Callbacks { mHandler, mWindowMagnifierCallback, displayManager, sysUiState, secureSettings); mFullscreenMagnificationControllerSupplier = new FullscreenMagnificationControllerSupplier( - context, mHandler, displayManager, sysUiState, secureSettings); + context, displayManager, mExecutor); mMagnificationSettingsSupplier = new SettingsSupplier(context, mMagnificationSettingsControllerCallback, displayManager, secureSettings); diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/FullscreenMagnificationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/FullscreenMagnificationControllerTest.java index 12f334ba08bb..c86b3489fde7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/FullscreenMagnificationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/FullscreenMagnificationControllerTest.java @@ -16,17 +16,33 @@ package com.android.systemui.accessibility; +import static android.os.Build.HW_TIMEOUT_MULTIPLIER; + import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; +import android.view.SurfaceControl; import android.view.SurfaceControlViewHost; +import android.view.View; import android.view.WindowManager; import android.view.accessibility.AccessibilityManager; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; import android.window.InputTransferToken; +import androidx.annotation.NonNull; import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; @@ -36,29 +52,40 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import java.util.function.Supplier; @SmallTest @TestableLooper.RunWithLooper @RunWith(AndroidTestingRunner.class) public class FullscreenMagnificationControllerTest extends SysuiTestCase { - + private static final long ANIMATION_DURATION_MS = 100L; + private static final long WAIT_TIMEOUT_S = 5L * HW_TIMEOUT_MULTIPLIER; + private static final long ANIMATION_TIMEOUT_MS = + 5L * ANIMATION_DURATION_MS * HW_TIMEOUT_MULTIPLIER; private FullscreenMagnificationController mFullscreenMagnificationController; private SurfaceControlViewHost mSurfaceControlViewHost; + private ValueAnimator mShowHideBorderAnimator; + private SurfaceControl.Transaction mTransaction; @Before public void setUp() { getInstrumentation().runOnMainSync(() -> mSurfaceControlViewHost = - new SurfaceControlViewHost(mContext, mContext.getDisplay(), - new InputTransferToken(), "FullscreenMagnification")); - + spy(new SurfaceControlViewHost(mContext, mContext.getDisplay(), + new InputTransferToken(), "FullscreenMagnification"))); Supplier<SurfaceControlViewHost> scvhSupplier = () -> mSurfaceControlViewHost; + mTransaction = new SurfaceControl.Transaction(); + mShowHideBorderAnimator = spy(newNullTargetObjectAnimator()); mFullscreenMagnificationController = new FullscreenMagnificationController( mContext, + mContext.getMainExecutor(), mContext.getSystemService(AccessibilityManager.class), mContext.getSystemService(WindowManager.class), - scvhSupplier); + scvhSupplier, + mTransaction, + mShowHideBorderAnimator); } @After @@ -69,29 +96,103 @@ public class FullscreenMagnificationControllerTest extends SysuiTestCase { } @Test - public void onFullscreenMagnificationActivationChange_activated_visibleBorder() { - getInstrumentation().runOnMainSync( - () -> mFullscreenMagnificationController - .onFullscreenMagnificationActivationChanged(true) - ); - - // Wait for Rects updated. - waitForIdleSync(); + public void enableFullscreenMagnification_visibleBorder() throws InterruptedException { + CountDownLatch transactionCommittedLatch = new CountDownLatch(1); + CountDownLatch animationEndLatch = new CountDownLatch(1); + mTransaction.addTransactionCommittedListener( + Runnable::run, transactionCommittedLatch::countDown); + mShowHideBorderAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + animationEndLatch.countDown(); + } + }); + getInstrumentation().runOnMainSync(() -> + //Enable fullscreen magnification + mFullscreenMagnificationController + .onFullscreenMagnificationActivationChanged(true)); + assertTrue("Failed to wait for transaction committed", + transactionCommittedLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS)); + assertTrue("Failed to wait for animation to be finished", + animationEndLatch.await(ANIMATION_TIMEOUT_MS, TimeUnit.MILLISECONDS)); + verify(mShowHideBorderAnimator).start(); assertThat(mSurfaceControlViewHost.getView().isVisibleToUser()).isTrue(); } @Test - public void onFullscreenMagnificationActivationChange_deactivated_invisibleBorder() { - getInstrumentation().runOnMainSync( - () -> { - mFullscreenMagnificationController - .onFullscreenMagnificationActivationChanged(true); - mFullscreenMagnificationController - .onFullscreenMagnificationActivationChanged(false); + public void disableFullscreenMagnification_reverseAnimationAndReleaseScvh() + throws InterruptedException { + CountDownLatch transactionCommittedLatch = new CountDownLatch(1); + CountDownLatch enableAnimationEndLatch = new CountDownLatch(1); + CountDownLatch disableAnimationEndLatch = new CountDownLatch(1); + mTransaction.addTransactionCommittedListener( + Runnable::run, transactionCommittedLatch::countDown); + mShowHideBorderAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(@NonNull Animator animation, boolean isReverse) { + if (isReverse) { + disableAnimationEndLatch.countDown(); + } else { + enableAnimationEndLatch.countDown(); } - ); + } + }); + getInstrumentation().runOnMainSync(() -> + //Enable fullscreen magnification + mFullscreenMagnificationController + .onFullscreenMagnificationActivationChanged(true)); + assertTrue("Failed to wait for transaction committed", + transactionCommittedLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS)); + assertTrue("Failed to wait for enabling animation to be finished", + enableAnimationEndLatch.await( + ANIMATION_TIMEOUT_MS, TimeUnit.MILLISECONDS)); + verify(mShowHideBorderAnimator).start(); + + getInstrumentation().runOnMainSync(() -> + // Disable fullscreen magnification + mFullscreenMagnificationController + .onFullscreenMagnificationActivationChanged(false)); - assertThat(mSurfaceControlViewHost.getView()).isNull(); + assertTrue("Failed to wait for disabling animation to be finished", + disableAnimationEndLatch.await( + ANIMATION_TIMEOUT_MS, TimeUnit.MILLISECONDS)); + verify(mShowHideBorderAnimator).reverse(); + verify(mSurfaceControlViewHost).release(); } + @Test + public void onFullscreenMagnificationActivationChangeTrue_deactivating_reverseAnimator() + throws InterruptedException { + // Simulate the hiding border animation is running + when(mShowHideBorderAnimator.isRunning()).thenReturn(true); + CountDownLatch transactionCommittedLatch = new CountDownLatch(1); + CountDownLatch animationEndLatch = new CountDownLatch(1); + mTransaction.addTransactionCommittedListener( + Runnable::run, transactionCommittedLatch::countDown); + mShowHideBorderAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + animationEndLatch.countDown(); + } + }); + + getInstrumentation().runOnMainSync( + () -> mFullscreenMagnificationController + .onFullscreenMagnificationActivationChanged(true)); + + assertTrue("Failed to wait for transaction committed", + transactionCommittedLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS)); + assertTrue("Failed to wait for animation to be finished", + animationEndLatch.await(ANIMATION_TIMEOUT_MS, TimeUnit.MILLISECONDS)); + verify(mShowHideBorderAnimator).reverse(); + } + + private ValueAnimator newNullTargetObjectAnimator() { + final ValueAnimator animator = + ObjectAnimator.ofFloat(/* target= */ null, View.ALPHA, 0f, 1f); + Interpolator interpolator = new DecelerateInterpolator(2.5f); + animator.setInterpolator(interpolator); + animator.setDuration(ANIMATION_DURATION_MS); + return animator; + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java index 41d5d5d919e2..25e5470e2781 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java @@ -101,7 +101,7 @@ public class IMagnificationConnectionTest extends SysuiTestCase { }).when(mAccessibilityManager).setMagnificationConnection( any(IMagnificationConnection.class)); mMagnification = new Magnification(getContext(), - getContext().getMainThreadHandler(), mCommandQueue, + getContext().getMainThreadHandler(), getContext().getMainExecutor(), mCommandQueue, mModeSwitchesController, mSysUiState, mOverviewProxyService, mSecureSettings, mDisplayTracker, getContext().getSystemService(DisplayManager.class), mA11yLogger); mMagnification.mWindowMagnificationControllerSupplier = diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationTest.java index 3b5cbea079a2..6dc5b7212cd0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationTest.java @@ -121,7 +121,8 @@ public class MagnificationTest extends SysuiTestCase { mCommandQueue = new CommandQueue(getContext(), mDisplayTracker); mMagnification = new Magnification(getContext(), - getContext().getMainThreadHandler(), mCommandQueue, mModeSwitchesController, + getContext().getMainThreadHandler(), getContext().getMainExecutor(), + mCommandQueue, mModeSwitchesController, mSysUiState, mOverviewProxyService, mSecureSettings, mDisplayTracker, getContext().getSystemService(DisplayManager.class), mA11yLogger); mMagnification.mWindowMagnificationControllerSupplier = new FakeControllerSupplier( |