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

Commit 6a7316ba authored by Luca Zuccarini's avatar Luca Zuccarini Committed by Android (Google) Code Review
Browse files

Merge changes from topics "b/397180418-gallery", "b/397180418-keyguard" into main

* changes:
  4 Update the long-lived transition APIs.
  3.7 Update Keyguard to use the modern Animation Library APIs.
  3.6 Implement a modern version of leash setup.
parents 89612698 f6b942f0
Loading
Loading
Loading
Loading
+234 −159
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),
@@ -390,10 +390,6 @@ constructor(
                    controller,
                    cookie,
                    scope,
                    callback,
                    transitionAnimator,
                    lifecycleListener,
                    transitionRegister,
                    includeReturn = animateReturn,
                )

@@ -437,11 +433,7 @@ constructor(
                return
            }

            val callback =
                this.callback
                    ?: throw IllegalStateException(
                        "ActivityTransitionAnimator.callback must be set before using this animator"
                    )
            val callback = validateCallback()
            val hideKeyguardWithAnimation = callback.isOnKeyguard() && !showOverLockscreen

            val runner = createEphemeralRunner(controller)
@@ -586,10 +578,6 @@ constructor(
        controller: Controller,
        cookie: TransitionCookie,
        scope: CoroutineScope,
        callback: Callback,
        transitionAnimator: TransitionAnimator,
        listener: Listener?,
        transitionRegister: TransitionRegister?,
        includeReturn: Boolean,
    ): Pair<RemoteTransition, RemoteTransition?> {
        // Make sure that any previous registrations linked to the same cookie are gone.
@@ -602,10 +590,6 @@ constructor(
                scope,
                isLaunch = true,
                label = "${cookie}_launchTransition",
                callback,
                transitionAnimator,
                listener,
                transitionRegister,
            )

        val returnTransition =
@@ -616,10 +600,6 @@ constructor(
                    scope,
                    isLaunch = false,
                    label = "${cookie}_returnTransition",
                    callback,
                    transitionAnimator,
                    listener,
                    transitionRegister,
                )
            } else {
                null
@@ -628,18 +608,17 @@ 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,
        scope: CoroutineScope,
        isLaunch: Boolean,
        label: String,
        callback: Callback,
        transitionAnimator: TransitionAnimator,
        listener: Listener?,
        transitionRegister: TransitionRegister?,
    ): 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 =
@@ -654,42 +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(
                OriginTransition(
                createOriginTransition(
                    createController = { controllerFactory.createController(isLaunch) },
                    scope,
                    callback,
                    transitionAnimator,
                    listener,
                    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)
    }

    /**
@@ -762,12 +866,68 @@ constructor(
        listeners.remove(listener)
    }

    /**
     * Create a new [IRemoteTransition] controlled by [controller].
     *
     * [scope] must remain valid until the transition is guaranteed to never be invoked anymore.
     */
    fun createOriginTransition(
        controller: Controller,
        scope: CoroutineScope,
        isDialogLaunch: Boolean = false,
        transitionHelper: RemoteTransitionHelper = DefaultTransitionHelper(),
    ): IRemoteTransition {
        check(shellMigrationEnabled()) {
            "Attempted to use the new APIs, but the animationLibraryShellMigration flag is disabled"
        }

        return createOriginTransition(
            createController = { controller },
            scope,
            isDialogLaunch = isDialogLaunch,
            transitionHelper = transitionHelper,
        )
    }

    private fun createOriginTransition(
        createController: suspend () -> Controller,
        scope: CoroutineScope,
        isDialogLaunch: Boolean = false,
        cleanUp: (() -> Unit)? = null,
        transitionHelper: RemoteTransitionHelper = DefaultTransitionHelper(),
    ): OriginTransition {
        // Make sure we use the modified timings when animating a dialog into an app.
        val transitionAnimator =
            if (isDialogLaunch) {
                dialogToAppAnimator
            } else {
                transitionAnimator
            }

        return OriginTransition(
            createController,
            scope,
            validateCallback(),
            transitionAnimator,
            lifecycleListener,
            cleanUp,
            transitionHelper,
        )
    }

    private fun validateCallback(): Callback {
        return checkNotNull(callback) {
            "ActivityTransitionAnimator.callback must be set before using this animator"
        }
    }

    /**
     * Create a new animation [Runner] controlled by [controller].
     *
     * This method must only be used for ephemeral (launch or return) transitions. Otherwise, use
     * [createLongLivedRunner].
     */
    @Deprecated("Use createOriginTransition() instead.")
    @VisibleForTesting
    fun createEphemeralRunner(controller: Controller): Runner {
        // Make sure we use the modified timings when animating a dialog into an app.
@@ -994,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].
@@ -1123,12 +1195,13 @@ constructor(
     * [CoroutineScope] which [createController] will use to provide the [Controller].
     */
    private inner class OriginTransition(
        private val createController: (suspend () -> Controller),
        private val createController: suspend () -> Controller,
        private val scope: CoroutineScope,
        private val callback: Callback,
        private val transitionAnimator: TransitionAnimator,
        private val listener: Listener?,
        private val cleanUp: (() -> Unit)? = null,
        private val transitionHelper: RemoteTransitionHelper,
    ) : RemoteTransitionStub() {
        private val timeoutHandler =
            if (disableWmTimeout) {
@@ -1189,22 +1262,23 @@ constructor(
            startTransaction: SurfaceControl.Transaction?,
            finishCallback: IRemoteTransitionFinishedCallback?,
        ) {
            if (info == null || startTransaction == null) {
            if (token == null || info == null || startTransaction == null) {
                Log.e(
                    TAG,
                    "Skipping the animation because the required data is missing: info=$info, " +
                        "startTransaction=$startTransaction",
                    "Skipping the animation because the required data is missing: token=$token, " +
                        "info=$info, startTransaction=$startTransaction",
                )
                finishCallback?.invoke(info)
                finishCallback?.invoke(token, info)
                return
            }

            initAndRun(onFailure = { finishCallback?.invoke(info) }) {
            initAndRun(onFailure = { finishCallback?.invoke(token, info) }) {
                transitionHelper.setUpAnimation(token, info, startTransaction, finishCallback)
                performAnimation(delegate) { delegate ->
                    delegate.onAnimationStart(
                        info,
                        startTransaction = startTransaction,
                        onAnimationFinished = { finishCallback?.invoke(info) },
                        onAnimationFinished = { finishCallback?.invoke(token, info) },
                    )
                }
            }
@@ -1212,28 +1286,29 @@ constructor(

        @BinderThread
        override fun takeOverAnimation(
            transition: IBinder?,
            token: IBinder?,
            info: TransitionInfo?,
            startTransaction: SurfaceControl.Transaction?,
            finishCallback: IRemoteTransitionFinishedCallback?,
            states: Array<out WindowAnimationState>,
        ) {
            if (info == null || startTransaction == null) {
            if (token == null || info == null || startTransaction == null) {
                Log.e(
                    TAG,
                    "Skipping the animation takeover because the required data is missing: " +
                        "info=$info, startTransaction=$startTransaction",
                        "token=$token, info=$info, startTransaction=$startTransaction",
                )
                finishCallback?.invoke(info)
                finishCallback?.invoke(token, info)
                return
            }

            initAndRun(onFailure = { finishCallback?.invoke(info) }) {
            initAndRun(onFailure = { finishCallback?.invoke(token, info) }) {
                transitionHelper.setUpAnimation(token, info, startTransaction, finishCallback)
                performAnimation(delegate) { delegate ->
                    delegate.takeOverAnimation(
                        info,
                        startTransaction = startTransaction,
                        onAnimationFinished = { finishCallback?.invoke(info) },
                        onAnimationFinished = { finishCallback?.invoke(token, info) },
                        states,
                    )
                }
@@ -1281,7 +1356,7 @@ constructor(
        }

        override fun mergeAnimation(
            transition: IBinder?,
            token: IBinder?,
            info: TransitionInfo?,
            transaction: SurfaceControl.Transaction?,
            mergeTarget: IBinder?,
@@ -1289,12 +1364,11 @@ constructor(
        ) {
            removeTimeouts()
            transaction?.close()
            info?.releaseAllSurfaces()
            mainExecutor.execute {
                cancelled = true
                delegate?.onAnimationCancelled()
            }
            finishCallback?.invoke(info)
            finishCallback?.invoke(token, info)
        }

        override fun onTransitionConsumed(transition: IBinder?, aborted: Boolean) {
@@ -1336,10 +1410,11 @@ constructor(
                timedOut = false
            }

        fun IRemoteTransitionFinishedCallback.invoke(info: TransitionInfo?) {
        fun IRemoteTransitionFinishedCallback.invoke(token: IBinder?, info: TransitionInfo?) {
            info?.releaseAllSurfaces()

            val finishTransaction = SurfaceControl.Transaction()
            token?.let { transitionHelper.cleanUpAnimation(token, finishTransaction) }
            try {
                onTransitionFinished(null, finishTransaction)
            } catch (e: RemoteException) {
+230 −0

File added.

Preview size limit exceeded, changes collapsed.

+54 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.animation

import android.os.IBinder
import android.view.SurfaceControl
import android.view.WindowManager.TRANSIT_CLOSE
import android.view.WindowManager.TRANSIT_OPEN
import android.view.WindowManager.TRANSIT_TO_BACK
import android.view.WindowManager.TRANSIT_TO_FRONT
import android.window.IRemoteTransitionFinishedCallback
import android.window.TransitionInfo

/**
 * Delegate for Animation Library remote transitions that takes care of initializing the changes and
 * cleaning up once the transition is done.
 */
interface RemoteTransitionHelper {
    companion object {
        val CLOSING_MODES = setOf(TRANSIT_CLOSE, TRANSIT_TO_BACK)
        val OPENING_MODES = setOf(TRANSIT_OPEN, TRANSIT_TO_FRONT)
    }

    /**
     * Applies transactions common to all remote transitions, such as setting up the alpha and
     * rotation of various changes. Invoked before the animation starts.
     */
    fun setUpAnimation(
        token: IBinder,
        info: TransitionInfo,
        transaction: SurfaceControl.Transaction,
        finishCallback: IRemoteTransitionFinishedCallback?,
    )

    /**
     * Cleans up any state leftover after [setUpAnimation] is called. Invoked right before the
     * transition's finish callback.
     */
    fun cleanUpAnimation(token: IBinder, transaction: SurfaceControl.Transaction)
}
+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
+108 −0

File added.

Preview size limit exceeded, changes collapsed.

Loading