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

Commit f6b942f0 authored by Luca Zuccarini's avatar Luca Zuccarini
Browse files

4 Update the long-lived transition APIs.

Note that for simplicity I didn't flag these on the library side,
since they're already flagged in the only place that they're used
anyway (see below).

Trying to make each of the CLs as small as possible to keep them
digestible and low risk. For the refactor plan see
go/animlib-shell-refactor-plan.

Bug: 397180418
Flag: com.android.systemui.status_bar_chips_return_animations
Test: atest ActivityTransitionAnimatorTest LegacyActivityStarterInternalImplTest + manual
Change-Id: Ifdca8c23aa6ccd68d9e2909b0e77bdeebf543f01
parent 0e98504c
Loading
Loading
Loading
Loading
+156 −114
Original line number Diff line number Diff line
@@ -80,13 +80,13 @@ private const val TAG = "ActivityTransitionAnimator"
 * nicely into the starting window.
 */
class ActivityTransitionAnimator
@JvmOverloads
@VisibleForTesting
constructor(
    /** The executor that runs on the main thread. */
    private val mainExecutor: Executor,

    /** The object used to register ephemeral returns and long-lived transitions. */
    private val transitionRegister: TransitionRegister? = null,
    /** The object used to register transitions with the WindowManager Shell. */
    private val transitionRegister: TransitionRegister,

    /** The animator used when animating a View into an app. */
    private val transitionAnimator: TransitionAnimator = defaultTransitionAnimator(mainExecutor),
@@ -608,7 +608,7 @@ constructor(
        return Pair(launchTransition, returnTransition)
    }

    /** Creates and registers a [RemoteTransition] that unregisters itself once it has run once. */
    /** Creates and registers a [RemoteTransition] that unregisters itself after running once. */
    private fun registerEphemeralTransition(
        controller: Controller,
        cookie: TransitionCookie,
@@ -616,6 +616,9 @@ constructor(
        isLaunch: Boolean,
        label: String,
    ): RemoteTransition {
        // This runnable is used to unregister the transition once it is run. Since it needs to be
        // accessible by the factory, we declare it first and assign its value once the transition
        // is created using that factory.
        var cleanUpRunnable: Runnable? = null

        val controllerFactory =
@@ -630,40 +633,167 @@ constructor(
                    }
                }
            }
        val remoteTransition =
            registerTransition(
                controllerFactory,
                scope,
                isLongLived = false,
                isLaunch = isLaunch,
                isDialogLaunch = controller.isDialogLaunch,
                label = label,
                cleanUp = { cleanUpRunnable?.run() },
            )

        val typeSet =
            if (isLaunch) {
                intArrayOf(TRANSIT_OPEN, TRANSIT_TO_FRONT)
            } else {
                intArrayOf(TRANSIT_CLOSE, TRANSIT_TO_BACK)
        cleanUpRunnable = Runnable { transitionRegister.unregister(remoteTransition) }

        return remoteTransition
    }
        val filter =
            TransitionFilter().apply {
                mTypeSet = typeSet
                mRequirements =
                    arrayOf(
                        TransitionFilter.Requirement().apply {
                            mLaunchCookie = controllerFactory.cookie
                            mModes = typeSet

    /**
     * Registers [controllerFactory] as a long-lived transition handler for launch and return
     * animations.
     *
     * The [Controller]s created by [controllerFactory] will only be used for transitions matching
     * the [cookie] and the [ComponentName] defined within. These [Controller]s can only be created
     * within [scope].
     *
     * Note that [cookie] must match [controllerFactory]'s internal cookie.
     */
    fun registerLongLivedTransitions(
        cookie: TransitionCookie,
        controllerFactory: ControllerFactory,
        scope: CoroutineScope,
    ) {
        check(cookie == controllerFactory.cookie) {
            "Cookie ($cookie) does not match the factory's cookie (${controllerFactory.cookie}"
        }

        check(controllerFactory.component != null) {
            "A component must be defined in order to use long-lived animations"
        }

        // Make sure that any previous registrations linked to the same cookie are gone.
        unregister(cookie)

        val launchTransition =
            registerTransition(
                controllerFactory,
                scope,
                isLongLived = true,
                isLaunch = true,
                isDialogLaunch = false,
                label = "${cookie}_launchTransition",
            )
        val returnTransition =
            registerTransition(
                controllerFactory,
                scope,
                isLongLived = true,
                isLaunch = false,
                isDialogLaunch = false,
                label = "${cookie}_returnTransition",
            )

        longLivedTransitions[cookie] = Pair(launchTransition, returnTransition)
    }

    private fun registerTransition(
        controllerFactory: ControllerFactory,
        scope: CoroutineScope,
        isLongLived: Boolean,
        isLaunch: Boolean,
        isDialogLaunch: Boolean,
        label: String,
        cleanUp: (() -> Unit)? = null,
    ): RemoteTransition {
        val filter =
            createTransitionFilter(
                controllerFactory.cookie,
                controllerFactory.component,
                isLongLived = isLongLived,
                isLaunch = isLaunch,
            )
        val remoteTransition =
            RemoteTransition(
                createOriginTransition(
                    createController = { controllerFactory.createController(isLaunch) },
                    scope,
                    isDialogLaunch = controller.isDialogLaunch,
                    cleanUp = { cleanUpRunnable?.run() },
                    isDialogLaunch = isDialogLaunch,
                    cleanUp = cleanUp,
                ),
                label,
            )
        transitionRegister.register(filter, remoteTransition, includeTakeover = isLongLived)
        return remoteTransition
    }

        cleanUpRunnable = Runnable { transitionRegister?.unregister(remoteTransition) }
        transitionRegister?.register(filter, remoteTransition, includeTakeover = false)
    private fun createTransitionFilter(
        cookie: TransitionCookie,
        component: ComponentName?,
        isLongLived: Boolean,
        isLaunch: Boolean,
    ): TransitionFilter {
        val openingModes = intArrayOf(TRANSIT_OPEN, TRANSIT_TO_FRONT)
        val closingModes = intArrayOf(TRANSIT_CLOSE, TRANSIT_TO_BACK)

        return remoteTransition
        return TransitionFilter().apply {
            if (isLongLived) {
                if (isLaunch) {
                    // Simply match the cookie and component for the opening window.
                    mRequirements =
                        arrayOf(
                            TransitionFilter.Requirement().apply {
                                mActivityType = WindowConfiguration.ACTIVITY_TYPE_STANDARD
                                mLaunchCookie = cookie
                                mModes = openingModes
                                mTopActivity = component
                            }
                        )
                } else {
                    // Cross-task close transitions should not use this animation, so we only
                    // filter it for when the opening window is Launcher.
                    mRequirements =
                        arrayOf(
                            TransitionFilter.Requirement().apply {
                                mActivityType = WindowConfiguration.ACTIVITY_TYPE_STANDARD
                                mModes = closingModes
                                mTopActivity = component
                            },
                            TransitionFilter.Requirement().apply {
                                mActivityType = WindowConfiguration.ACTIVITY_TYPE_HOME
                                mModes = openingModes
                            },
                        )
                }
            } else {
                // Ephemeral transitions are simpler and behave the same in both directions.
                val modes =
                    if (isLaunch) {
                        openingModes
                    } else {
                        closingModes
                    }

                mTypeSet = modes
                mRequirements =
                    arrayOf(
                        TransitionFilter.Requirement().apply {
                            mLaunchCookie = cookie
                            mModes = modes
                        }
                    )
            }
        }
    }

    /** Unregisters all controllers previously registered that contain [cookie]. */
    fun unregisterLongLivedTransitions(cookie: TransitionCookie) = unregister(cookie)

    private fun unregister(cookie: TransitionCookie) {
        val transitions = longLivedTransitions[cookie] ?: return
        transitionRegister.unregister(transitions.first)
        transitionRegister.unregister(transitions.second)
        longLivedTransitions.remove(cookie)
    }

    /**
@@ -1024,94 +1154,6 @@ constructor(
        fun onDispose() {}
    }

    /**
     * Registers [controllerFactory] as a long-lived transition handler for launch and return
     * animations.
     *
     * The [Controller]s created by [controllerFactory] will only be used for transitions matching
     * the [cookie], or the [ComponentName] defined within it if the cookie matching fails. These
     * [Controller]s can only be created within [scope].
     */
    fun register(
        cookie: TransitionCookie,
        controllerFactory: ControllerFactory,
        scope: CoroutineScope,
    ) {
        if (transitionRegister == null) {
            throw IllegalStateException(
                "A RemoteTransitionRegister must be provided when creating this animator in " +
                    "order to use long-lived animations"
            )
        }

        val component =
            controllerFactory.component
                ?: throw IllegalStateException(
                    "A component must be defined in order to use long-lived animations"
                )

        // Make sure that any previous registrations linked to the same cookie are gone.
        unregister(cookie)

        val launchFilter =
            TransitionFilter().apply {
                mRequirements =
                    arrayOf(
                        TransitionFilter.Requirement().apply {
                            mActivityType = WindowConfiguration.ACTIVITY_TYPE_STANDARD
                            mModes = intArrayOf(TRANSIT_OPEN, TRANSIT_TO_FRONT)
                            mTopActivity = component
                        }
                    )
            }
        val launchRemoteTransition =
            RemoteTransition(
                LegacyOriginTransition(
                    createLongLivedRunner(controllerFactory, scope, forLaunch = true)
                ),
                "${cookie}_launchTransition",
            )
        // TODO(b/403529740): re-enable takeovers once we solve the Compose jank issues.
        transitionRegister.register(launchFilter, launchRemoteTransition, includeTakeover = false)

        // Cross-task close transitions should not use this animation, so we only register it for
        // when the opening window is Launcher.
        val returnFilter =
            TransitionFilter().apply {
                mRequirements =
                    arrayOf(
                        TransitionFilter.Requirement().apply {
                            mActivityType = WindowConfiguration.ACTIVITY_TYPE_STANDARD
                            mModes = intArrayOf(TRANSIT_CLOSE, TRANSIT_TO_BACK)
                            mTopActivity = component
                        },
                        TransitionFilter.Requirement().apply {
                            mActivityType = WindowConfiguration.ACTIVITY_TYPE_HOME
                            mModes = intArrayOf(TRANSIT_OPEN, TRANSIT_TO_FRONT)
                        },
                    )
            }
        val returnRemoteTransition =
            RemoteTransition(
                LegacyOriginTransition(
                    createLongLivedRunner(controllerFactory, scope, forLaunch = false)
                ),
                "${cookie}_returnTransition",
            )
        // TODO(b/403529740): re-enable takeovers once we solve the Compose jank issues.
        transitionRegister.register(returnFilter, returnRemoteTransition, includeTakeover = false)

        longLivedTransitions[cookie] = Pair(launchRemoteTransition, returnRemoteTransition)
    }

    /** Unregisters all controllers previously registered that contain [cookie]. */
    fun unregister(cookie: TransitionCookie) {
        val transitions = longLivedTransitions[cookie] ?: return
        transitionRegister?.unregister(transitions.first)
        transitionRegister?.unregister(transitions.second)
        longLivedTransitions.remove(cookie)
    }

    /**
     * Invokes [onAnimationComplete] when animation is either cancelled or completed. Delegates all
     * events to the passed [delegate].
+3 −2
Original line number Diff line number Diff line
@@ -164,7 +164,8 @@ class LegacyActivityStarterInternalImplTest : SysuiTestCase() {

                underTest.registerTransition(cookie, controllerFactory, testScope)

                verify(activityTransitionAnimator).register(eq(cookie), any(), eq(testScope))
                verify(activityTransitionAnimator)
                    .registerLongLivedTransitions(eq(cookie), any(), eq(testScope))
            }
        }
    }
@@ -175,7 +176,7 @@ class LegacyActivityStarterInternalImplTest : SysuiTestCase() {

        underTest.unregisterTransition(cookie)

        verify(activityTransitionAnimator).unregister(eq(cookie))
        verify(activityTransitionAnimator).unregisterLongLivedTransitions(eq(cookie))
    }

    @Test
+2 −2
Original line number Diff line number Diff line
@@ -139,11 +139,11 @@ constructor(
                }
            }

        activityTransitionAnimator.register(cookie, factory, scope)
        activityTransitionAnimator.registerLongLivedTransitions(cookie, factory, scope)
    }

    override fun unregisterTransition(cookie: ActivityTransitionAnimator.TransitionCookie) {
        activityTransitionAnimator.unregister(cookie)
        activityTransitionAnimator.unregisterLongLivedTransitions(cookie)
    }

    override fun startPendingIntentDismissingKeyguard(
+2 −2
Original line number Diff line number Diff line
@@ -135,11 +135,11 @@ constructor(
                }
            }

        activityTransitionAnimator.register(cookie, factory, scope)
        activityTransitionAnimator.registerLongLivedTransitions(cookie, factory, scope)
    }

    override fun unregisterTransition(cookie: ActivityTransitionAnimator.TransitionCookie) {
        activityTransitionAnimator.unregister(cookie)
        activityTransitionAnimator.unregisterLongLivedTransitions(cookie)
    }

    override fun startActivityDismissingKeyguard(
+23 −26
Original line number Diff line number Diff line
@@ -732,28 +732,28 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() {
        kosmos.runTest {
            val controller = createController()
            var factory = controllerFactory(controller)
            underTest.register(factory.cookie, factory, testScope)
            underTest.registerLongLivedTransitions(factory.cookie, factory, testScope)
            assertThat(testShellTransitions.remotes.size).isEqualTo(2)
            assertThat(testShellTransitions.remotesForTakeover).isEmpty()
            assertThat(testShellTransitions.remotesForTakeover.size).isEqualTo(2)

            factory = controllerFactory(controller)
            underTest.register(factory.cookie, factory, testScope)
            underTest.registerLongLivedTransitions(factory.cookie, factory, testScope)
            assertThat(testShellTransitions.remotes.size).isEqualTo(4)
            assertThat(testShellTransitions.remotesForTakeover).isEmpty()
            assertThat(testShellTransitions.remotesForTakeover.size).isEqualTo(4)
        }
    }

    @Test
    fun registersLongLivedTransitionOverridingPreviousRegistration() {
    fun registersLongLivedTransition_overridingPreviousRegistration() {
        kosmos.runTest {
            val controller = createController()
            val cookie = ActivityTransitionAnimator.TransitionCookie("test_cookie")
            var factory = controllerFactory(controller, cookie)
            underTest.register(cookie, factory, testScope)
            underTest.registerLongLivedTransitions(cookie, factory, testScope)
            val transitions = testShellTransitions.remotes.values.toList()

            factory = controllerFactory(controller, cookie)
            underTest.register(cookie, factory, testScope)
            underTest.registerLongLivedTransitions(cookie, factory, testScope)
            assertThat(testShellTransitions.remotes.size).isEqualTo(2)
            for (transition in transitions) {
                assertThat(testShellTransitions.remotes.values).doesNotContain(transition)
@@ -762,28 +762,25 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() {
    }

    @Test
    fun doesNotRegisterLongLivedTransitionIfMissingRequiredProperties() {
    fun doesNotRegisterLongLivedTransition_ifMissingRequiredProperties() {
        kosmos.runTest {
            val controller = createController()

            // No ComponentName
            var factory = controllerFactory(controller, component = null)
            // Cookies don't match
            val cookie = ActivityTransitionAnimator.TransitionCookie("test_cookie")
            var factory = controllerFactory(controller, cookie)
            assertThrows(IllegalStateException::class.java) {
                underTest.register(factory.cookie, factory, testScope)
                underTest.registerLongLivedTransitions(
                    ActivityTransitionAnimator.TransitionCookie("wrong_cookie"),
                    factory,
                    testScope,
                )
            }

            // No TransitionRegister
            val activityTransitionAnimator =
                ActivityTransitionAnimator(
                    mainExecutor,
                    transitionRegister = null,
                    testTransitionAnimator,
                    testTransitionAnimator,
                    disableWmTimeout = true,
                )
            factory = controllerFactory(controller)
            // No ComponentName
            factory = controllerFactory(controller, component = null)
            assertThrows(IllegalStateException::class.java) {
                activityTransitionAnimator.register(factory.cookie, factory, testScope)
                underTest.registerLongLivedTransitions(factory.cookie, factory, testScope)
            }
        }
    }
@@ -797,16 +794,16 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() {
            for (index in 0 until 3) {
                cookies[index] = mock(ActivityTransitionAnimator.TransitionCookie::class.java)
                val factory = controllerFactory(controller, cookies[index]!!)
                underTest.register(factory.cookie, factory, testScope)
                underTest.registerLongLivedTransitions(factory.cookie, factory, testScope)
            }

            underTest.unregister(cookies[0]!!)
            underTest.unregisterLongLivedTransitions(cookies[0]!!)
            assertThat(testShellTransitions.remotes.size).isEqualTo(4)

            underTest.unregister(cookies[2]!!)
            underTest.unregisterLongLivedTransitions(cookies[2]!!)
            assertThat(testShellTransitions.remotes.size).isEqualTo(2)

            underTest.unregister(cookies[1]!!)
            underTest.unregisterLongLivedTransitions(cookies[1]!!)
            assertThat(testShellTransitions.remotes).isEmpty()
        }
    }
Loading