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

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

3.6 Implement a modern version of leash setup.

The new DefaultTransitionHelper implements the same leash setup logic
as RemoteAnimationRunneCompat. It is introduced as an interface so
that Keyguard can have its own custom implementation (upcoming).

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.animation_library_shell_migration
Test: atest ActivityTransitionAnimatorTest + manual
Change-Id: I9d0ecb50188e66be50619a3f34c5261f2a328581
parent c2a0f226
Loading
Loading
Loading
Loading
+79 −46
Original line number Diff line number Diff line
@@ -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
@@ -635,10 +615,6 @@ constructor(
        scope: CoroutineScope,
        isLaunch: Boolean,
        label: String,
        callback: Callback,
        transitionAnimator: TransitionAnimator,
        listener: Listener?,
        transitionRegister: TransitionRegister?,
    ): RemoteTransition {
        var cleanUpRunnable: Runnable? = null

@@ -675,12 +651,10 @@ constructor(

        val remoteTransition =
            RemoteTransition(
                OriginTransition(
                createOriginTransition(
                    createController = { controllerFactory.createController(isLaunch) },
                    scope,
                    callback,
                    transitionAnimator,
                    listener,
                    isDialogLaunch = controller.isDialogLaunch,
                    cleanUp = { cleanUpRunnable?.run() },
                ),
                label,
@@ -762,12 +736,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.
@@ -1123,12 +1153,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 +1220,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 +1244,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 +1314,7 @@ constructor(
        }

        override fun mergeAnimation(
            transition: IBinder?,
            token: IBinder?,
            info: TransitionInfo?,
            transaction: SurfaceControl.Transaction?,
            mergeTarget: IBinder?,
@@ -1289,12 +1322,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 +1368,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
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.app.WindowConfiguration.ACTIVITY_TYPE_HOME
import android.os.IBinder
import android.util.Log
import android.util.RotationUtils
import android.view.SurfaceControl
import android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_EXIT_BY_MINIMIZE_TRANSITION_BUGFIX
import android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS_BUGFIX
import android.window.IRemoteTransitionFinishedCallback
import android.window.TransitionInfo
import android.window.WindowContainerToken
import com.android.systemui.animation.RemoteTransitionHelper.Companion.CLOSING_MODES
import com.android.systemui.animation.RemoteTransitionHelper.Companion.OPENING_MODES
import com.android.wm.shell.shared.CounterRotator
import com.android.wm.shell.shared.TransitionUtil.FLAG_IS_DESKTOP_WALLPAPER_ACTIVITY
import com.android.wm.shell.shared.TransitionUtil.isClosingMode
import com.android.wm.shell.shared.TransitionUtil.isClosingType

private const val TAG = "DefaultTransitionHelper"

/**
 * General purpose RemoteTransitionHelper that initializes changes with particular attention to the
 * wallpaper and Launcher.
 */
class DefaultTransitionHelper : RemoteTransitionHelper {
    private val launcherRotators = mutableMapOf<IBinder, CounterRotator>()
    private val wallpaperRotators = mutableMapOf<IBinder, CounterRotator>()

    override fun setUpAnimation(
        token: IBinder,
        info: TransitionInfo,
        transaction: SurfaceControl.Transaction,
        finishCallback: IRemoteTransitionFinishedCallback?,
    ) {
        val launcherRotator = CounterRotator()
        launcherRotators[token] = launcherRotator
        val wallpaperRotator = CounterRotator()
        wallpaperRotators[token] = wallpaperRotator

        var launcher: Launcher? = null
        var rotationDelta = 0
        var displayWidth = 0f
        var displayHeight = 0f

        // First we extract the Launcher (if it is part of the transition) and the rotation delta,
        // as we will need them to process each change.
        info.changes.forEachIndexed { index, change ->
            // No need to keep going if we already have the info we needed to extract.
            if (launcher != null && rotationDelta != 0) return

            // Identify the Launcher task, its position, and its role in the transition.
            if (change.taskInfo?.activityType == ACTIVITY_TYPE_HOME) {
                val isOpening = OPENING_MODES.contains(change.mode)
                launcher =
                    Launcher(
                        change,
                        // If Launcher is opening, make sure it is at the back so we can put other
                        // surfaces in front of it. Otherwise give it the default layer (reverse of
                        // position in changes).
                        layer =
                            if (isOpening) {
                                info.changes.size * 3
                            } else {
                                info.changes.size - index
                            },
                        parentToken = change.parent,
                        info = info,
                        isOpening = isOpening,
                    )
            }

            // Find the rotation delta. This is the same across all surfaces that have a non-zero
            // value, so we just need to find the first one. We only look at root surfaces for this,
            // as child surfaces will inherit their parent's rotation.
            if (change.parent == null && rotationDelta == 0) {
                rotationDelta =
                    RotationUtils.deltaRotation(change.startRotation, change.endRotation)
                displayWidth = change.endAbsBounds.width().toFloat()
                displayHeight = change.endAbsBounds.height().toFloat()
            }
        }

        if (launcher?.parent != null && rotationDelta != 0) {
            launcherRotator.setup(
                transaction,
                launcher!!.parent!!.leash,
                rotationDelta,
                displayWidth,
                displayHeight,
            )
            transaction.setLayer(launcherRotator.surface, launcher!!.layer)
        }

        if (launcher?.isOpening == true) {
            setUpReturnToHome(info, launcher!!, transaction, launcherRotator)
        } else {
            setUpInternal(
                info,
                launcher,
                launcherRotator,
                wallpaperRotator,
                rotationDelta,
                transaction,
                displayWidth,
                displayHeight,
            )
        }

        transaction.apply()
    }

    /** Sets up the surfaces for a transition that puts Launcher in front. */
    private fun setUpReturnToHome(
        info: TransitionInfo,
        launcher: Launcher,
        transaction: SurfaceControl.Transaction,
        launcherRotator: CounterRotator,
    ) {
        info.changes.forEachIndexed { index, change ->
            // We only care about independent surfaces, as dependent ones inherit their parent's
            // properties.
            if (!TransitionInfo.isIndependent(change, info)) return@forEachIndexed

            if (CLOSING_MODES.contains(change.mode)) {
                // Launcher expects closing surfaces to be "boosted" above itself in the ordering.
                transaction.setLayer(change.leash, info.changes.size * 3 - index)
                // Add the surface to the Launcher's rotator, so their rotations match.
                launcherRotator.addChild(transaction, change.leash)
            }

            // Make the wallpaper immediately visible.
            if ((change.flags and TransitionInfo.FLAG_IS_WALLPAPER) != 0) {
                transaction.show(change.leash)
                transaction.setAlpha(change.leash, 1f)
            }

            // If needed, reset the alpha of the Launcher leash to give the Launcher time to hide
            // its Views before the exit-desktop animation starts.
            if (ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS_BUGFIX.isTrue) {
                if (
                    !isClosingType(info.type) &&
                        !ENABLE_DESKTOP_WINDOWING_EXIT_BY_MINIMIZE_TRANSITION_BUGFIX.isTrue
                ) {
                    return@forEachIndexed
                }

                if (
                    isClosingMode(change.mode) &&
                        (change.taskInfo?.isFreeform == true ||
                            (change.flags and FLAG_IS_DESKTOP_WALLPAPER_ACTIVITY) != 0)
                ) {
                    transaction.setAlpha(launcher.change.leash, 0f)
                }
            }
        }
    }

    /** Sets up the surfaces for any transition that does not involve putting Launcher in front. */
    private fun setUpInternal(
        info: TransitionInfo,
        launcher: Launcher?,
        launcherRotator: CounterRotator,
        wallpaperRotator: CounterRotator,
        rotationDelta: Int,
        transaction: SurfaceControl.Transaction,
        displayWidth: Float,
        displayHeight: Float,
    ) {
        // Make sure Launcher is rotated using its own rotator.
        if (launcher != null) launcherRotator.addChild(transaction, launcher.change.leash)

        // Set up the wallpaper's rotation.
        val wallpaper = info.changes.find { (it.flags and TransitionInfo.FLAG_IS_WALLPAPER) != 0 }
        val wallpaperParent = wallpaper?.parent
        if (wallpaperParent != null && rotationDelta != 0) {
            val parent = info.getChange(wallpaperParent)
            if (parent != null) {
                wallpaperRotator.setup(
                    transaction,
                    parent.leash,
                    rotationDelta,
                    displayWidth,
                    displayHeight,
                )
                transaction.setLayer(wallpaperRotator.surface, -1)
                wallpaperRotator.addChild(transaction, wallpaper.leash)
            } else {
                Log.e(
                    TAG,
                    "Malformed: $wallpaper has parent=$wallpaperParent, which is not part of the " +
                        "transition info=$info.",
                )
            }
        }
    }

    override fun cleanUpAnimation(token: IBinder, transaction: SurfaceControl.Transaction) {
        launcherRotators.remove(token)?.cleanUp(transaction)
        wallpaperRotators.remove(token)?.cleanUp(transaction)
        transaction.apply()
    }

    /** Wrapper for information related to a change for the Launcher surface. */
    private class Launcher(
        val change: TransitionInfo.Change,
        val layer: Int,
        val isOpening: Boolean,
        parentToken: WindowContainerToken?,
        info: TransitionInfo,
    ) {
        val parent: TransitionInfo.Change? = parentToken?.let { info.getChange(parentToken) }
    }
}
+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)
}
+113 −88

File changed.

Preview size limit exceeded, changes collapsed.