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

Commit 3b64f537 authored by Jordan Demeulenaere's avatar Jordan Demeulenaere
Browse files

Delay TransitionAnimator.onAnimationEnd by one frame (1/4)

This CL delays by one frame all the work done at the end of any
transition driven by TransitionAnimator. This includes all dialog and
activity launch animations.

The reason for this CL is explained in the comment added in
TransitionAnimator: any State change performed in an Android animator
callback will be picked up one frame later by Compose during
recomposition, given that both recompositions and Android animators
are scheduled on a Choreographer frame. By posting the work in the main
executor, we leave the current Choreographer frame so that all side
effects at the end of the transitions will be observable at the exact
same time.

This fix/trick was actually already used by Dialog transitions since
http://ag/20139844, so the delay was moved from DialogTransitionAnimator
to TransitionAnimator.

Bug: 330672236
Test: atest ActivityTransitionAnimatorTest
Test: atest DialogTransitionAnimatorTest
Flag: N/A
Change-Id: Idbfced2fa959ec258a8d9f0090e6c1703ea3fc6e
parent 1a89ba6a
Loading
Loading
Loading
Loading
+27 −18
Original line number Diff line number Diff line
@@ -44,6 +44,7 @@ import com.android.app.animation.Interpolators
import com.android.internal.annotations.VisibleForTesting
import com.android.internal.policy.ScreenDecorationsUtils
import com.android.systemui.Flags.activityTransitionUseLargestWindow
import java.util.concurrent.Executor
import kotlin.math.roundToInt

private const val TAG = "ActivityTransitionAnimator"
@@ -52,14 +53,19 @@ private const val TAG = "ActivityTransitionAnimator"
 * A class that allows activities to be started in a seamless way from a view that is transforming
 * nicely into the starting window.
 */
class ActivityTransitionAnimator(
class ActivityTransitionAnimator
@JvmOverloads
constructor(
    /** The executor that runs on the main thread. */
    private val mainExecutor: Executor,

    /** The animator used when animating a View into an app. */
    private val transitionAnimator: TransitionAnimator = DEFAULT_TRANSITION_ANIMATOR,
    private val transitionAnimator: TransitionAnimator = defaultTransitionAnimator(mainExecutor),

    /** The animator used when animating a Dialog into an app. */
    // TODO(b/218989950): Remove this animator and instead set the duration of the dim fade out to
    // TIMINGS.contentBeforeFadeOutDuration.
    private val dialogToAppAnimator: TransitionAnimator = DEFAULT_DIALOG_TO_APP_ANIMATOR,
    private val dialogToAppAnimator: TransitionAnimator = defaultDialogToAppAnimator(mainExecutor),

    /**
     * Whether we should disable the WindowManager timeout. This should be set to true in tests
@@ -100,10 +106,6 @@ class ActivityTransitionAnimator(
        // TODO(b/288507023): Remove this flag.
        @JvmField val DEBUG_TRANSITION_ANIMATION = Build.IS_DEBUGGABLE

        private val DEFAULT_TRANSITION_ANIMATOR = TransitionAnimator(TIMINGS, INTERPOLATORS)
        private val DEFAULT_DIALOG_TO_APP_ANIMATOR =
            TransitionAnimator(DIALOG_TIMINGS, INTERPOLATORS)

        /** Durations & interpolators for the navigation bar fading in & out. */
        private const val ANIMATION_DURATION_NAV_FADE_IN = 266L
        private const val ANIMATION_DURATION_NAV_FADE_OUT = 133L
@@ -121,6 +123,14 @@ class ActivityTransitionAnimator(
         * cancelled by WM.
         */
        private const val LONG_TRANSITION_TIMEOUT = 5_000L

        private fun defaultTransitionAnimator(mainExecutor: Executor): TransitionAnimator {
            return TransitionAnimator(mainExecutor, TIMINGS, INTERPOLATORS)
        }

        private fun defaultDialogToAppAnimator(mainExecutor: Executor): TransitionAnimator {
            return TransitionAnimator(mainExecutor, DIALOG_TIMINGS, INTERPOLATORS)
        }
    }

    /**
@@ -257,9 +267,7 @@ class ActivityTransitionAnimator(

    private fun Controller.callOnIntentStartedOnMainThread(willAnimate: Boolean) {
        if (Looper.myLooper() != Looper.getMainLooper()) {
            this.transitionContainer.context.mainExecutor.execute {
                callOnIntentStartedOnMainThread(willAnimate)
            }
            mainExecutor.execute { callOnIntentStartedOnMainThread(willAnimate) }
        } else {
            if (DEBUG_TRANSITION_ANIMATION) {
                Log.d(
@@ -479,12 +487,10 @@ class ActivityTransitionAnimator(
        controller: Controller,
        callback: Callback,
        /** The animator to use to animate the window transition. */
        transitionAnimator: TransitionAnimator = DEFAULT_TRANSITION_ANIMATOR,
        transitionAnimator: TransitionAnimator,
        /** Listener for animation lifecycle events. */
        listener: Listener? = null
    ) : IRemoteAnimationRunner.Stub() {
        private val context = controller.transitionContainer.context

        // This is being passed across IPC boundaries and cycles (through PendingIntentRecords,
        // etc.) are possible. So we need to make sure we drop any references that might
        // transitively cause leaks when we're done with animation.
@@ -493,11 +499,12 @@ class ActivityTransitionAnimator(
        init {
            delegate =
                AnimationDelegate(
                    mainExecutor,
                    controller,
                    callback,
                    DelegatingAnimationCompletionListener(listener, this::dispose),
                    transitionAnimator,
                    disableWmTimeout
                    disableWmTimeout,
                )
        }

@@ -510,7 +517,7 @@ class ActivityTransitionAnimator(
            finishedCallback: IRemoteAnimationFinishedCallback?
        ) {
            val delegate = delegate
            context.mainExecutor.execute {
            mainExecutor.execute {
                if (delegate == null) {
                    Log.i(TAG, "onAnimationStart called after completion")
                    // Animation started too late and timed out already. We need to still
@@ -525,7 +532,7 @@ class ActivityTransitionAnimator(
        @BinderThread
        override fun onAnimationCancelled() {
            val delegate = delegate
            context.mainExecutor.execute {
            mainExecutor.execute {
                delegate ?: Log.wtf(TAG, "onAnimationCancelled called after completion")
                delegate?.onAnimationCancelled()
            }
@@ -535,19 +542,21 @@ class ActivityTransitionAnimator(
        fun dispose() {
            // Drop references to animation controller once we're done with the animation
            // to avoid leaking.
            context.mainExecutor.execute { delegate = null }
            mainExecutor.execute { delegate = null }
        }
    }

    class AnimationDelegate
    @JvmOverloads
    constructor(
        private val mainExecutor: Executor,
        private val controller: Controller,
        private val callback: Callback,
        /** Listener for animation lifecycle events. */
        private val listener: Listener? = null,
        /** The animator to use to animate the window transition. */
        private val transitionAnimator: TransitionAnimator = DEFAULT_TRANSITION_ANIMATOR,
        private val transitionAnimator: TransitionAnimator =
            defaultTransitionAnimator(mainExecutor),

        /**
         * Whether we should disable the WindowManager timeout. This should be set to true in tests
+11 −19
Original line number Diff line number Diff line
@@ -37,6 +37,7 @@ import com.android.internal.jank.Cuj.CujType
import com.android.internal.jank.InteractionJankMonitor
import com.android.systemui.util.maybeForceFullscreen
import com.android.systemui.util.registerAnimationOnBackInvoked
import java.util.concurrent.Executor
import kotlin.math.roundToInt

private const val TAG = "DialogTransitionAnimator"
@@ -55,10 +56,16 @@ private const val TAG = "DialogTransitionAnimator"
class DialogTransitionAnimator
@JvmOverloads
constructor(
    private val mainExecutor: Executor,
    private val callback: Callback,
    private val interactionJankMonitor: InteractionJankMonitor,
    private val featureFlags: AnimationFeatureFlags,
    private val transitionAnimator: TransitionAnimator = TransitionAnimator(TIMINGS, INTERPOLATORS),
    private val transitionAnimator: TransitionAnimator =
        TransitionAnimator(
            mainExecutor,
            TIMINGS,
            INTERPOLATORS,
        ),
    private val isForTesting: Boolean = false,
) {
    private companion object {
@@ -937,25 +944,10 @@ private class AnimatedDialog(
                }

                override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) {
                    // onLaunchAnimationEnd is called by an Animator at the end of the animation,
                    // on a Choreographer animation tick. The following calls will move the animated
                    // content from the dialog overlay back to its original position, and this
                    // change must be reflected in the next frame given that we then sync the next
                    // frame of both the content and dialog ViewRoots. However, in case that content
                    // is rendered by Compose, whose compositions are also scheduled on a
                    // Choreographer frame, any state change made *right now* won't be reflected in
                    // the next frame given that a Choreographer frame can't schedule another and
                    // have it happen in the same frame. So we post the forwarded calls to
                    // [Controller.onLaunchAnimationEnd], leaving this Choreographer frame, ensuring
                    // that the move of the content back to its original window will be reflected in
                    // the next frame right after [onLaunchAnimationEnd] is called.
                    dialog.context.mainExecutor.execute {
                    startController.onTransitionAnimationEnd(isExpandingFullyAbove)
                    endController.onTransitionAnimationEnd(isExpandingFullyAbove)

                    onLaunchAnimationEnd()
                }
                }

                override fun onTransitionAnimationProgress(
                    state: TransitionAnimator.State,
+26 −5
Original line number Diff line number Diff line
@@ -31,12 +31,17 @@ import android.view.animation.Interpolator
import androidx.annotation.VisibleForTesting
import com.android.app.animation.Interpolators.LINEAR
import com.android.systemui.shared.Flags.returnAnimationFrameworkLibrary
import java.util.concurrent.Executor
import kotlin.math.roundToInt

private const val TAG = "TransitionAnimator"

/** A base class to animate a window (activity or dialog) launch to or return from a view . */
class TransitionAnimator(private val timings: Timings, private val interpolators: Interpolators) {
class TransitionAnimator(
    private val mainExecutor: Executor,
    private val timings: Timings,
    private val interpolators: Interpolators,
) {
    companion object {
        internal const val DEBUG = false
        private val SRC_MODE = PorterDuffXfermode(PorterDuff.Mode.SRC)
@@ -351,6 +356,21 @@ class TransitionAnimator(private val timings: Timings, private val interpolators
                    if (DEBUG) {
                        Log.d(TAG, "Animation ended")
                    }

                    // onAnimationEnd is called at the end of the animation, on a Choreographer
                    // animation tick. During dialog launches, the following calls will move the
                    // animated content from the dialog overlay back to its original position, and
                    // this change must be reflected in the next frame given that we then sync the
                    // next frame of both the content and dialog ViewRoots. During SysUI activity
                    // launches, we will instantly collapse the shade at the end of the transition.
                    // However, if those are rendered by Compose, whose compositions are also
                    // scheduled on a Choreographer frame, any state change made *right now* won't
                    // be reflected in the next frame given that a Choreographer frame can't
                    // schedule another and have it happen in the same frame. So we post the
                    // forwarded calls to [Controller.onLaunchAnimationEnd] in the main executor,
                    // leaving this Choreographer frame, ensuring that any state change applied by
                    // onTransitionAnimationEnd() will be reflected in the same frame.
                    mainExecutor.execute {
                        controller.onTransitionAnimationEnd(isExpandingFullyAbove)
                        transitionContainerOverlay.remove(windowBackgroundLayer)

@@ -359,6 +379,7 @@ class TransitionAnimator(private val timings: Timings, private val interpolators
                        }
                    }
                }
            }
        )

        animator.addUpdateListener { animation ->
+8 −5
Original line number Diff line number Diff line
@@ -32,6 +32,7 @@ import com.android.systemui.animation.DialogTransitionAnimator;
import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dump.DumpHandler;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.media.controls.domain.pipeline.MediaDataManager;
@@ -56,10 +57,10 @@ import com.android.systemui.statusbar.notification.collection.render.Notificatio
import com.android.systemui.statusbar.phone.CentralSurfacesImpl;
import com.android.systemui.statusbar.phone.ManagedProfileController;
import com.android.systemui.statusbar.phone.ManagedProfileControllerImpl;
import com.android.systemui.statusbar.phone.ui.StatusBarIconList;
import com.android.systemui.statusbar.phone.StatusBarRemoteInputCallback;
import com.android.systemui.statusbar.phone.ui.StatusBarIconController;
import com.android.systemui.statusbar.phone.ui.StatusBarIconControllerImpl;
import com.android.systemui.statusbar.phone.ui.StatusBarIconList;
import com.android.systemui.statusbar.policy.KeyguardStateController;

import dagger.Binds;
@@ -209,14 +210,16 @@ public interface CentralSurfacesDependenciesModule {
    /** */
    @Provides
    @SysUISingleton
    static ActivityTransitionAnimator provideActivityTransitionAnimator() {
        return new ActivityTransitionAnimator();
    static ActivityTransitionAnimator provideActivityTransitionAnimator(
            @Main Executor mainExecutor) {
        return new ActivityTransitionAnimator(mainExecutor);
    }

    /** */
    @Provides
    @SysUISingleton
    static DialogTransitionAnimator provideDialogTransitionAnimator(IDreamManager dreamManager,
    static DialogTransitionAnimator provideDialogTransitionAnimator(@Main Executor mainExecutor,
            IDreamManager dreamManager,
            KeyguardStateController keyguardStateController,
            Lazy<AlternateBouncerInteractor> alternateBouncerInteractor,
            InteractionJankMonitor interactionJankMonitor,
@@ -243,7 +246,7 @@ public interface CentralSurfacesDependenciesModule {
            }
        };
        return new DialogTransitionAnimator(
                callback, interactionJankMonitor, animationFeatureFlags);
                mainExecutor, callback, interactionJankMonitor, animationFeatureFlags);
    }

    /** */
+4 −2
Original line number Diff line number Diff line
@@ -46,7 +46,8 @@ import org.mockito.junit.MockitoJUnit
@RunWithLooper
class ActivityTransitionAnimatorTest : SysuiTestCase() {
    private val transitionContainer = LinearLayout(mContext)
    private val testTransitionAnimator = fakeTransitionAnimator()
    private val mainExecutor = context.mainExecutor
    private val testTransitionAnimator = fakeTransitionAnimator(mainExecutor)
    @Mock lateinit var callback: ActivityTransitionAnimator.Callback
    @Mock lateinit var listener: ActivityTransitionAnimator.Listener
    @Spy private val controller = TestTransitionAnimatorController(transitionContainer)
@@ -59,9 +60,10 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() {
    fun setup() {
        activityTransitionAnimator =
            ActivityTransitionAnimator(
                mainExecutor,
                testTransitionAnimator,
                testTransitionAnimator,
                disableWmTimeout = true
                disableWmTimeout = true,
            )
        activityTransitionAnimator.callback = callback
        activityTransitionAnimator.addListener(listener)
Loading