Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 013f1644 authored by Candice's avatar Candice
Browse files

fix(fullscreen magnification): Add animation to the orange border

Fade in/out when create/remove the orange fullscreen magnification
border: apply animation on the border view's alpha.

Bug: 291891390
Test: adb shell device_config put accessibility com.android.window.flags.always_draw_magnification_fullscreen_border true
      atest SystemUITests:FullscreenMagnificationControllerTest
Flag: ACONFIG always_draw_magnification_fullscreen_border DEVELOPMENT
Change-Id: I536b0f86951cff150fd8b2db5441a3c93100a28c
parent f1f87d88
Loading
Loading
Loading
Loading
+105 −16
Original line number Diff line number Diff line
@@ -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() {
        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());

        SurfaceControl surfaceControl = mSurfaceControlViewHost
                .getSurfacePackage().getSurfaceControl();
            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,
+10 −5
Original line number Diff line number Diff line
@@ -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);

+123 −22
Original line number Diff line number Diff line
@@ -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(
                () -> {
    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);
                        .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);
                        .onFullscreenMagnificationActivationChanged(false));

        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();
            }
        );
        });

        assertThat(mSurfaceControlViewHost.getView()).isNull();
        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;
    }
}
+1 −1
Original line number Diff line number Diff line
@@ -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 =
+2 −1
Original line number Diff line number Diff line
@@ -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(