Extract ActivityLaunchAnimator into a reusable class.

This CL removes all logic specific to notifications in
ActivityLaunchAnimator, so that it can be reused for other launch
animations.

I would suggest to look at ag/14057092 in parallel to make more sense of
the abstractions added in this CL.

For the sake of not making this CL even bigger than it already is, a few
things still need to be done in follow-up CLs:
 - Show the status bar icons at the right time, instead of at the end of
   the animation, when using the StatusBarLaunchAnimatorController.
 - Move the animation/ package outside of the SystemUIPluginLib library
   and instead have it in its own reusable library.
 - Replace the animation durations and interpolator to the latest
   designs.
 - Improve split screen by retrieving the final window bounds and
   prevent the clipping of the window during the animation.
 - Handle animations in the lock screen.

For review, I would recommend to review in order:
 1. ActivityLaunchAnimator.kt
 2. StatusBarLaunchAnimatorController.kt
 3. NotificationLaunchAnimatorController.kt
 4. GhostedViewLaunchAnimatorController.kt
 5. Everything else.

Bug: 184121838
Bug: 181654098
Test: Tap a notification when the shade is open and unlocked.
Change-Id: If4c3c64fcd153bb8e89111f56332013ca6dff156
diff --git a/packages/SystemUI/plugin/Android.bp b/packages/SystemUI/plugin/Android.bp
index d6204db..b3aba22 100644
--- a/packages/SystemUI/plugin/Android.bp
+++ b/packages/SystemUI/plugin/Android.bp
@@ -27,6 +27,7 @@
 
     srcs: [
         "src/**/*.java",
+        "src/**/*.kt",
         "bcsmartspace/src/**/*.java",
     ],
 
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java
index 25a3fa2..e003b2e 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java
@@ -19,6 +19,7 @@
 import android.content.Intent;
 import android.view.View;
 
+import com.android.systemui.plugins.animation.ActivityLaunchAnimator;
 import com.android.systemui.plugins.annotations.ProvidesInterface;
 
 /**
@@ -44,7 +45,15 @@
      * specifies an associated view that should be used for the activity launch animation.
      */
     void startPendingIntentDismissingKeyguard(PendingIntent intent,
-            Runnable intentSentUiThreadCallback, View associatedView);
+            Runnable intentSentUiThreadCallback, @Nullable View associatedView);
+
+    /**
+     * Similar to {@link #startPendingIntentDismissingKeyguard(PendingIntent, Runnable)}, but also
+     * specifies an animation controller that should be used for the activity launch animation.
+     */
+    void startPendingIntentDismissingKeyguard(PendingIntent intent,
+            Runnable intentSentUiThreadCallback,
+            @Nullable ActivityLaunchAnimator.Controller animationController);
 
     /**
      * The intent flag can be specified in startActivity().
@@ -55,6 +64,14 @@
     void startActivity(Intent intent, boolean dismissShade, Callback callback);
     void postStartActivityDismissingKeyguard(Intent intent, int delay);
     void postStartActivityDismissingKeyguard(PendingIntent intent);
+
+    /**
+     * Similar to {@link #postStartActivityDismissingKeyguard(PendingIntent)}, but also specifies an
+     * animation controller that should be used for the activity launch animation.
+     */
+    void postStartActivityDismissingKeyguard(PendingIntent intent,
+            @Nullable ActivityLaunchAnimator.Controller animationController);
+
     void postQSRunnableDismissingKeyguard(Runnable runnable);
 
     void dismissKeyguardThenExecute(OnDismissAction action, @Nullable Runnable cancel,
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/animation/ActivityLaunchAnimator.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/animation/ActivityLaunchAnimator.kt
new file mode 100644
index 0000000..e8bdb67
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/animation/ActivityLaunchAnimator.kt
@@ -0,0 +1,479 @@
+package com.android.systemui.plugins.animation
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.ValueAnimator
+import android.app.ActivityManager
+import android.app.PendingIntent
+import android.graphics.Matrix
+import android.graphics.Rect
+import android.os.RemoteException
+import android.util.MathUtils
+import android.view.IRemoteAnimationFinishedCallback
+import android.view.IRemoteAnimationRunner
+import android.view.RemoteAnimationAdapter
+import android.view.RemoteAnimationTarget
+import android.view.SyncRtSurfaceTransactionApplier
+import android.view.View
+import android.view.WindowManager
+import android.view.animation.LinearInterpolator
+import android.view.animation.PathInterpolator
+import com.android.internal.annotations.VisibleForTesting
+import com.android.internal.policy.ScreenDecorationsUtils
+import kotlin.math.roundToInt
+
+/**
+ * A class that allows activities to be started in a seamless way from a view that is transforming
+ * nicely into the starting window.
+ */
+class ActivityLaunchAnimator {
+    companion object {
+        const val ANIMATION_DURATION = 400L
+        const val ANIMATION_DURATION_FADE_OUT_CONTENT = 67L
+        const val ANIMATION_DURATION_FADE_IN_WINDOW = 200L
+        private const val ANIMATION_DURATION_NAV_FADE_IN = 266L
+        private const val ANIMATION_DURATION_NAV_FADE_OUT = 133L
+        private const val ANIMATION_DELAY_NAV_FADE_IN =
+                ANIMATION_DURATION - ANIMATION_DURATION_NAV_FADE_IN
+        private const val LAUNCH_TIMEOUT = 500L
+
+        // TODO(b/184121838): Use android.R.interpolator.fast_out_extra_slow_in instead.
+        // TODO(b/184121838): Move com.android.systemui.Interpolators in an animation library we can
+        // reuse here.
+        private val ANIMATION_INTERPOLATOR = PathInterpolator(0f, 0f, 0.2f, 1f)
+        private val LINEAR_INTERPOLATOR = LinearInterpolator()
+        private val ALPHA_IN_INTERPOLATOR = PathInterpolator(0.4f, 0f, 1f, 1f)
+        private val ALPHA_OUT_INTERPOLATOR = PathInterpolator(0f, 0f, 0.8f, 1f)
+        private val NAV_FADE_IN_INTERPOLATOR = PathInterpolator(0f, 0f, 0f, 1f)
+        private val NAV_FADE_OUT_INTERPOLATOR = PathInterpolator(0.2f, 0f, 1f, 1f)
+
+        /**
+         * Given the [linearProgress] of a launch animation, return the linear progress of the
+         * sub-animation starting [delay] ms after the launch animation and that lasts [duration].
+         */
+        @JvmStatic
+        fun getProgress(linearProgress: Float, delay: Long, duration: Long): Float {
+            return MathUtils.constrain(
+                    (linearProgress * ANIMATION_DURATION - delay) / duration,
+                    0.0f,
+                    1.0f
+            )
+        }
+    }
+
+    /**
+     * Start an intent and animate the opening window. The intent will be started by running
+     * [intentStarter], which should use the provided [RemoteAnimationAdapter] and return the launch
+     * result. [controller] is responsible from animating the view from which the intent was started
+     * in [Controller.onLaunchAnimationProgress]. No animation will start if there is no window
+     * opening.
+     *
+     * If [controller] is null, then the intent will be started and no animation will run.
+     *
+     * This method will throw any exception thrown by [intentStarter].
+     */
+    inline fun startIntentWithAnimation(
+        controller: Controller?,
+        intentStarter: (RemoteAnimationAdapter?) -> Int
+    ) {
+        if (controller == null) {
+            intentStarter(null)
+            return
+        }
+
+        val runner = Runner(controller)
+        val animationAdapter = RemoteAnimationAdapter(
+            runner,
+            ANIMATION_DURATION,
+            ANIMATION_DURATION - 150 /* statusBarTransitionDelay */
+        )
+        val launchResult = intentStarter(animationAdapter)
+        val willAnimate = launchResult == ActivityManager.START_TASK_TO_FRONT ||
+            launchResult == ActivityManager.START_SUCCESS
+        runner.context.mainExecutor.execute { controller.onIntentStarted(willAnimate) }
+
+        // If we expect an animation, post a timeout to cancel it in case the remote animation is
+        // never started.
+        if (willAnimate) {
+            runner.postTimeout()
+        }
+    }
+
+    /**
+     * Same as [startIntentWithAnimation] but allows [intentStarter] to throw a
+     * [PendingIntent.CanceledException] which must then be handled by the caller. This is useful
+     * for Java caller starting a [PendingIntent].
+     */
+    @Throws(PendingIntent.CanceledException::class)
+    fun startPendingIntentWithAnimation(
+        controller: Controller?,
+        intentStarter: PendingIntentStarter
+    ) {
+        startIntentWithAnimation(controller) { intentStarter.startPendingIntent(it) }
+    }
+
+    interface PendingIntentStarter {
+        /**
+         * Start a pending intent using the provided [animationAdapter] and return the launch
+         * result.
+         */
+        @Throws(PendingIntent.CanceledException::class)
+        fun startPendingIntent(animationAdapter: RemoteAnimationAdapter?): Int
+    }
+
+    /**
+     * A controller that takes care of applying the animation to an expanding view.
+     *
+     * Note that all callbacks (onXXX methods) are all called on the main thread.
+     */
+    interface Controller {
+        companion object {
+            /**
+             * Return a [Controller] that will animate and expand [view] into the opening window.
+             *
+             * Important: The view must be attached to the window when calling this function and
+             * during the animation.
+             */
+            @JvmStatic
+            fun fromView(view: View): Controller = GhostedViewLaunchAnimatorController(view)
+        }
+
+        /**
+         * Return the root [View] that contains the view that started the intent and will be
+         * animating together with the window.
+         *
+         * This view will be used to:
+         *  - Get the associated [Context].
+         *  - Compute whether we are expanding fully above the current window.
+         *  - Apply surface transactions in sync with RenderThread.
+         */
+        fun getRootView(): View
+
+        /**
+         * Return the [State] of the view that will be animated. We will animate from this state to
+         * the final window state.
+         *
+         * Note: This state will be mutated and passed to [onLaunchAnimationProgress] during the
+         * animation.
+         */
+        fun createAnimatorState(): State
+
+        /**
+         * The intent was started. If [willAnimate] is false, nothing else will happen and the
+         * animation will not be started.
+         */
+        fun onIntentStarted(willAnimate: Boolean) {}
+
+        /**
+         * The animation started. This is typically used to initialize any additional resource
+         * needed for the animation. [isExpandingFullyAbove] will be true if the window is expanding
+         * fully above the [root view][getRootView].
+         */
+        fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {}
+
+        /** The animation made progress and the expandable view [state] should be updated. */
+        fun onLaunchAnimationProgress(state: State, progress: Float, linearProgress: Float) {}
+
+        /**
+         * The animation ended. This will be called *if and only if* [onLaunchAnimationStart] was
+         * called previously. This is typically used to clean up the resources initialized when the
+         * animation was started.
+         */
+        fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {}
+
+        /**
+         * The animation was cancelled remotely. Note that [onLaunchAnimationEnd] will still be
+         * called after this if the animation was already started, i.e. if [onLaunchAnimationStart]
+         * was called before the cancellation.
+         */
+        fun onLaunchAnimationCancelled() {}
+
+        /**
+         * The remote animation was not started within the expected time. It timed out and will
+         * never [start][onLaunchAnimationStart].
+         */
+        fun onLaunchAnimationTimedOut() {}
+
+        /**
+         * The animation was aborted because the opening window was not found. It will never
+         * [start][onLaunchAnimationStart].
+         */
+        fun onLaunchAnimationAborted() {}
+    }
+
+    /** The state of an expandable view during an [ActivityLaunchAnimator] animation. */
+    open class State(
+        /** The position of the view in screen space coordinates. */
+        var top: Int,
+        var bottom: Int,
+        var left: Int,
+        var right: Int,
+
+        var topCornerRadius: Float = 0f,
+        var bottomCornerRadius: Float = 0f,
+
+        var contentAlpha: Float = 1f,
+        var backgroundAlpha: Float = 1f
+    ) {
+        private val startTop = top
+        private val startLeft = left
+        private val startRight = right
+
+        val width: Int
+            get() = right - left
+
+        val height: Int
+            get() = bottom - top
+
+        open val topChange: Int
+            get() = top - startTop
+
+        val leftChange: Int
+            get() = left - startLeft
+
+        val rightChange: Int
+            get() = right - startRight
+    }
+
+    @VisibleForTesting
+    class Runner(private val controller: Controller) : IRemoteAnimationRunner.Stub() {
+        private val rootView = controller.getRootView()
+        @PublishedApi internal val context = rootView.context
+        private val transactionApplier = SyncRtSurfaceTransactionApplier(rootView)
+        private var animator: ValueAnimator? = null
+
+        private var windowCrop = Rect()
+        private var timedOut = false
+        private var cancelled = false
+
+        // A timeout to cancel the remote animation if it is not started within X milliseconds after
+        // the intent was started.
+        //
+        // Note that this is important to keep this a Runnable (and not a Kotlin lambda), otherwise
+        // it will be automatically converted when posted and we wouldn't be able to remove it after
+        // posting it.
+        private var onTimeout = Runnable { onAnimationTimedOut() }
+
+        @PublishedApi
+        internal fun postTimeout() {
+            rootView.postDelayed(onTimeout, LAUNCH_TIMEOUT)
+        }
+
+        private fun removeTimeout() {
+            rootView.removeCallbacks(onTimeout)
+        }
+
+        override fun onAnimationStart(
+            @WindowManager.TransitionOldType transit: Int,
+            remoteAnimationTargets: Array<out RemoteAnimationTarget>,
+            remoteAnimationWallpaperTargets: Array<out RemoteAnimationTarget>,
+            remoteAnimationNonAppTargets: Array<out RemoteAnimationTarget>,
+            iRemoteAnimationFinishedCallback: IRemoteAnimationFinishedCallback
+        ) {
+            removeTimeout()
+
+            // The animation was started too late and we already notified the controller that it
+            // timed out.
+            if (timedOut) {
+                invokeCallback(iRemoteAnimationFinishedCallback)
+                return
+            }
+
+            // This should not happen, but let's make sure we don't start the animation if it was
+            // cancelled before and we already notified the controller.
+            if (cancelled) {
+                return
+            }
+
+            context.mainExecutor.execute {
+                startAnimation(remoteAnimationTargets, iRemoteAnimationFinishedCallback)
+            }
+        }
+
+        private fun startAnimation(
+            remoteAnimationTargets: Array<out RemoteAnimationTarget>,
+            iCallback: IRemoteAnimationFinishedCallback
+        ) {
+            val window = remoteAnimationTargets.firstOrNull {
+                it.mode == RemoteAnimationTarget.MODE_OPENING
+            }
+
+            if (window == null) {
+                removeTimeout()
+                invokeCallback(iCallback)
+                controller.onLaunchAnimationAborted()
+                return
+            }
+
+            val navigationBar = remoteAnimationTargets.firstOrNull {
+                it.windowType == WindowManager.LayoutParams.TYPE_NAVIGATION_BAR
+            }
+
+            // Start state.
+            val state = controller.createAnimatorState()
+
+            val startTop = state.top
+            val startBottom = state.bottom
+            val startLeft = state.left
+            val startRight = state.right
+
+            val startTopCornerRadius = state.topCornerRadius
+            val startBottomCornerRadius = state.bottomCornerRadius
+
+            // End state.
+            val windowBounds = window.screenSpaceBounds
+            val endTop = windowBounds.top
+            val endBottom = windowBounds.bottom
+            val endLeft = windowBounds.left
+            val endRight = windowBounds.right
+
+            // TODO(b/184121838): Ensure that we are launching on the same screen.
+            val rootViewLocation = rootView.locationOnScreen
+            val isExpandingFullyAbove = endTop <= rootViewLocation[1] &&
+                endBottom >= rootViewLocation[1] + rootView.height &&
+                endLeft <= rootViewLocation[0] &&
+                endRight >= rootViewLocation[0] + rootView.width
+
+            // TODO(b/184121838): We should somehow get the top and bottom radius of the window.
+            val endRadius = if (isExpandingFullyAbove) {
+                // Most of the time, expanding fully above the root view means expanding in full
+                // screen.
+                ScreenDecorationsUtils.getWindowCornerRadius(context.resources)
+            } else {
+                // This usually means we are in split screen mode, so 2 out of 4 corners will have
+                // a radius of 0.
+                0f
+            }
+
+            // Update state.
+            val animator = ValueAnimator.ofFloat(0f, 1f)
+            this.animator = animator
+            animator.duration = ANIMATION_DURATION
+            animator.interpolator = LINEAR_INTERPOLATOR
+
+            animator.addListener(object : AnimatorListenerAdapter() {
+                override fun onAnimationStart(animation: Animator?, isReverse: Boolean) {
+                    controller.onLaunchAnimationStart(isExpandingFullyAbove)
+                }
+
+                override fun onAnimationEnd(animation: Animator?) {
+                    invokeCallback(iCallback)
+                    controller.onLaunchAnimationEnd(isExpandingFullyAbove)
+                }
+            })
+
+            animator.addUpdateListener { animation ->
+                if (cancelled) {
+                    return@addUpdateListener
+                }
+
+                val linearProgress = animation.animatedFraction
+                val progress = ANIMATION_INTERPOLATOR.getInterpolation(linearProgress)
+
+                state.top = lerp(startTop, endTop, progress).roundToInt()
+                state.bottom = lerp(startBottom, endBottom, progress).roundToInt()
+                state.left = lerp(startLeft, endLeft, progress).roundToInt()
+                state.right = lerp(startRight, endRight, progress).roundToInt()
+
+                state.topCornerRadius = MathUtils.lerp(startTopCornerRadius, endRadius, progress)
+                state.bottomCornerRadius =
+                    MathUtils.lerp(startBottomCornerRadius, endRadius, progress)
+
+                val contentAlphaProgress = getProgress(linearProgress, 0,
+                        ANIMATION_DURATION_FADE_OUT_CONTENT)
+                state.contentAlpha =
+                        1 - ALPHA_OUT_INTERPOLATOR.getInterpolation(contentAlphaProgress)
+
+                val backgroundAlphaProgress = getProgress(linearProgress,
+                        ANIMATION_DURATION_FADE_OUT_CONTENT, ANIMATION_DURATION_FADE_IN_WINDOW)
+                state.backgroundAlpha =
+                        1 - ALPHA_IN_INTERPOLATOR.getInterpolation(backgroundAlphaProgress)
+
+                applyStateToWindow(window, state)
+                navigationBar?.let { applyStateToNavigationBar(it, state, linearProgress) }
+                controller.onLaunchAnimationProgress(state, progress, linearProgress)
+            }
+
+            animator.start()
+        }
+
+        private fun applyStateToWindow(window: RemoteAnimationTarget, state: State) {
+            val m = Matrix()
+            m.postTranslate(0f, (state.top - window.sourceContainerBounds.top).toFloat())
+            windowCrop.set(state.left, 0, state.right, state.height)
+
+            val cornerRadius = minOf(state.topCornerRadius, state.bottomCornerRadius)
+            val params = SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(window.leash)
+                    .withAlpha(1f)
+                    .withMatrix(m)
+                    .withWindowCrop(windowCrop)
+                    .withLayer(window.prefixOrderIndex)
+                    .withCornerRadius(cornerRadius)
+                    .withVisibility(true)
+                    .build()
+
+            transactionApplier.scheduleApply(params)
+        }
+
+        private fun applyStateToNavigationBar(
+            navigationBar: RemoteAnimationTarget,
+            state: State,
+            linearProgress: Float
+        ) {
+            val fadeInProgress = getProgress(linearProgress, ANIMATION_DELAY_NAV_FADE_IN,
+                    ANIMATION_DURATION_NAV_FADE_OUT)
+
+            val params = SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(navigationBar.leash)
+            if (fadeInProgress > 0) {
+                val m = Matrix()
+                m.postTranslate(0f, (state.top - navigationBar.sourceContainerBounds.top).toFloat())
+                windowCrop.set(state.left, 0, state.right, state.height)
+                params
+                        .withAlpha(NAV_FADE_IN_INTERPOLATOR.getInterpolation(fadeInProgress))
+                        .withMatrix(m)
+                        .withWindowCrop(windowCrop)
+                        .withVisibility(true)
+            } else {
+                val fadeOutProgress = getProgress(linearProgress, 0,
+                        ANIMATION_DURATION_NAV_FADE_OUT)
+                params.withAlpha(1f - NAV_FADE_OUT_INTERPOLATOR.getInterpolation(fadeOutProgress))
+            }
+
+            transactionApplier.scheduleApply(params.build())
+        }
+
+        private fun onAnimationTimedOut() {
+            if (cancelled) {
+                return
+            }
+
+            timedOut = true
+            controller.onLaunchAnimationTimedOut()
+        }
+
+        override fun onAnimationCancelled() {
+            if (timedOut) {
+                return
+            }
+
+            cancelled = true
+            removeTimeout()
+            context.mainExecutor.execute {
+                animator?.cancel()
+                controller.onLaunchAnimationCancelled()
+            }
+        }
+
+        private fun invokeCallback(iCallback: IRemoteAnimationFinishedCallback) {
+            try {
+                iCallback.onAnimationFinished()
+            } catch (e: RemoteException) {
+                e.printStackTrace()
+            }
+        }
+
+        private fun lerp(start: Int, stop: Int, amount: Float): Float {
+            return MathUtils.lerp(start.toFloat(), stop.toFloat(), amount)
+        }
+    }
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/animation/GhostedViewLaunchAnimatorController.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/animation/GhostedViewLaunchAnimatorController.kt
new file mode 100644
index 0000000..a237224
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/animation/GhostedViewLaunchAnimatorController.kt
@@ -0,0 +1,196 @@
+package com.android.systemui.plugins.animation
+
+import android.graphics.Canvas
+import android.graphics.ColorFilter
+import android.graphics.PixelFormat
+import android.graphics.PorterDuff
+import android.graphics.PorterDuffXfermode
+import android.graphics.Rect
+import android.graphics.drawable.Drawable
+import android.view.GhostView
+import android.view.View
+import android.view.ViewGroup
+import android.widget.FrameLayout
+
+/**
+ * A base implementation of [ActivityLaunchAnimator.Controller] which creates a [ghost][GhostView]
+ * of [ghostedView] as well as an expandable background view, which are drawn and animated instead
+ * of the ghosted view.
+ *
+ * Important: [ghostedView] must be attached to the window when calling this function and during the
+ * animation.
+ *
+ * Note: Avoid instantiating this directly and call [ActivityLaunchAnimator.Controller.fromView]
+ * whenever possible instead.
+ */
+open class GhostedViewLaunchAnimatorController(
+    /** The view that will be ghosted and from which the background will be extracted. */
+    private val ghostedView: View
+) : ActivityLaunchAnimator.Controller {
+    /** The root view to which we will add the ghost view and expanding background. */
+    private val rootView = ghostedView.rootView as ViewGroup
+    private val rootViewOverlay = rootView.overlay
+
+    /** The ghost view that is drawn and animated instead of the ghosted view. */
+    private var ghostView: View? = null
+
+    /**
+     * The expanding background view that will be added to [rootView] (below [ghostView]) and
+     * animate.
+     */
+    private var backgroundView: FrameLayout? = null
+
+    /**
+     * The drawable wrapping the [ghostedView] background and used as background for
+     * [backgroundView].
+     */
+    private var backgroundDrawable: WrappedDrawable? = null
+    private var startBackgroundAlpha: Int = 0xFF
+
+    /**
+     * Return the background of the [ghostedView]. This background will be used to draw the
+     * background of the background view that is expanding up to the final animation position. This
+     * is called at the start of the animation.
+     *
+     * Note that during the animation, the alpha value value of this background will be set to 0,
+     * then set back to its initial value at the end of the animation.
+     */
+    protected open fun getBackground(): Drawable? = ghostedView.background
+
+    /**
+     * Set the corner radius of [background]. The background is the one that was returned by
+     * [getBackground].
+     */
+    protected open fun setBackgroundCornerRadius(
+        background: Drawable,
+        topCornerRadius: Float,
+        bottomCornerRadius: Float
+    ) {
+        // TODO(b/184121838): Add default support for GradientDrawable and LayerDrawable to make
+        // this work out of the box for common rounded backgrounds.
+    }
+
+    /** Return the current top corner radius of the background. */
+    protected open fun getCurrentTopCornerRadius(): Float = 0f
+
+    /** Return the current bottom corner radius of the background. */
+    protected open fun getCurrentBottomCornerRadius(): Float = 0f
+
+    override fun getRootView(): View {
+        return rootView
+    }
+
+    override fun createAnimatorState(): ActivityLaunchAnimator.State {
+        val location = ghostedView.locationOnScreen
+        return ActivityLaunchAnimator.State(
+            top = location[1],
+            bottom = location[1] + ghostedView.height,
+            left = location[0],
+            right = location[0] + ghostedView.width,
+            topCornerRadius = getCurrentTopCornerRadius(),
+            bottomCornerRadius = getCurrentBottomCornerRadius()
+        )
+    }
+
+    override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
+        backgroundView = FrameLayout(rootView.context).apply {
+            forceHasOverlappingRendering(true)
+        }
+        rootViewOverlay.add(backgroundView)
+
+        // We wrap the ghosted view background and use it to draw the expandable background. Its
+        // alpha will be set to 0 as soon as we start drawing the expanding background.
+        val drawable = getBackground()
+        startBackgroundAlpha = drawable?.alpha ?: 0xFF
+        backgroundDrawable = WrappedDrawable(drawable)
+        backgroundView?.background = backgroundDrawable
+
+        // Create a ghost of the view that will be moving and fading out. This allows to fade out
+        // the content before fading out the background.
+        ghostView = GhostView.addGhost(ghostedView, rootView).apply {
+            setLayerType(View.LAYER_TYPE_HARDWARE, null)
+        }
+    }
+
+    override fun onLaunchAnimationProgress(
+        state: ActivityLaunchAnimator.State,
+        progress: Float,
+        linearProgress: Float
+    ) {
+        val ghostView = this.ghostView!!
+        ghostView.translationX = (state.leftChange + state.rightChange) / 2.toFloat()
+        ghostView.translationY = state.topChange.toFloat()
+        ghostView.alpha = state.contentAlpha
+
+        val backgroundView = this.backgroundView!!
+        backgroundView.top = state.top
+        backgroundView.bottom = state.bottom
+        backgroundView.left = state.left
+        backgroundView.right = state.right
+
+        val backgroundDrawable = backgroundDrawable!!
+        backgroundDrawable.alpha = (0xFF * state.backgroundAlpha).toInt()
+        backgroundDrawable.wrapped?.let {
+            setBackgroundCornerRadius(it, state.topCornerRadius, state.bottomCornerRadius)
+        }
+    }
+
+    override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
+        backgroundDrawable?.wrapped?.alpha = startBackgroundAlpha
+
+        GhostView.removeGhost(ghostedView)
+        rootViewOverlay.remove(backgroundView)
+        ghostedView.invalidate()
+    }
+
+    private class WrappedDrawable(val wrapped: Drawable?) : Drawable() {
+        companion object {
+            private val SRC_MODE = PorterDuffXfermode(PorterDuff.Mode.SRC)
+        }
+
+        private var currentAlpha = 0xFF
+        private var previousBounds = Rect()
+
+        override fun draw(canvas: Canvas) {
+            val wrapped = this.wrapped ?: return
+
+            wrapped.copyBounds(previousBounds)
+
+            wrapped.alpha = currentAlpha
+            wrapped.bounds = bounds
+            wrapped.setXfermode(SRC_MODE)
+
+            wrapped.draw(canvas)
+
+            // The background view (and therefore this drawable) is drawn before the ghost view, so
+            // the ghosted view background alpha should always be 0 when it is drawn above the
+            // background.
+            wrapped.alpha = 0
+            wrapped.bounds = previousBounds
+            wrapped.setXfermode(null)
+        }
+
+        override fun setAlpha(alpha: Int) {
+            if (alpha != currentAlpha) {
+                currentAlpha = alpha
+                invalidateSelf()
+            }
+        }
+
+        override fun getAlpha() = currentAlpha
+
+        override fun getOpacity(): Int {
+            val wrapped = this.wrapped ?: return PixelFormat.TRANSPARENT
+
+            val previousAlpha = wrapped.alpha
+            wrapped.alpha = currentAlpha
+            val opacity = wrapped.opacity
+            wrapped.alpha = previousAlpha
+            return opacity
+        }
+
+        override fun setColorFilter(filter: ColorFilter?) {
+            wrapped?.colorFilter = filter
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/ActivityStarterDelegate.java b/packages/SystemUI/src/com/android/systemui/ActivityStarterDelegate.java
index 3d6d381..ecbb70e 100644
--- a/packages/SystemUI/src/com/android/systemui/ActivityStarterDelegate.java
+++ b/packages/SystemUI/src/com/android/systemui/ActivityStarterDelegate.java
@@ -20,6 +20,7 @@
 
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.plugins.animation.ActivityLaunchAnimator;
 import com.android.systemui.statusbar.phone.StatusBar;
 
 import java.util.Optional;
@@ -51,18 +52,27 @@
 
     @Override
     public void startPendingIntentDismissingKeyguard(PendingIntent intent,
-            Runnable intentSentCallback) {
+            Runnable intentSentUiThreadCallback) {
         mActualStarter.ifPresent(
                 starter -> starter.get().startPendingIntentDismissingKeyguard(intent,
-                        intentSentCallback));
+                        intentSentUiThreadCallback));
     }
 
     @Override
     public void startPendingIntentDismissingKeyguard(PendingIntent intent,
-            Runnable intentSentCallback, View associatedView) {
+            Runnable intentSentUiThreadCallback, View associatedView) {
         mActualStarter.ifPresent(
                 starter -> starter.get().startPendingIntentDismissingKeyguard(intent,
-                        intentSentCallback, associatedView));
+                        intentSentUiThreadCallback, associatedView));
+    }
+
+    @Override
+    public void startPendingIntentDismissingKeyguard(PendingIntent intent,
+            Runnable intentSentUiThreadCallback,
+            ActivityLaunchAnimator.Controller animationController) {
+        mActualStarter.ifPresent(
+                starter -> starter.get().startPendingIntentDismissingKeyguard(intent,
+                        intentSentUiThreadCallback, animationController));
     }
 
     @Override
@@ -103,6 +113,13 @@
     }
 
     @Override
+    public void postStartActivityDismissingKeyguard(PendingIntent intent,
+            ActivityLaunchAnimator.Controller animationController) {
+        mActualStarter.ifPresent(starter ->
+                starter.get().postStartActivityDismissingKeyguard(intent, animationController));
+    }
+
+    @Override
     public void postQSRunnableDismissingKeyguard(Runnable runnable) {
         mActualStarter.ifPresent(
                 starter -> starter.get().postQSRunnableDismissingKeyguard(runnable));
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CrossFadeHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/CrossFadeHelper.java
index 8e6398f..9525975 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/CrossFadeHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/CrossFadeHelper.java
@@ -105,9 +105,7 @@
             }
         } else if (view.getLayerType() == View.LAYER_TYPE_HARDWARE
                 && view.getTag(R.id.cross_fade_layer_type_changed_tag) != null) {
-            if (view.getTag(R.id.cross_fade_layer_type_changed_tag) != null) {
-                view.setLayerType(View.LAYER_TYPE_NONE, null);
-            }
+            view.setLayerType(View.LAYER_TYPE_NONE, null);
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt
index e27c1a2..a0a4e31 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt
@@ -35,7 +35,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.plugins.statusbar.StatusBarStateController
-import com.android.systemui.statusbar.notification.ActivityLaunchAnimator
+import com.android.systemui.statusbar.notification.ExpandAnimationParameters
 import com.android.systemui.statusbar.phone.BiometricUnlockController
 import com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK
 import com.android.systemui.statusbar.phone.DozeParameters
@@ -111,7 +111,7 @@
      * When launching an app from the shade, the animations progress should affect how blurry the
      * shade is, overriding the expansion amount.
      */
-    var notificationLaunchAnimationParams: ActivityLaunchAnimator.ExpandAnimationParameters? = null
+    var notificationLaunchAnimationParams: ExpandAnimationParameters? = null
         set(value) {
             field = value
             if (value != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java
index 24515f7..737167e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java
@@ -90,9 +90,14 @@
     /** Sets the state of whether the user activities are forced or not. */
     default void setForceUserActivity(boolean forceUserActivity) {}
 
-    /** Sets the state of whether the user activities are forced or not. */
+    /** Sets the state of whether an activity is launching or not. */
     default void setLaunchingActivity(boolean launching) {}
 
+    /** Get whether an activity is launching or not. */
+    default boolean isLaunchingActivity() {
+        return false;
+    }
+
     /** Sets the state of whether the scrim is visible or not. */
     default void setScrimsVisibility(int scrimsVisibility) {}
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ActivityLaunchAnimator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ActivityLaunchAnimator.java
deleted file mode 100644
index 23d13d3..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ActivityLaunchAnimator.java
+++ /dev/null
@@ -1,490 +0,0 @@
-/*
- * Copyright (C) 2018 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.statusbar.notification;
-
-import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR;
-
-import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_APP_START;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ValueAnimator;
-import android.app.ActivityManager;
-import android.graphics.Matrix;
-import android.graphics.Rect;
-import android.os.RemoteException;
-import android.util.MathUtils;
-import android.view.IRemoteAnimationFinishedCallback;
-import android.view.IRemoteAnimationRunner;
-import android.view.RemoteAnimationAdapter;
-import android.view.RemoteAnimationTarget;
-import android.view.SyncRtSurfaceTransactionApplier;
-import android.view.SyncRtSurfaceTransactionApplier.SurfaceParams;
-import android.view.View;
-import android.view.WindowManager;
-import android.view.animation.Interpolator;
-import android.view.animation.PathInterpolator;
-
-import com.android.internal.jank.InteractionJankMonitor;
-import com.android.internal.policy.ScreenDecorationsUtils;
-import com.android.systemui.Interpolators;
-import com.android.systemui.statusbar.NotificationShadeDepthController;
-import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
-import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
-import com.android.systemui.statusbar.phone.CollapsedStatusBarFragment;
-import com.android.systemui.statusbar.phone.NotificationPanelViewController;
-import com.android.systemui.statusbar.phone.NotificationShadeWindowViewController;
-
-import java.util.concurrent.Executor;
-
-/**
- * A class that allows activities to be launched in a seamless way where the notification
- * transforms nicely into the starting window.
- */
-public class ActivityLaunchAnimator {
-
-    private static final int ANIMATION_DURATION = 400;
-    public static final long ANIMATION_DURATION_FADE_CONTENT = 67;
-    public static final long ANIMATION_DURATION_FADE_APP = 200;
-    public static final long ANIMATION_DELAY_ICON_FADE_IN = ANIMATION_DURATION -
-            CollapsedStatusBarFragment.FADE_IN_DURATION - CollapsedStatusBarFragment.FADE_IN_DELAY
-            - 16;
-    private static final int ANIMATION_DURATION_NAV_FADE_IN = 266;
-    private static final int ANIMATION_DURATION_NAV_FADE_OUT = 133;
-    private static final long ANIMATION_DELAY_NAV_FADE_IN =
-            ANIMATION_DURATION - ANIMATION_DURATION_NAV_FADE_IN;
-    private static final Interpolator NAV_FADE_IN_INTERPOLATOR =
-            new PathInterpolator(0f, 0f, 0f, 1f);
-    private static final Interpolator NAV_FADE_OUT_INTERPOLATOR =
-            new PathInterpolator(0.2f, 0f, 1f, 1f);
-    private static final long LAUNCH_TIMEOUT = 500;
-    private final NotificationPanelViewController mNotificationPanel;
-    private final NotificationListContainer mNotificationContainer;
-    private final float mWindowCornerRadius;
-    private final NotificationShadeWindowViewController mNotificationShadeWindowViewController;
-    private final NotificationShadeDepthController mDepthController;
-    private final Executor mMainExecutor;
-    private Callback mCallback;
-    private final Runnable mTimeoutRunnable = () -> {
-        setAnimationPending(false);
-        mCallback.onExpandAnimationTimedOut();
-    };
-    private boolean mAnimationPending;
-    private boolean mAnimationRunning;
-    private boolean mIsLaunchForActivity;
-
-    public ActivityLaunchAnimator(
-            NotificationShadeWindowViewController notificationShadeWindowViewController,
-            Callback callback,
-            NotificationPanelViewController notificationPanel,
-            NotificationShadeDepthController depthController,
-            NotificationListContainer container,
-            Executor mainExecutor) {
-        mNotificationPanel = notificationPanel;
-        mNotificationContainer = container;
-        mDepthController = depthController;
-        mNotificationShadeWindowViewController = notificationShadeWindowViewController;
-        mCallback = callback;
-        mMainExecutor = mainExecutor;
-        mWindowCornerRadius = ScreenDecorationsUtils
-                .getWindowCornerRadius(mNotificationShadeWindowViewController.getView()
-                        .getResources());
-    }
-
-    public RemoteAnimationAdapter getLaunchAnimation(
-            View sourceView, boolean occluded) {
-        if (!(sourceView instanceof ExpandableNotificationRow)
-                || !mCallback.areLaunchAnimationsEnabled() || occluded) {
-            return null;
-        }
-        AnimationRunner animationRunner = new AnimationRunner(
-                (ExpandableNotificationRow) sourceView);
-        return new RemoteAnimationAdapter(animationRunner, ANIMATION_DURATION,
-                ANIMATION_DURATION - 150 /* statusBarTransitionDelay */);
-    }
-
-    public boolean isAnimationPending() {
-        return mAnimationPending;
-    }
-
-    /**
-     * Set the launch result the intent requested
-     *
-     * @param launchResult the launch result
-     * @param wasIntentActivity was this launch for an activity
-     */
-    public void setLaunchResult(int launchResult, boolean wasIntentActivity) {
-        mIsLaunchForActivity = wasIntentActivity;
-        setAnimationPending((launchResult == ActivityManager.START_TASK_TO_FRONT
-                || launchResult == ActivityManager.START_SUCCESS)
-                        && mCallback.areLaunchAnimationsEnabled());
-    }
-
-    public boolean isLaunchForActivity() {
-        return mIsLaunchForActivity;
-    }
-
-    private void setAnimationPending(boolean pending) {
-        mAnimationPending = pending;
-        mNotificationShadeWindowViewController.setExpandAnimationPending(pending);
-        if (pending) {
-            mNotificationShadeWindowViewController.getView().postDelayed(mTimeoutRunnable,
-                    LAUNCH_TIMEOUT);
-        } else {
-            mNotificationShadeWindowViewController.getView().removeCallbacks(mTimeoutRunnable);
-        }
-    }
-
-    public boolean isAnimationRunning() {
-        return mAnimationRunning;
-    }
-
-    class AnimationRunner extends IRemoteAnimationRunner.Stub {
-
-        private final ExpandableNotificationRow mSourceNotification;
-        private final ExpandAnimationParameters mParams;
-        private final Rect mWindowCrop = new Rect();
-        private boolean mIsFullScreenLaunch = true;
-        private final SyncRtSurfaceTransactionApplier mSyncRtTransactionApplier;
-
-        private final float mNotificationStartTopCornerRadius;
-        private final float mNotificationStartBottomCornerRadius;
-
-        AnimationRunner(ExpandableNotificationRow sourceNotification) {
-            mSourceNotification = sourceNotification;
-            mParams = new ExpandAnimationParameters();
-            mSyncRtTransactionApplier = new SyncRtSurfaceTransactionApplier(mSourceNotification);
-            mNotificationStartTopCornerRadius = mSourceNotification.getCurrentBackgroundRadiusTop();
-            mNotificationStartBottomCornerRadius =
-                    mSourceNotification.getCurrentBackgroundRadiusBottom();
-        }
-
-        @Override
-        public void onAnimationStart(@WindowManager.TransitionOldType int transit,
-                RemoteAnimationTarget[] remoteAnimationTargets,
-                RemoteAnimationTarget[] remoteAnimationWallpaperTargets,
-                RemoteAnimationTarget[] remoteAnimationNonAppTargets,
-                IRemoteAnimationFinishedCallback iRemoteAnimationFinishedCallback)
-                    throws RemoteException {
-            mMainExecutor.execute(() -> {
-                RemoteAnimationTarget primary = getPrimaryRemoteAnimationTarget(
-                        remoteAnimationTargets);
-                if (primary == null) {
-                    setAnimationPending(false);
-                    invokeCallback(iRemoteAnimationFinishedCallback);
-                    mNotificationPanel.collapse(false /* delayed */, 1.0f /* speedUpFactor */);
-                    return;
-                }
-
-                setExpandAnimationRunning(true);
-                mIsFullScreenLaunch = primary.position.y == 0
-                        && primary.sourceContainerBounds.height()
-                                >= mNotificationPanel.getHeight();
-                if (!mIsFullScreenLaunch) {
-                    mNotificationPanel.collapseWithDuration(ANIMATION_DURATION);
-                }
-                ValueAnimator anim = ValueAnimator.ofFloat(0, 1);
-                mParams.startPosition = mSourceNotification.getLocationOnScreen();
-                mParams.startTranslationZ = mSourceNotification.getTranslationZ();
-                mParams.startClipTopAmount = mSourceNotification.getClipTopAmount();
-                if (mSourceNotification.isChildInGroup()) {
-                    int parentClip = mSourceNotification
-                            .getNotificationParent().getClipTopAmount();
-                    mParams.parentStartClipTopAmount = parentClip;
-                    // We need to calculate how much the child is clipped by the parent
-                    // because children always have 0 clipTopAmount
-                    if (parentClip != 0) {
-                        float childClip = parentClip
-                                - mSourceNotification.getTranslationY();
-                        if (childClip > 0.0f) {
-                            mParams.startClipTopAmount = (int) Math.ceil(childClip);
-                        }
-                    }
-                }
-                int targetWidth = primary.sourceContainerBounds.width();
-                // If the notification panel is collapsed, the clip may be larger than the height.
-                int notificationHeight = Math.max(mSourceNotification.getActualHeight()
-                        - mSourceNotification.getClipBottomAmount(), 0);
-                int notificationWidth = mSourceNotification.getWidth();
-                final RemoteAnimationTarget navigationBarTarget =
-                        getNavBarRemoteAnimationTarget(remoteAnimationNonAppTargets);
-                anim.setDuration(ANIMATION_DURATION);
-                anim.setInterpolator(Interpolators.LINEAR);
-                anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
-                    @Override
-                    public void onAnimationUpdate(ValueAnimator animation) {
-                        mParams.linearProgress = animation.getAnimatedFraction();
-                        float progress = Interpolators.FAST_OUT_SLOW_IN.getInterpolation(
-                                        mParams.linearProgress);
-                        int newWidth = (int) MathUtils.lerp(notificationWidth,
-                                targetWidth, progress);
-                        mParams.left = (int) ((targetWidth - newWidth) / 2.0f);
-                        mParams.right = mParams.left + newWidth;
-                        mParams.top = (int) MathUtils.lerp(mParams.startPosition[1],
-                                primary.position.y, progress);
-                        mParams.bottom = (int) MathUtils.lerp(mParams.startPosition[1]
-                                        + notificationHeight,
-                                primary.position.y + primary.sourceContainerBounds.bottom,
-                                progress);
-                        mParams.topCornerRadius = MathUtils.lerp(mNotificationStartTopCornerRadius,
-                                mWindowCornerRadius, progress);
-                        mParams.bottomCornerRadius = MathUtils.lerp(
-                                mNotificationStartBottomCornerRadius,
-                                mWindowCornerRadius, progress);
-                        applyParamsToWindow(primary);
-                        applyParamsToNotification(mParams);
-                        applyParamsToNotificationShade(mParams);
-                        applyNavigationBarParamsToWindow(navigationBarTarget);
-                    }
-                });
-                anim.addListener(new AnimatorListenerAdapter() {
-                    private boolean mWasCancelled;
-
-                    @Override
-                    public void onAnimationStart(Animator animation) {
-                        InteractionJankMonitor.getInstance().begin(mSourceNotification,
-                                CUJ_NOTIFICATION_APP_START);
-                    }
-
-                    @Override
-                    public void onAnimationCancel(Animator animation) {
-                        mWasCancelled = true;
-                    }
-
-                    @Override
-                    public void onAnimationEnd(Animator animation) {
-                        setExpandAnimationRunning(false);
-                        invokeCallback(iRemoteAnimationFinishedCallback);
-                        if (!mWasCancelled) {
-                            InteractionJankMonitor.getInstance().end(CUJ_NOTIFICATION_APP_START);
-                        } else {
-                            InteractionJankMonitor.getInstance().cancel(CUJ_NOTIFICATION_APP_START);
-                        }
-                    }
-                });
-                anim.start();
-                setAnimationPending(false);
-            });
-        }
-
-        private void invokeCallback(IRemoteAnimationFinishedCallback callback) {
-            try {
-                callback.onAnimationFinished();
-            } catch (RemoteException e) {
-                e.printStackTrace();
-            }
-        }
-
-        private RemoteAnimationTarget getPrimaryRemoteAnimationTarget(
-                RemoteAnimationTarget[] remoteAnimationTargets) {
-            RemoteAnimationTarget primary = null;
-            for (RemoteAnimationTarget app : remoteAnimationTargets) {
-                if (app.mode == RemoteAnimationTarget.MODE_OPENING) {
-                    primary = app;
-                    break;
-                }
-            }
-            return primary;
-        }
-
-        private RemoteAnimationTarget getNavBarRemoteAnimationTarget(
-                RemoteAnimationTarget[] remoteAnimationTargets) {
-            RemoteAnimationTarget navBar = null;
-            for (RemoteAnimationTarget target : remoteAnimationTargets) {
-                if (target.windowType == TYPE_NAVIGATION_BAR) {
-                    navBar = target;
-                    break;
-                }
-            }
-            return navBar;
-        }
-
-        private void setExpandAnimationRunning(boolean running) {
-            mNotificationPanel.setLaunchingNotification(running);
-            mSourceNotification.setExpandAnimationRunning(running);
-            mNotificationShadeWindowViewController.setExpandAnimationRunning(running);
-            mNotificationContainer.setExpandingNotification(running ? mSourceNotification : null);
-            mAnimationRunning = running;
-            if (!running) {
-                mCallback.onExpandAnimationFinished(mIsFullScreenLaunch);
-                applyParamsToNotification(null);
-                applyParamsToNotificationShade(null);
-            }
-
-        }
-
-        private void applyParamsToNotificationShade(ExpandAnimationParameters params) {
-            mNotificationContainer.applyExpandAnimationParams(params);
-            mNotificationPanel.applyExpandAnimationParams(params);
-            mDepthController.setNotificationLaunchAnimationParams(params);
-        }
-
-        private void applyParamsToNotification(ExpandAnimationParameters params) {
-            mSourceNotification.applyExpandAnimationParams(params);
-        }
-
-        private void applyParamsToWindow(RemoteAnimationTarget app) {
-            Matrix m = new Matrix();
-            m.postTranslate(0, (float) (mParams.top - app.position.y));
-            mWindowCrop.set(mParams.left, 0, mParams.right, mParams.getHeight());
-            float cornerRadius = Math.min(mParams.topCornerRadius, mParams.bottomCornerRadius);
-            SurfaceParams params = new SurfaceParams.Builder(app.leash)
-                    .withAlpha(1f)
-                    .withMatrix(m)
-                    .withWindowCrop(mWindowCrop)
-                    .withLayer(app.prefixOrderIndex)
-                    .withCornerRadius(cornerRadius)
-                    .withVisibility(true)
-                    .build();
-            mSyncRtTransactionApplier.scheduleApply(params);
-        }
-
-        private void applyNavigationBarParamsToWindow(RemoteAnimationTarget navBarTarget) {
-            if (navBarTarget == null) {
-                return;
-            }
-
-            // calculate navigation bar fade-out progress
-            final float fadeOutProgress = mParams.getProgress(0,
-                    ANIMATION_DURATION_NAV_FADE_OUT);
-
-            // calculate navigation bar fade-in progress
-            final float fadeInProgress = mParams.getProgress(ANIMATION_DELAY_NAV_FADE_IN,
-                    ANIMATION_DURATION_NAV_FADE_OUT);
-
-            final SurfaceParams.Builder builder = new SurfaceParams.Builder(navBarTarget.leash);
-            if (fadeInProgress > 0) {
-                Matrix m = new Matrix();
-                m.postTranslate(0, (float) (mParams.top - navBarTarget.position.y));
-                mWindowCrop.set(mParams.left, 0, mParams.right, mParams.getHeight());
-                builder.withMatrix(m)
-                        .withWindowCrop(mWindowCrop)
-                        .withVisibility(true);
-                builder.withAlpha(NAV_FADE_IN_INTERPOLATOR.getInterpolation(fadeInProgress));
-            } else {
-                builder.withAlpha(1f - NAV_FADE_OUT_INTERPOLATOR.getInterpolation(fadeOutProgress));
-            }
-            mSyncRtTransactionApplier.scheduleApply(builder.build());
-        }
-
-        @Override
-        public void onAnimationCancelled() throws RemoteException {
-            mMainExecutor.execute(() -> {
-                setAnimationPending(false);
-                mCallback.onLaunchAnimationCancelled();
-            });
-        }
-    };
-
-    public static class ExpandAnimationParameters {
-        public float linearProgress;
-        int[] startPosition;
-        float startTranslationZ;
-        int left;
-        int top;
-        int right;
-        int bottom;
-        int startClipTopAmount;
-        int parentStartClipTopAmount;
-        float topCornerRadius;
-        float bottomCornerRadius;
-
-        public ExpandAnimationParameters() {
-        }
-
-        public int getTop() {
-            return top;
-        }
-
-        public int getBottom() {
-            return bottom;
-        }
-
-        public int getWidth() {
-            return right - left;
-        }
-
-        public int getHeight() {
-            return bottom - top;
-        }
-
-        public int getTopChange() {
-            // We need this compensation to ensure that the QS moves in sync.
-            int clipTopAmountCompensation = 0;
-            if (startClipTopAmount != 0.0f) {
-                clipTopAmountCompensation = (int) MathUtils.lerp(0, startClipTopAmount,
-                        Interpolators.FAST_OUT_SLOW_IN.getInterpolation(linearProgress));
-            }
-            return Math.min(top - startPosition[1] - clipTopAmountCompensation, 0);
-        }
-
-        public float getProgress() {
-            return linearProgress;
-        }
-
-        public float getProgress(long delay, long duration) {
-            return MathUtils.constrain((linearProgress * ANIMATION_DURATION - delay)
-                    / duration, 0.0f, 1.0f);
-        }
-
-        public int getStartClipTopAmount() {
-            return startClipTopAmount;
-        }
-
-        public int getParentStartClipTopAmount() {
-            return parentStartClipTopAmount;
-        }
-
-        public float getStartTranslationZ() {
-            return startTranslationZ;
-        }
-
-        public float getTopCornerRadius() {
-            return topCornerRadius;
-        }
-
-        public float getBottomCornerRadius() {
-            return bottomCornerRadius;
-        }
-    }
-
-    public interface Callback {
-
-        /**
-         * Called when the launch animation was cancelled.
-         */
-        void onLaunchAnimationCancelled();
-
-        /**
-         * Called when the launch animation has timed out without starting an actual animation.
-         */
-        void onExpandAnimationTimedOut();
-
-        /**
-         * Called when the expand animation has finished.
-         *
-         * @param launchIsFullScreen True if this launch was fullscreen, such that now the window
-         *                           fills the whole screen
-         */
-        void onExpandAnimationFinished(boolean launchIsFullScreen);
-
-        /**
-         * Are animations currently enabled.
-         */
-        boolean areLaunchAnimationsEnabled();
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ExpandAnimationParameters.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ExpandAnimationParameters.kt
new file mode 100644
index 0000000..d5835fc
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ExpandAnimationParameters.kt
@@ -0,0 +1,44 @@
+package com.android.systemui.statusbar.notification
+
+import android.util.MathUtils
+import com.android.internal.annotations.VisibleForTesting
+import com.android.systemui.Interpolators
+import com.android.systemui.plugins.animation.ActivityLaunchAnimator
+import kotlin.math.min
+
+/** Parameters for the notifications expand animations. */
+class ExpandAnimationParameters(
+    top: Int,
+    bottom: Int,
+    left: Int,
+    right: Int,
+
+    topCornerRadius: Float = 0f,
+    bottomCornerRadius: Float = 0f
+) : ActivityLaunchAnimator.State(top, bottom, left, right, topCornerRadius, bottomCornerRadius) {
+    @VisibleForTesting
+    constructor() : this(
+        top = 0, bottom = 0, left = 0, right = 0, topCornerRadius = 0f, bottomCornerRadius = 0f
+    )
+
+    var startTranslationZ = 0f
+    var startClipTopAmount = 0
+    var parentStartClipTopAmount = 0
+    var progress = 0f
+    var linearProgress = 0f
+
+    override val topChange: Int
+        get() {
+            // We need this compensation to ensure that the QS moves in sync.
+            var clipTopAmountCompensation = 0
+            if (startClipTopAmount.toFloat() != 0.0f) {
+                clipTopAmountCompensation = MathUtils.lerp(0f, startClipTopAmount.toFloat(),
+                        Interpolators.FAST_OUT_SLOW_IN.getInterpolation(linearProgress)).toInt()
+            }
+            return min(super.topChange - clipTopAmountCompensation, 0)
+        }
+
+    fun getProgress(delay: Long, duration: Long): Float {
+        return ActivityLaunchAnimator.getProgress(linearProgress, delay, duration)
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt
new file mode 100644
index 0000000..2f966b4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt
@@ -0,0 +1,136 @@
+package com.android.systemui.statusbar.notification
+
+import android.view.View
+import com.android.internal.jank.InteractionJankMonitor
+import com.android.systemui.statusbar.NotificationShadeDepthController
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
+import com.android.systemui.statusbar.notification.stack.NotificationListContainer
+import com.android.systemui.statusbar.phone.NotificationPanelViewController
+import com.android.systemui.plugins.animation.ActivityLaunchAnimator
+import com.android.systemui.statusbar.phone.NotificationShadeWindowViewController
+import kotlin.math.ceil
+import kotlin.math.max
+
+/** A provider of [NotificationLaunchAnimatorController]. */
+class NotificationLaunchAnimatorControllerProvider(
+    private val notificationShadeWindowViewController: NotificationShadeWindowViewController,
+    private val notificationPanelViewController: NotificationPanelViewController,
+    private val notificationListContainer: NotificationListContainer,
+    private val depthController: NotificationShadeDepthController
+) {
+    fun getAnimatorController(
+        notification: ExpandableNotificationRow
+    ): NotificationLaunchAnimatorController {
+        return NotificationLaunchAnimatorController(
+            notificationShadeWindowViewController,
+            notificationPanelViewController,
+            notificationListContainer,
+            depthController,
+            notification
+        )
+    }
+}
+
+/**
+ * An [ActivityLaunchAnimator.Controller] that animates an [ExpandableNotificationRow]. An instance
+ * of this class can be passed to [ActivityLaunchAnimator.startIntentWithAnimation] to animate a
+ * notification expanding into an opening window.
+ */
+class NotificationLaunchAnimatorController(
+    private val notificationShadeWindowViewController: NotificationShadeWindowViewController,
+    private val notificationPanelViewController: NotificationPanelViewController,
+    private val notificationListContainer: NotificationListContainer,
+    private val depthController: NotificationShadeDepthController,
+    private val notification: ExpandableNotificationRow
+) : ActivityLaunchAnimator.Controller {
+    override fun getRootView(): View = notification.rootView
+
+    override fun createAnimatorState(): ActivityLaunchAnimator.State {
+        // If the notification panel is collapsed, the clip may be larger than the height.
+        val height = max(0, notification.actualHeight - notification.clipBottomAmount)
+        val location = notification.locationOnScreen
+
+        val params = ExpandAnimationParameters(
+                top = location[1],
+                bottom = location[1] + height,
+                left = location[0],
+                right = location[0] + notification.width,
+                topCornerRadius = notification.currentBackgroundRadiusTop,
+                bottomCornerRadius = notification.currentBackgroundRadiusBottom
+        )
+
+        params.startTranslationZ = notification.translationZ
+        params.startClipTopAmount = notification.clipTopAmount
+        if (notification.isChildInGroup) {
+            val parentClip = notification.notificationParent.clipTopAmount
+            params.parentStartClipTopAmount = parentClip
+
+            // We need to calculate how much the child is clipped by the parent because children
+            // always have 0 clipTopAmount
+            if (parentClip != 0) {
+                val childClip = parentClip - notification.translationY
+                if (childClip > 0) {
+                    params.startClipTopAmount = ceil(childClip.toDouble()).toInt()
+                }
+            }
+        }
+
+        return params
+    }
+
+    override fun onIntentStarted(willAnimate: Boolean) {
+        notificationShadeWindowViewController.setExpandAnimationRunning(willAnimate)
+    }
+
+    override fun onLaunchAnimationCancelled() {
+        // TODO(b/184121838): Should we call InteractionJankMonitor.cancel if the animation started
+        // here?
+        notificationShadeWindowViewController.setExpandAnimationRunning(false)
+    }
+
+    override fun onLaunchAnimationTimedOut() {
+        notificationShadeWindowViewController.setExpandAnimationRunning(false)
+    }
+
+    override fun onLaunchAnimationAborted() {
+        notificationShadeWindowViewController.setExpandAnimationRunning(false)
+    }
+
+    override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
+        notificationPanelViewController.setLaunchingNotification(true)
+        notification.isExpandAnimationRunning = true
+        notificationListContainer.setExpandingNotification(notification)
+
+        InteractionJankMonitor.getInstance().begin(notification,
+            InteractionJankMonitor.CUJ_NOTIFICATION_APP_START)
+    }
+
+    override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
+        InteractionJankMonitor.getInstance().end(InteractionJankMonitor.CUJ_NOTIFICATION_APP_START)
+
+        notificationPanelViewController.setLaunchingNotification(false)
+        notification.isExpandAnimationRunning = false
+        notificationShadeWindowViewController.setExpandAnimationRunning(false)
+        notificationListContainer.setExpandingNotification(null)
+        applyParams(null)
+    }
+
+    private fun applyParams(params: ExpandAnimationParameters?) {
+        notification.applyExpandAnimationParams(params)
+        notificationListContainer.applyExpandAnimationParams(params)
+        notificationPanelViewController.applyExpandAnimationParams(params)
+        depthController.notificationLaunchAnimationParams = params
+    }
+
+    override fun onLaunchAnimationProgress(
+        state: ActivityLaunchAnimator.State,
+        progress: Float,
+        linearProgress: Float
+    ) {
+        val params = state as ExpandAnimationParameters
+        params.progress = progress
+        params.linearProgress = linearProgress
+
+        applyParams(params)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index a3a4014..207a894 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -19,7 +19,6 @@
 import static android.app.Notification.Action.SEMANTIC_ACTION_MARK_CONVERSATION_AS_PRIORITY;
 import static android.service.notification.NotificationListenerService.REASON_CANCEL;
 
-import static com.android.systemui.statusbar.notification.ActivityLaunchAnimator.ExpandAnimationParameters;
 import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_HEADSUP;
 import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_PUBLIC;
 
@@ -79,6 +78,7 @@
 import com.android.systemui.classifier.FalsingCollector;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.PluginListener;
+import com.android.systemui.plugins.animation.ActivityLaunchAnimator;
 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin.MenuItem;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -86,7 +86,7 @@
 import com.android.systemui.statusbar.RemoteInputController;
 import com.android.systemui.statusbar.StatusBarIconView;
 import com.android.systemui.statusbar.notification.AboveShelfChangedListener;
-import com.android.systemui.statusbar.notification.ActivityLaunchAnimator;
+import com.android.systemui.statusbar.notification.ExpandAnimationParameters;
 import com.android.systemui.statusbar.notification.NotificationUtils;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.legacy.VisualStabilityManager;
@@ -2045,9 +2045,8 @@
         float extraWidthForClipping = params.getWidth() - getWidth();
         setExtraWidthForClipping(extraWidthForClipping);
         int top = params.getTop();
-        float interpolation = Interpolators.FAST_OUT_SLOW_IN.getInterpolation(params.getProgress());
         int startClipTopAmount = params.getStartClipTopAmount();
-        int clipTopAmount = (int) MathUtils.lerp(startClipTopAmount, 0, interpolation);
+        int clipTopAmount = (int) MathUtils.lerp(startClipTopAmount, 0, params.getProgress());
         if (mNotificationParent != null) {
             float parentY = mNotificationParent.getTranslationY();
             top -= parentY;
@@ -2096,7 +2095,7 @@
         if (expandAnimationRunning) {
             contentView.animate()
                     .alpha(0f)
-                    .setDuration(ActivityLaunchAnimator.ANIMATION_DURATION_FADE_CONTENT)
+                    .setDuration(ActivityLaunchAnimator.ANIMATION_DURATION_FADE_OUT_CONTENT)
                     .setInterpolator(Interpolators.ALPHA_OUT);
             setAboveShelf(true);
             mExpandAnimationRunning = true;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java
index 9588563..07d1e68 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java
@@ -31,7 +31,8 @@
 import com.android.internal.util.ArrayUtils;
 import com.android.systemui.Interpolators;
 import com.android.systemui.R;
-import com.android.systemui.statusbar.notification.ActivityLaunchAnimator;
+import com.android.systemui.plugins.animation.ActivityLaunchAnimator;
+import com.android.systemui.statusbar.notification.ExpandAnimationParameters;
 
 /**
  * A view that can be used for both the dimmed and normal background of an notification.
@@ -277,13 +278,14 @@
         invalidate();
     }
 
-    public void setExpandAnimationParams(ActivityLaunchAnimator.ExpandAnimationParameters params) {
+    /** Set the current expand animation parameters. */
+    public void setExpandAnimationParams(ExpandAnimationParameters params) {
         mActualHeight = params.getHeight();
         mActualWidth = params.getWidth();
         float alphaProgress = Interpolators.ALPHA_IN.getInterpolation(
                 params.getProgress(
-                        ActivityLaunchAnimator.ANIMATION_DURATION_FADE_CONTENT /* delay */,
-                        ActivityLaunchAnimator.ANIMATION_DURATION_FADE_APP /* duration */));
+                        ActivityLaunchAnimator.ANIMATION_DURATION_FADE_OUT_CONTENT /* delay */,
+                        ActivityLaunchAnimator.ANIMATION_DURATION_FADE_IN_WINDOW /* duration */));
         mBackground.setAlpha((int) (mDrawableAlpha * (1.0f - alphaProgress)));
         invalidate();
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationListContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationListContainer.java
index 72f3216..2a2e733f7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationListContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationListContainer.java
@@ -16,14 +16,13 @@
 
 package com.android.systemui.statusbar.notification.stack;
 
-import static com.android.systemui.statusbar.notification.ActivityLaunchAnimator.ExpandAnimationParameters;
-
 import android.view.View;
 import android.view.ViewGroup;
 
 import androidx.annotation.Nullable;
 
 import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper;
+import com.android.systemui.statusbar.notification.ExpandAnimationParameters;
 import com.android.systemui.statusbar.notification.NotificationActivityStarter;
 import com.android.systemui.statusbar.notification.VisibilityLocationProvider;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 40c0b89..ad06e7d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -17,7 +17,6 @@
 package com.android.systemui.statusbar.notification.stack;
 
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_SCROLL_FLING;
-import static com.android.systemui.statusbar.notification.ActivityLaunchAnimator.ExpandAnimationParameters;
 import static com.android.systemui.statusbar.notification.stack.NotificationSectionsManagerKt.BUCKET_SILENT;
 import static com.android.systemui.statusbar.notification.stack.StackStateAnimator.ANIMATION_DURATION_SWIPE;
 import static com.android.systemui.util.InjectionInflationController.VIEW_CONTEXT;
@@ -91,6 +90,7 @@
 import com.android.systemui.statusbar.RemoteInputController;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
+import com.android.systemui.statusbar.notification.ExpandAnimationParameters;
 import com.android.systemui.statusbar.notification.FakeShadowView;
 import com.android.systemui.statusbar.notification.NotificationActivityStarter;
 import com.android.systemui.statusbar.notification.NotificationUtils;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index 7baad1c..ce7b397 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -75,8 +75,8 @@
 import com.android.systemui.statusbar.RemoteInputController;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
-import com.android.systemui.statusbar.notification.ActivityLaunchAnimator;
 import com.android.systemui.statusbar.notification.DynamicPrivacyController;
+import com.android.systemui.statusbar.notification.ExpandAnimationParameters;
 import com.android.systemui.statusbar.notification.ForegroundServiceDismissalFeatureController;
 import com.android.systemui.statusbar.notification.NotificationActivityStarter;
 import com.android.systemui.statusbar.notification.NotificationEntryListener;
@@ -1497,8 +1497,7 @@
         }
 
         @Override
-        public void applyExpandAnimationParams(
-                ActivityLaunchAnimator.ExpandAnimationParameters params) {
+        public void applyExpandAnimationParams(ExpandAnimationParameters params) {
             mView.applyExpandAnimationParams(params);
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
index 555df5c..0cad113 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
@@ -26,7 +26,6 @@
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE;
 import static com.android.systemui.classifier.Classifier.QUICK_SETTINGS;
 import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
-import static com.android.systemui.statusbar.notification.ActivityLaunchAnimator.ExpandAnimationParameters;
 import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.ROWS_ALL;
 
 import static java.lang.Float.isNaN;
@@ -100,6 +99,7 @@
 import com.android.systemui.media.MediaDataManager;
 import com.android.systemui.media.MediaHierarchyManager;
 import com.android.systemui.plugins.FalsingManager;
+import com.android.systemui.plugins.animation.ActivityLaunchAnimator;
 import com.android.systemui.plugins.qs.DetailAdapter;
 import com.android.systemui.plugins.qs.QS;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -117,10 +117,10 @@
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.VibratorHelper;
-import com.android.systemui.statusbar.notification.ActivityLaunchAnimator;
 import com.android.systemui.statusbar.notification.AnimatableProperty;
 import com.android.systemui.statusbar.notification.ConversationNotificationManager;
 import com.android.systemui.statusbar.notification.DynamicPrivacyController;
+import com.android.systemui.statusbar.notification.ExpandAnimationParameters;
 import com.android.systemui.statusbar.notification.NotificationEntryManager;
 import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator;
 import com.android.systemui.statusbar.notification.PropertyAnimator;
@@ -178,6 +178,10 @@
      * Fling until QS is completely hidden.
      */
     private static final int FLING_HIDE = 2;
+    private static final long ANIMATION_DELAY_ICON_FADE_IN =
+            ActivityLaunchAnimator.ANIMATION_DURATION - CollapsedStatusBarFragment.FADE_IN_DURATION
+                    - CollapsedStatusBarFragment.FADE_IN_DELAY - 16;
+
     private final DozeParameters mDozeParameters;
     private final OnHeightChangedListener mOnHeightChangedListener = new OnHeightChangedListener();
     private final OnClickListener mOnClickListener = new OnClickListener();
@@ -3208,8 +3212,7 @@
             return;
         }
 
-        boolean hideIcons = params.getProgress(
-                ActivityLaunchAnimator.ANIMATION_DELAY_ICON_FADE_IN, 100) == 0.0f;
+        boolean hideIcons = params.getProgress(ANIMATION_DELAY_ICON_FADE_IN, 100) == 0.0f;
         if (hideIcons != mHideIconsDuringNotificationLaunch) {
             mHideIconsDuringNotificationLaunch = hideIcons;
             if (!hideIcons) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowControllerImpl.java
index d074e64..ee78c00 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowControllerImpl.java
@@ -527,6 +527,11 @@
     }
 
     @Override
+    public boolean isLaunchingActivity() {
+        return mCurrentState.mLaunchingActivity;
+    }
+
+    @Override
     public void setScrimsVisibility(int scrimsVisibility) {
         mCurrentState.mScrimsVisibility = scrimsVisibility;
         apply(mCurrentState);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewController.java
index 2ff7c99..72f7ff8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewController.java
@@ -92,7 +92,6 @@
     private View mBrightnessMirror;
     private boolean mTouchActive;
     private boolean mTouchCancelled;
-    private boolean mExpandAnimationPending;
     private boolean mExpandAnimationRunning;
     private NotificationStackScrollLayout mStackScrollLayout;
     private PhoneStatusBarView mStatusBarView;
@@ -235,7 +234,7 @@
                         || ev.getActionMasked() == MotionEvent.ACTION_CANCEL) {
                     setTouchActive(false);
                 }
-                if (mTouchCancelled || mExpandAnimationRunning || mExpandAnimationPending) {
+                if (mTouchCancelled || mExpandAnimationRunning) {
                     return false;
                 }
                 mFalsingCollector.onTouchEvent(ev);
@@ -435,8 +434,6 @@
     }
 
     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
-        pw.print("  mExpandAnimationPending=");
-        pw.println(mExpandAnimationPending);
         pw.print("  mExpandAnimationRunning=");
         pw.println(mExpandAnimationRunning);
         pw.print("  mTouchCancelled=");
@@ -445,19 +442,10 @@
         pw.println(mTouchActive);
     }
 
-    public void setExpandAnimationPending(boolean pending) {
-        if (mExpandAnimationPending != pending) {
-            mExpandAnimationPending = pending;
-            mNotificationShadeWindowController
-                    .setLaunchingActivity(mExpandAnimationPending | mExpandAnimationRunning);
-        }
-    }
-
     public void setExpandAnimationRunning(boolean running) {
         if (mExpandAnimationRunning != running) {
             mExpandAnimationRunning = running;
-            mNotificationShadeWindowController
-                    .setLaunchingActivity(mExpandAnimationPending | mExpandAnimationRunning);
+            mNotificationShadeWindowController.setLaunchingActivity(mExpandAnimationRunning);
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
index 8ed9cd6..5fd0d66 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
@@ -173,6 +173,7 @@
 import com.android.systemui.plugins.OverlayPlugin;
 import com.android.systemui.plugins.PluginDependencyProvider;
 import com.android.systemui.plugins.PluginListener;
+import com.android.systemui.plugins.animation.ActivityLaunchAnimator;
 import com.android.systemui.plugins.qs.QS;
 import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.SnoozeOption;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -208,9 +209,9 @@
 import com.android.systemui.statusbar.VibratorHelper;
 import com.android.systemui.statusbar.charging.ChargingRippleView;
 import com.android.systemui.statusbar.charging.WiredChargingRippleController;
-import com.android.systemui.statusbar.notification.ActivityLaunchAnimator;
 import com.android.systemui.statusbar.notification.DynamicPrivacyController;
 import com.android.systemui.statusbar.notification.NotificationActivityStarter;
+import com.android.systemui.statusbar.notification.NotificationLaunchAnimatorControllerProvider;
 import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.legacy.VisualStabilityManager;
@@ -263,7 +264,7 @@
         ActivityStarter, KeyguardStateController.Callback,
         OnHeadsUpChangedListener, CommandQueue.Callbacks,
         ColorExtractor.OnColorsChangedListener, ConfigurationListener,
-        StatusBarStateController.StateListener, ActivityLaunchAnimator.Callback,
+        StatusBarStateController.StateListener,
         LifecycleOwner, BatteryController.BatteryStateChangeCallback {
     public static final boolean MULTIUSER_DEBUG = false;
 
@@ -692,6 +693,7 @@
     private boolean mVibrateOnOpening;
     private final VibratorHelper mVibratorHelper;
     private ActivityLaunchAnimator mActivityLaunchAnimator;
+    private NotificationLaunchAnimatorControllerProvider mNotificationAnimationProvider;
     protected StatusBarNotificationPresenter mPresenter;
     private NotificationActivityStarter mNotificationActivityStarter;
     private Lazy<NotificationShadeDepthController> mNotificationShadeDepthControllerLazy;
@@ -1370,16 +1372,18 @@
 
     private void setUpPresenter() {
         // Set up the initial notification state.
-        mActivityLaunchAnimator = new ActivityLaunchAnimator(
-                mNotificationShadeWindowViewController, this, mNotificationPanelViewController,
-                mNotificationShadeDepthControllerLazy.get(),
+        mActivityLaunchAnimator = new ActivityLaunchAnimator();
+        mNotificationAnimationProvider = new NotificationLaunchAnimatorControllerProvider(
+                mNotificationShadeWindowViewController,
+                mNotificationPanelViewController,
                 mStackScrollerController.getNotificationListContainer(),
-                mContext.getMainExecutor());
+                mNotificationShadeDepthControllerLazy.get()
+        );
 
         // TODO: inject this.
         mPresenter = new StatusBarNotificationPresenter(mContext, mNotificationPanelViewController,
                 mHeadsUpManager, mNotificationShadeWindowView, mStackScrollerController,
-                mDozeScrimController, mScrimController, mActivityLaunchAnimator,
+                mDozeScrimController, mScrimController, mNotificationShadeWindowController,
                 mDynamicPrivacyController, mKeyguardStateController,
                 mKeyguardIndicationController,
                 this /* statusBar */, mShadeController, mCommandQueue, mInitController,
@@ -1392,6 +1396,7 @@
                 mStatusBarNotificationActivityStarterBuilder
                         .setStatusBar(this)
                         .setActivityLaunchAnimator(mActivityLaunchAnimator)
+                        .setNotificationAnimatorControllerProvider(mNotificationAnimationProvider)
                         .setNotificationPresenter(mPresenter)
                         .setNotificationPanelViewController(mNotificationPanelViewController)
                         .build();
@@ -1990,16 +1995,16 @@
         return mHeadsUpAppearanceController.shouldBeVisible();
     }
 
+    /** A launch animation was cancelled. */
     //TODO: These can / should probably be moved to NotificationPresenter or ShadeController
-    @Override
     public void onLaunchAnimationCancelled() {
         if (!mPresenter.isCollapsing()) {
             onClosingFinished();
         }
     }
 
-    @Override
-    public void onExpandAnimationFinished(boolean launchIsFullScreen) {
+    /** A launch animation ended. */
+    public void onLaunchAnimationEnd(boolean launchIsFullScreen) {
         if (!mPresenter.isCollapsing()) {
             onClosingFinished();
         }
@@ -2008,19 +2013,19 @@
         }
     }
 
-    @Override
-    public void onExpandAnimationTimedOut() {
+    /** A launch animation timed out. */
+    public void onLaunchAnimationTimedOut(boolean isLaunchForActivity) {
         if (mPresenter.isPresenterFullyCollapsed() && !mPresenter.isCollapsing()
-                && mActivityLaunchAnimator != null
-                && !mActivityLaunchAnimator.isLaunchForActivity()) {
+                && isLaunchForActivity) {
             onClosingFinished();
         } else {
             mShadeController.collapsePanel(true /* animate */);
         }
     }
 
-    @Override
+    /** Whether we should animate an activity launch. */
     public boolean areLaunchAnimationsEnabled() {
+        // TODO(b/184121838): Support lock screen launch animations.
         return mState == StatusBarState.SHADE;
     }
 
@@ -3134,8 +3139,15 @@
     }
 
     @Override
-    public void postStartActivityDismissingKeyguard(final PendingIntent intent) {
-        mHandler.post(() -> startPendingIntentDismissingKeyguard(intent));
+    public void postStartActivityDismissingKeyguard(PendingIntent intent) {
+        postStartActivityDismissingKeyguard(intent, null /* animationController */);
+    }
+
+    @Override
+    public void postStartActivityDismissingKeyguard(final PendingIntent intent,
+            @Nullable ActivityLaunchAnimator.Controller animationController) {
+        mHandler.post(() -> startPendingIntentDismissingKeyguard(intent,
+                null /* intentSentUiThreadCallback */, animationController));
     }
 
     @Override
@@ -3618,6 +3630,23 @@
         mShadeController.runPostCollapseRunnables();
     }
 
+    /**
+     * Collapse the panel directly if we are on the main thread, post the collapsing on the main
+     * thread if we are not.
+     */
+    void collapsePanelOnMainThread() {
+        if (Looper.getMainLooper().isCurrentThread()) {
+            mShadeController.collapsePanel();
+        } else {
+            mContext.getMainExecutor().execute(mShadeController::collapsePanel);
+        }
+    }
+
+    /** Collapse the panel. The collapsing will be animated for the given {@code duration}. */
+    void collapsePanelWithDuration(int duration) {
+        mNotificationPanelViewController.collapseWithDuration(duration);
+    }
+
     @Override
     public void onStatePreChange(int oldState, int newState) {
         // If we're visible and switched to SHADE_LOCKED (the user dragged
@@ -4415,7 +4444,14 @@
         KeyboardShortcuts.dismiss();
     }
 
-    public void executeActionDismissingKeyguard(Runnable action, boolean afterKeyguardGone) {
+    /**
+     * Dismiss the keyguard then execute an action.
+     *
+     * @param action The action to execute after dismissing the keyguard.
+     * @param collapsePanel Whether we should collapse the panel after dismissing the keyguard.
+     */
+    private void executeActionDismissingKeyguard(Runnable action, boolean afterKeyguardGone,
+            boolean collapsePanel) {
         if (!mDeviceProvisionedController.isDeviceProvisioned()) return;
 
         dismissKeyguardThenExecute(() -> {
@@ -4431,7 +4467,8 @@
                 action.run();
             }).start();
 
-            return mShadeController.collapsePanel();
+            boolean deferred = collapsePanel ? mShadeController.collapsePanel() : false;
+            return deferred;
         }, afterKeyguardGone);
     }
 
@@ -4443,27 +4480,55 @@
     @Override
     public void startPendingIntentDismissingKeyguard(
             final PendingIntent intent, @Nullable final Runnable intentSentUiThreadCallback) {
-        startPendingIntentDismissingKeyguard(intent, intentSentUiThreadCallback, null /* row */);
+        startPendingIntentDismissingKeyguard(intent, intentSentUiThreadCallback,
+                (ActivityLaunchAnimator.Controller) null);
+    }
+
+    @Override
+    public void startPendingIntentDismissingKeyguard(PendingIntent intent,
+            Runnable intentSentUiThreadCallback, View associatedView) {
+        ActivityLaunchAnimator.Controller animationController = null;
+        if (associatedView instanceof ExpandableNotificationRow) {
+            animationController = mNotificationAnimationProvider.getAnimatorController(
+                    ((ExpandableNotificationRow) associatedView));
+        }
+
+        startPendingIntentDismissingKeyguard(intent, intentSentUiThreadCallback,
+                animationController);
     }
 
     @Override
     public void startPendingIntentDismissingKeyguard(
             final PendingIntent intent, @Nullable final Runnable intentSentUiThreadCallback,
-            View associatedView) {
+            @Nullable ActivityLaunchAnimator.Controller animationController) {
         final boolean afterKeyguardGone = intent.isActivity()
                 && mActivityIntentHelper.wouldLaunchResolverActivity(intent.getIntent(),
                 mLockscreenUserManager.getCurrentUserId());
 
+        boolean animate =
+                animationController != null && areLaunchAnimationsEnabled() && !isOccluded();
+        boolean collapse = !animate;
         executeActionDismissingKeyguard(() -> {
             try {
-                intent.send(null, 0, null, null, null, null, getActivityOptions(
-                        mDisplayId,
-                        mActivityLaunchAnimator.getLaunchAnimation(associatedView, isOccluded())));
+                // We wrap animationCallback with a StatusBarLaunchAnimatorController so that the
+                // shade is collapsed after the animation (or when it is cancelled, aborted, etc).
+                ActivityLaunchAnimator.Controller controller =
+                        animate ? new StatusBarLaunchAnimatorController(animationController, this,
+                                intent.isActivity())
+                                : null;
+
+                mActivityLaunchAnimator.startPendingIntentWithAnimation(
+                        controller,
+                        (animationAdapter) -> intent.sendAndReturnResult(null, 0, null, null, null,
+                                null, getActivityOptions(mDisplayId, animationAdapter)));
             } catch (PendingIntent.CanceledException e) {
                 // the stack trace isn't very helpful here.
                 // Just log the exception message.
                 Log.w(TAG, "Sending intent failed: " + e);
-
+                if (!collapse) {
+                    // executeActionDismissingKeyguard did not collapse for us already.
+                    collapsePanelOnMainThread();
+                }
                 // TODO: Dismiss Keyguard.
             }
             if (intent.isActivity()) {
@@ -4472,7 +4537,7 @@
             if (intentSentUiThreadCallback != null) {
                 postOnUiThread(intentSentUiThreadCallback);
             }
-        }, afterKeyguardGone);
+        }, afterKeyguardGone, collapse);
     }
 
     private void postOnUiThread(Runnable runnable) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt
new file mode 100644
index 0000000..d45f64f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt
@@ -0,0 +1,47 @@
+package com.android.systemui.statusbar.phone
+
+import com.android.systemui.plugins.animation.ActivityLaunchAnimator
+
+/**
+ * A [ActivityLaunchAnimator.Controller] that takes care of collapsing the status bar at the right
+ * time.
+ */
+class StatusBarLaunchAnimatorController(
+    private val delegate: ActivityLaunchAnimator.Controller,
+    private val statusBar: StatusBar,
+    private val isLaunchForActivity: Boolean = true
+) : ActivityLaunchAnimator.Controller by delegate {
+    override fun onIntentStarted(willAnimate: Boolean) {
+        delegate.onIntentStarted(willAnimate)
+        if (!willAnimate) {
+            statusBar.collapsePanelOnMainThread()
+        }
+    }
+
+    override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
+        delegate.onLaunchAnimationStart(isExpandingFullyAbove)
+        if (!isExpandingFullyAbove) {
+            statusBar.collapsePanelWithDuration(ActivityLaunchAnimator.ANIMATION_DURATION.toInt())
+        }
+    }
+
+    override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
+        delegate.onLaunchAnimationEnd(isExpandingFullyAbove)
+        statusBar.onLaunchAnimationEnd(isExpandingFullyAbove)
+    }
+
+    override fun onLaunchAnimationCancelled() {
+        delegate.onLaunchAnimationCancelled()
+        statusBar.onLaunchAnimationCancelled()
+    }
+
+    override fun onLaunchAnimationTimedOut() {
+        delegate.onLaunchAnimationTimedOut()
+        statusBar.onLaunchAnimationTimedOut(isLaunchForActivity)
+    }
+
+    override fun onLaunchAnimationAborted() {
+        delegate.onLaunchAnimationAborted()
+        statusBar.collapsePanelOnMainThread()
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
index 801ac96..2e918da 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
@@ -21,7 +21,6 @@
 import static com.android.systemui.statusbar.phone.StatusBar.getActivityOptions;
 
 import android.app.ActivityManager;
-import android.app.ActivityTaskManager;
 import android.app.KeyguardManager;
 import android.app.Notification;
 import android.app.NotificationManager;
@@ -40,7 +39,6 @@
 import android.service.notification.StatusBarNotification;
 import android.text.TextUtils;
 import android.util.EventLog;
-import android.view.RemoteAnimationAdapter;
 
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.statusbar.NotificationVisibility;
@@ -52,6 +50,7 @@
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dagger.qualifiers.UiBackground;
 import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.plugins.animation.ActivityLaunchAnimator;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.FeatureFlags;
@@ -60,11 +59,10 @@
 import com.android.systemui.statusbar.NotificationPresenter;
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
 import com.android.systemui.statusbar.RemoteInputController;
-import com.android.systemui.statusbar.StatusBarState;
-import com.android.systemui.statusbar.notification.ActivityLaunchAnimator;
 import com.android.systemui.statusbar.notification.NotificationActivityStarter;
 import com.android.systemui.statusbar.notification.NotificationEntryListener;
 import com.android.systemui.statusbar.notification.NotificationEntryManager;
+import com.android.systemui.statusbar.notification.NotificationLaunchAnimatorControllerProvider;
 import com.android.systemui.statusbar.notification.collection.NotifPipeline;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
@@ -124,6 +122,7 @@
     private final NotificationPresenter mPresenter;
     private final NotificationPanelViewController mNotificationPanel;
     private final ActivityLaunchAnimator mActivityLaunchAnimator;
+    private final NotificationLaunchAnimatorControllerProvider mNotificationAnimationProvider;
     private final OnUserInteractionCallback mOnUserInteractionCallback;
 
     private boolean mIsCollapsingToShowActivityOverLockscreen;
@@ -162,7 +161,8 @@
             StatusBar statusBar,
             NotificationPresenter presenter,
             NotificationPanelViewController panel,
-            ActivityLaunchAnimator activityLaunchAnimator) {
+            ActivityLaunchAnimator activityLaunchAnimator,
+            NotificationLaunchAnimatorControllerProvider notificationAnimationProvider) {
         mContext = context;
         mCommandQueue = commandQueue;
         mMainThreadHandler = mainThreadHandler;
@@ -198,6 +198,7 @@
         mPresenter = presenter;
         mNotificationPanel = panel;
         mActivityLaunchAnimator = activityLaunchAnimator;
+        mNotificationAnimationProvider = notificationAnimationProvider;
 
         if (!mFeatureFlags.isNewNotifPipelineRenderingEnabled()) {
             mEntryManager.addNotificationEntryListener(new NotificationEntryListener() {
@@ -400,17 +401,15 @@
         }
 
         if (Looper.getMainLooper().isCurrentThread()) {
-            mBubblesManagerOptional.get().expandStackAndSelectBubble(entry);
+            expandBubbleStack(entry);
         } else {
-            mMainThreadHandler.post(
-                    () -> mBubblesManagerOptional.get().expandStackAndSelectBubble(entry));
+            mMainThreadHandler.post(() -> expandBubbleStack(entry));
         }
+    }
 
-        // expandStackAndSelectBubble won't affect shouldCollapse, so we can collapse directly even
-        // if we are not on the main thread.
-        if (shouldCollapse()) {
-            collapseOnMainThread();
-        }
+    private void expandBubbleStack(NotificationEntry entry) {
+        mBubblesManagerOptional.get().expandStackAndSelectBubble(entry);
+        mShadeController.collapsePanel();
     }
 
     private void startNotificationIntent(
@@ -420,32 +419,36 @@
             ExpandableNotificationRow row,
             boolean wasOccluded,
             boolean isActivityIntent) {
-        RemoteAnimationAdapter adapter = mActivityLaunchAnimator.getLaunchAnimation(row,
-                wasOccluded);
         mLogger.logStartNotificationIntent(entry.getKey(), intent);
         try {
-            if (adapter != null) {
-                ActivityTaskManager.getService()
-                        .registerRemoteAnimationForNextActivityStart(
-                                intent.getCreatorPackage(), adapter);
+            ActivityLaunchAnimator.Controller animationController = null;
+            if (!wasOccluded && mStatusBar.areLaunchAnimationsEnabled()) {
+                animationController = new StatusBarLaunchAnimatorController(
+                        mNotificationAnimationProvider.getAnimatorController(row), mStatusBar,
+                        isActivityIntent);
             }
-            long eventTime = row.getAndResetLastActionUpTime();
-            Bundle options = eventTime > 0
-                    ? getActivityOptions(
-                            mStatusBar.getDisplayId(),
-                            adapter,
-                            mKeyguardStateController.isShowing(),
-                            eventTime)
-                    : getActivityOptions(mStatusBar.getDisplayId(), adapter);
-            int launchResult = intent.sendAndReturnResult(mContext, 0, fillInIntent, null,
-                    null, null, options);
-            mMainThreadHandler.post(() -> {
-                mActivityLaunchAnimator.setLaunchResult(launchResult, isActivityIntent);
-                if (shouldCollapse()) {
-                    collapseOnMainThread();
-                }
-            });
-        } catch (RemoteException | PendingIntent.CanceledException e) {
+
+            mActivityLaunchAnimator.startPendingIntentWithAnimation(animationController,
+                    (adapter) -> {
+                        long eventTime = row.getAndResetLastActionUpTime();
+                        Bundle options = eventTime > 0
+                                ? getActivityOptions(
+                                mStatusBar.getDisplayId(),
+                                adapter,
+                                mKeyguardStateController.isShowing(),
+                                eventTime)
+                                : getActivityOptions(mStatusBar.getDisplayId(), adapter);
+                        return intent.sendAndReturnResult(mContext, 0, fillInIntent, null,
+                                null, null, options);
+                    });
+
+            // Note that other cases when we should still collapse (like activity already on top) is
+            // handled by the StatusBarLaunchAnimatorController.
+            boolean shouldCollapse = animationController == null;
+            if (shouldCollapse) {
+                collapseOnMainThread();
+            }
+        } catch (PendingIntent.CanceledException e) {
             // the stack trace isn't very helpful here.
             // Just log the exception message.
             mLogger.logSendingIntentFailed(e);
@@ -458,20 +461,30 @@
             ExpandableNotificationRow row) {
         mActivityStarter.dismissKeyguardThenExecute(() -> {
             AsyncTask.execute(() -> {
-                int launchResult = TaskStackBuilder.create(mContext)
-                        .addNextIntentWithParentStack(intent)
-                        .startActivities(getActivityOptions(
-                                mStatusBar.getDisplayId(),
-                                mActivityLaunchAnimator.getLaunchAnimation(
-                                        row, mStatusBar.isOccluded())),
-                                new UserHandle(UserHandle.getUserId(appUid)));
+                ActivityLaunchAnimator.Controller animationController = null;
+                if (!mStatusBar.isOccluded() && mStatusBar.areLaunchAnimationsEnabled()) {
+                    animationController = new StatusBarLaunchAnimatorController(
+                            mNotificationAnimationProvider.getAnimatorController(row), mStatusBar,
+                            true /* isActivityIntent */);
+                }
+
+                mActivityLaunchAnimator.startIntentWithAnimation(
+                        animationController,
+                        (adapter) -> TaskStackBuilder.create(mContext)
+                                .addNextIntentWithParentStack(intent)
+                                .startActivities(getActivityOptions(
+                                        mStatusBar.getDisplayId(),
+                                        adapter),
+                                        new UserHandle(UserHandle.getUserId(appUid))));
+
+                // Note that other cases when we should still collapse (like activity already on
+                // top) is handled by the StatusBarLaunchAnimatorController.
+                boolean shouldCollapse = animationController == null;
 
                 // Putting it back on the main thread, since we're touching views
                 mMainThreadHandler.post(() -> {
-                    mActivityLaunchAnimator.setLaunchResult(launchResult,
-                            true /* isActivityIntent */);
                     removeHUN(row);
-                    if (shouldCollapse()) {
+                    if (shouldCollapse) {
                         mCommandQueue.animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL,
                                 true /* force */);
                     }
@@ -494,11 +507,10 @@
                     tsb.addNextIntent(intent);
                 }
                 tsb.startActivities(null, UserHandle.CURRENT);
-                if (shouldCollapse()) {
-                    // Putting it back on the main thread, since we're touching views
-                    mMainThreadHandler.post(() -> mCommandQueue.animateCollapsePanels(
-                            CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL, true /* force */));
-                }
+
+                // Putting it back on the main thread, since we're touching views
+                mMainThreadHandler.post(() -> mCommandQueue.animateCollapsePanels(
+                        CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL, true /* force */));
             });
             return true;
         }, null, false /* afterKeyguardGone */);
@@ -576,11 +588,6 @@
         }
     }
 
-    private boolean shouldCollapse() {
-        return mStatusBarStateController.getState() != StatusBarState.SHADE
-                || !mActivityLaunchAnimator.isAnimationPending();
-    }
-
     private boolean shouldSuppressFullScreenIntent(NotificationEntry entry) {
         if (mPresenter.isDeviceInVrMode()) {
             return true;
@@ -639,6 +646,7 @@
         private NotificationPresenter mNotificationPresenter;
         private NotificationPanelViewController mNotificationPanelViewController;
         private ActivityLaunchAnimator mActivityLaunchAnimator;
+        private NotificationLaunchAnimatorControllerProvider mNotificationAnimationProvider;
 
         @Inject
         public Builder(
@@ -714,12 +722,20 @@
             return this;
         }
 
+        /** Set the ActivityLaunchAnimator. */
         public Builder setActivityLaunchAnimator(ActivityLaunchAnimator activityLaunchAnimator) {
             mActivityLaunchAnimator = activityLaunchAnimator;
             return this;
         }
 
-        /** Set the NotificationPanelViewController */
+        /** Set the NotificationLaunchAnimatorControllerProvider. */
+        public Builder setNotificationAnimatorControllerProvider(
+                NotificationLaunchAnimatorControllerProvider notificationAnimationProvider) {
+            mNotificationAnimationProvider = notificationAnimationProvider;
+            return this;
+        }
+
+        /** Set the NotificationPanelViewController. */
         public Builder setNotificationPanelViewController(
                 NotificationPanelViewController notificationPanelViewController) {
             mNotificationPanelViewController = notificationPanelViewController;
@@ -759,7 +775,8 @@
                     mStatusBar,
                     mNotificationPresenter,
                     mNotificationPanelViewController,
-                    mActivityLaunchAnimator);
+                    mActivityLaunchAnimator,
+                    mNotificationAnimationProvider);
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
index 94edd1e..088f947 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
@@ -57,7 +57,6 @@
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.notification.AboveShelfObserver;
-import com.android.systemui.statusbar.notification.ActivityLaunchAnimator;
 import com.android.systemui.statusbar.notification.DynamicPrivacyController;
 import com.android.systemui.statusbar.notification.NotificationEntryListener;
 import com.android.systemui.statusbar.notification.NotificationEntryManager;
@@ -117,7 +116,7 @@
 
     private final AccessibilityManager mAccessibilityManager;
     private final KeyguardManager mKeyguardManager;
-    private final ActivityLaunchAnimator mActivityLaunchAnimator;
+    private final NotificationShadeWindowController mNotificationShadeWindowController;
     private final IStatusBarService mBarService;
     private final DynamicPrivacyController mDynamicPrivacyController;
     private boolean mReinflateNotificationsOnUserSwitched;
@@ -133,7 +132,7 @@
             NotificationStackScrollLayoutController stackScrollerController,
             DozeScrimController dozeScrimController,
             ScrimController scrimController,
-            ActivityLaunchAnimator activityLaunchAnimator,
+            NotificationShadeWindowController notificationShadeWindowController,
             DynamicPrivacyController dynamicPrivacyController,
             KeyguardStateController keyguardStateController,
             KeyguardIndicationController keyguardIndicationController,
@@ -152,7 +151,7 @@
         mShadeController = shadeController;
         mCommandQueue = commandQueue;
         mAboveShelfObserver = new AboveShelfObserver(stackScrollerController.getView());
-        mActivityLaunchAnimator = activityLaunchAnimator;
+        mNotificationShadeWindowController = notificationShadeWindowController;
         mAboveShelfObserver.setListener(statusBarWindow.findViewById(
                 R.id.notification_container_parent));
         mAccessibilityManager = context.getSystemService(AccessibilityManager.class);
@@ -272,8 +271,7 @@
     @Override
     public boolean isCollapsing() {
         return mNotificationPanel.isCollapsing()
-                || mActivityLaunchAnimator.isAnimationPending()
-                || mActivityLaunchAnimator.isAnimationRunning();
+                || mNotificationShadeWindowController.isLaunchingActivity();
     }
 
     private void maybeEndAmbientPulse() {
diff --git a/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayout.kt b/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayout.kt
index 603d423..46611e0 100644
--- a/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayout.kt
@@ -50,6 +50,8 @@
 
     private var desiredMeasureWidth = 0
     private var desiredMeasureHeight = 0
+    private var transitionVisibility = View.VISIBLE
+
     /**
      * The measured state of this view which is the one we will lay ourselves out with. This
      * may differ from the currentState if there is an external animation or transition running.
@@ -81,6 +83,13 @@
         }
     }
 
+    override fun setTransitionVisibility(visibility: Int) {
+        // We store the last transition visibility assigned to this view to restore it later if
+        // necessary.
+        super.setTransitionVisibility(visibility)
+        transitionVisibility = visibility
+    }
+
     override fun onFinishInflate() {
         super.onFinishInflate()
         val childCount = childCount
@@ -162,7 +171,16 @@
         updateBounds()
         translationX = currentState.translation.x
         translationY = currentState.translation.y
+
         CrossFadeHelper.fadeIn(this, currentState.alpha)
+
+        // CrossFadeHelper#fadeIn will change this view visibility, which overrides the transition
+        // visibility. We set the transition visibility again to make sure that this view plays well
+        // with GhostView, which sets the transition visibility and is used for activity launch
+        // animations.
+        if (transitionVisibility != View.VISIBLE) {
+            setTransitionVisibility(transitionVisibility)
+        }
     }
 
     private fun applyCurrentStateOnPredraw() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/plugins/animation/ActivityLaunchAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/plugins/animation/ActivityLaunchAnimatorTest.kt
new file mode 100644
index 0000000..722b0b1
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/plugins/animation/ActivityLaunchAnimatorTest.kt
@@ -0,0 +1,188 @@
+package com.android.systemui.plugins.animation
+
+import android.app.ActivityManager
+import android.app.WindowConfiguration
+import android.graphics.Point
+import android.graphics.Rect
+import android.os.Looper
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper.RunWithLooper
+import android.view.IRemoteAnimationFinishedCallback
+import android.view.RemoteAnimationAdapter
+import android.view.RemoteAnimationTarget
+import android.view.SurfaceControl
+import android.view.View
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import junit.framework.Assert.assertFalse
+import junit.framework.Assert.assertNotNull
+import junit.framework.Assert.assertNull
+import junit.framework.Assert.assertTrue
+import junit.framework.AssertionFailedError
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.Spy
+import org.mockito.junit.MockitoJUnit
+import kotlin.concurrent.thread
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@RunWithLooper
+class ActivityLaunchAnimatorTest : SysuiTestCase() {
+    private val activityLaunchAnimator = ActivityLaunchAnimator()
+    private val rootView = View(mContext)
+    @Spy private val controller = TestLaunchAnimatorController(rootView)
+    @Mock lateinit var iCallback: IRemoteAnimationFinishedCallback
+
+    @get:Rule val rule = MockitoJUnit.rule()
+
+    private fun startIntentWithAnimation(
+        controller: ActivityLaunchAnimator.Controller? = this.controller,
+        intentStarter: (RemoteAnimationAdapter?) -> Int
+    ) {
+        // We start in a new thread so that we can ensure that the callbacks are called in the main
+        // thread.
+        thread {
+            activityLaunchAnimator.startIntentWithAnimation(controller, intentStarter)
+        }.join()
+    }
+
+    @Test
+    fun animationAdapterIsNullIfControllerIsNull() {
+        var startedIntent = false
+        var animationAdapter: RemoteAnimationAdapter? = null
+
+        startIntentWithAnimation(controller = null) { adapter ->
+            startedIntent = true
+            animationAdapter = adapter
+
+            ActivityManager.START_SUCCESS
+        }
+
+        assertTrue(startedIntent)
+        assertNull(animationAdapter)
+    }
+
+    @Test
+    fun animatesIfActivityOpens() {
+        val willAnimateCaptor = ArgumentCaptor.forClass(Boolean::class.java)
+        var animationAdapter: RemoteAnimationAdapter? = null
+        startIntentWithAnimation { adapter ->
+            animationAdapter = adapter
+            ActivityManager.START_SUCCESS
+        }
+
+        assertNotNull(animationAdapter)
+        waitForIdleSync()
+        verify(controller).onIntentStarted(willAnimateCaptor.capture())
+        assertTrue(willAnimateCaptor.value)
+    }
+
+    @Test
+    fun doesNotAnimateIfActivityIsAlreadyOpen() {
+        val willAnimateCaptor = ArgumentCaptor.forClass(Boolean::class.java)
+        startIntentWithAnimation { ActivityManager.START_DELIVERED_TO_TOP }
+
+        waitForIdleSync()
+        verify(controller).onIntentStarted(willAnimateCaptor.capture())
+        assertFalse(willAnimateCaptor.value)
+    }
+
+    @Test
+    fun doesNotStartIfAnimationIsCancelled() {
+        val runner = ActivityLaunchAnimator.Runner(controller)
+        runner.onAnimationCancelled()
+        runner.onAnimationStart(0, emptyArray(), emptyArray(), emptyArray(), iCallback)
+
+        waitForIdleSync()
+        verify(controller).onLaunchAnimationCancelled()
+        verify(controller, never()).onLaunchAnimationStart(anyBoolean())
+    }
+
+    @Test
+    fun abortsIfNoOpeningWindowIsFound() {
+        val runner = ActivityLaunchAnimator.Runner(controller)
+        runner.onAnimationStart(0, emptyArray(), emptyArray(), emptyArray(), iCallback)
+
+        waitForIdleSync()
+        verify(controller).onLaunchAnimationAborted()
+        verify(controller, never()).onLaunchAnimationStart(anyBoolean())
+    }
+
+    @Test
+    fun startsAnimationIfWindowIsOpening() {
+        val runner = ActivityLaunchAnimator.Runner(controller)
+        runner.onAnimationStart(0, arrayOf(fakeWindow()), emptyArray(), emptyArray(), iCallback)
+        waitForIdleSync()
+        verify(controller).onLaunchAnimationStart(anyBoolean())
+    }
+
+    private fun fakeWindow() = RemoteAnimationTarget(
+            0, RemoteAnimationTarget.MODE_OPENING, SurfaceControl(), false, Rect(), Rect(), 0,
+            Point(), Rect(), Rect(), WindowConfiguration(), false, SurfaceControl(), Rect(),
+            ActivityManager.RunningTaskInfo()
+    )
+}
+
+/**
+ * A simple implementation of [ActivityLaunchAnimator.Controller] which throws if it is called
+ * outside of the main thread.
+ */
+private class TestLaunchAnimatorController(
+    private val rootView: View
+) : ActivityLaunchAnimator.Controller {
+    override fun getRootView(): View = rootView
+
+    override fun createAnimatorState() = ActivityLaunchAnimator.State(
+            top = 100,
+            bottom = 200,
+            left = 300,
+            right = 400,
+            topCornerRadius = 10f,
+            bottomCornerRadius = 20f
+    )
+
+    private fun assertOnMainThread() {
+        if (Looper.myLooper() != Looper.getMainLooper()) {
+            throw AssertionFailedError("Called outside of main thread.")
+        }
+    }
+
+    override fun onIntentStarted(willAnimate: Boolean) {
+        assertOnMainThread()
+    }
+
+    override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
+        assertOnMainThread()
+    }
+
+    override fun onLaunchAnimationProgress(
+        state: ActivityLaunchAnimator.State,
+        progress: Float,
+        linearProgress: Float
+    ) {
+        assertOnMainThread()
+    }
+
+    override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
+        assertOnMainThread()
+    }
+
+    override fun onLaunchAnimationCancelled() {
+        assertOnMainThread()
+    }
+
+    override fun onLaunchAnimationTimedOut() {
+        assertOnMainThread()
+    }
+
+    override fun onLaunchAnimationAborted() {
+        assertOnMainThread()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt
index e65db5e..fd94812 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt
@@ -27,7 +27,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.plugins.statusbar.StatusBarStateController
-import com.android.systemui.statusbar.notification.ActivityLaunchAnimator
+import com.android.systemui.statusbar.notification.ExpandAnimationParameters
 import com.android.systemui.statusbar.phone.BiometricUnlockController
 import com.android.systemui.statusbar.phone.DozeParameters
 import com.android.systemui.statusbar.policy.KeyguardStateController
@@ -215,7 +215,7 @@
     fun updateBlurCallback_appLaunchAnimation_overridesZoom() {
         `when`(shadeSpring.radius).thenReturn(maxBlur)
         `when`(shadeAnimation.radius).thenReturn(maxBlur)
-        val animProgress = ActivityLaunchAnimator.ExpandAnimationParameters()
+        val animProgress = ExpandAnimationParameters()
         animProgress.linearProgress = 1f
         notificationShadeDepthController.notificationLaunchAnimationParams = animProgress
         notificationShadeDepthController.updateBlurCallback.doFrame(0)
@@ -264,7 +264,7 @@
 
     @Test
     fun setNotificationLaunchAnimationParams_schedulesFrame() {
-        val animProgress = ActivityLaunchAnimator.ExpandAnimationParameters()
+        val animProgress = ExpandAnimationParameters()
         animProgress.linearProgress = 0.5f
         notificationShadeDepthController.notificationLaunchAnimationParams = animProgress
         verify(choreographer).postFrameCallback(
@@ -273,7 +273,7 @@
 
     @Test
     fun setNotificationLaunchAnimationParams_whennNull_ignoresIfShadeHasNoBlur() {
-        val animProgress = ActivityLaunchAnimator.ExpandAnimationParameters()
+        val animProgress = ExpandAnimationParameters()
         animProgress.linearProgress = 0.5f
         `when`(shadeSpring.radius).thenReturn(0)
         `when`(shadeAnimation.radius).thenReturn(0)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/ActivityLaunchAnimatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/ActivityLaunchAnimatorTest.java
deleted file mode 100644
index 2fa6cf0..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/ActivityLaunchAnimatorTest.java
+++ /dev/null
@@ -1,132 +0,0 @@
-/*
- * Copyright (C) 2018 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.statusbar.notification;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyLong;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.app.ActivityManager;
-import android.test.suitebuilder.annotation.SmallTest;
-import android.testing.AndroidTestingRunner;
-import android.testing.TestableLooper;
-import android.view.RemoteAnimationAdapter;
-import android.view.View;
-
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.statusbar.NotificationShadeDepthController;
-import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
-import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
-import com.android.systemui.statusbar.phone.NotificationPanelViewController;
-import com.android.systemui.statusbar.phone.NotificationShadeWindowView;
-import com.android.systemui.statusbar.phone.NotificationShadeWindowViewController;
-import com.android.systemui.util.concurrency.FakeExecutor;
-import com.android.systemui.util.time.FakeSystemClock;
-
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.junit.MockitoJUnit;
-import org.mockito.junit.MockitoRule;
-
-@SmallTest
-@RunWith(AndroidTestingRunner.class)
-@TestableLooper.RunWithLooper
-public class ActivityLaunchAnimatorTest extends SysuiTestCase {
-
-    private final FakeExecutor mExecutor = new FakeExecutor(new FakeSystemClock());
-    private ActivityLaunchAnimator mLaunchAnimator;
-    @Mock
-    private ActivityLaunchAnimator.Callback mCallback;
-    @Mock
-    private NotificationShadeWindowViewController mNotificationShadeWindowViewController;
-    @Mock
-    private NotificationShadeWindowView mNotificationShadeWindowView;
-    @Mock
-    private NotificationListContainer mNotificationContainer;
-    @Mock
-    private ExpandableNotificationRow mRow;
-    @Mock
-    private NotificationShadeDepthController mNotificationShadeDepthController;
-    @Mock
-    private NotificationPanelViewController mNotificationPanelViewController;
-    @Rule
-    public MockitoRule rule = MockitoJUnit.rule();
-
-    @Before
-    public void setUp() throws Exception {
-        when(mNotificationShadeWindowViewController.getView())
-                .thenReturn(mNotificationShadeWindowView);
-        when(mNotificationShadeWindowView.getResources()).thenReturn(mContext.getResources());
-        when(mCallback.areLaunchAnimationsEnabled()).thenReturn(true);
-        mLaunchAnimator = new ActivityLaunchAnimator(
-                mNotificationShadeWindowViewController,
-                mCallback,
-                mNotificationPanelViewController,
-                mNotificationShadeDepthController,
-                mNotificationContainer,
-                mExecutor);
-    }
-
-    @Test
-    public void testReturnsNullIfNotEnabled() {
-        when(mCallback.areLaunchAnimationsEnabled()).thenReturn(false);
-        RemoteAnimationAdapter launchAnimation = mLaunchAnimator.getLaunchAnimation(mRow,
-                false /* occluded */);
-        Assert.assertTrue("The LaunchAnimator generated an animation even though animations are "
-                        + "disabled", launchAnimation == null);
-    }
-
-    @Test
-    public void testNotWorkingWhenOccluded() {
-        when(mCallback.areLaunchAnimationsEnabled()).thenReturn(false);
-        RemoteAnimationAdapter launchAnimation = mLaunchAnimator.getLaunchAnimation(mRow,
-                true /* occluded */);
-        Assert.assertTrue("The LaunchAnimator generated an animation even though we're occluded",
-                launchAnimation == null);
-    }
-
-    @Test
-    public void testTimeoutCalled() {
-        RemoteAnimationAdapter launchAnimation = mLaunchAnimator.getLaunchAnimation(mRow,
-                false /* occluded */);
-        Assert.assertTrue("No animation generated", launchAnimation != null);
-        executePostsImmediately(mNotificationShadeWindowView);
-        mLaunchAnimator.setLaunchResult(ActivityManager.START_SUCCESS,
-                true /* wasIntentActivity */);
-        verify(mCallback).onExpandAnimationTimedOut();
-    }
-
-    private void executePostsImmediately(View view) {
-        doAnswer((i) -> {
-            Runnable run = i.getArgument(0);
-            run.run();
-            return null;
-        }).when(view).post(any());
-        doAnswer((i) -> {
-            Runnable run = i.getArgument(0);
-            run.run();
-            return null;
-        }).when(view).postDelayed(any(), anyLong());
-    }
-}
-
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java
index 68464ce..e34bc0c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java
@@ -35,7 +35,6 @@
 import android.app.KeyguardManager;
 import android.app.Notification;
 import android.app.PendingIntent;
-import android.content.Context;
 import android.content.Intent;
 import android.os.Handler;
 import android.os.RemoteException;
@@ -54,6 +53,7 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.assist.AssistManager;
 import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.plugins.animation.ActivityLaunchAnimator;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.FeatureFlags;
@@ -63,9 +63,9 @@
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
 import com.android.systemui.statusbar.RemoteInputController;
 import com.android.systemui.statusbar.StatusBarState;
-import com.android.systemui.statusbar.notification.ActivityLaunchAnimator;
 import com.android.systemui.statusbar.notification.NotificationActivityStarter;
 import com.android.systemui.statusbar.notification.NotificationEntryManager;
+import com.android.systemui.statusbar.notification.NotificationLaunchAnimatorControllerProvider;
 import com.android.systemui.statusbar.notification.collection.NotifPipeline;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.legacy.NotificationGroupManagerLegacy;
@@ -136,6 +136,8 @@
     private OnUserInteractionCallback mOnUserInteractionCallback;
     @Mock
     private NotificationActivityStarter mNotificationActivityStarter;
+    @Mock
+    private ActivityLaunchAnimator mActivityLaunchAnimator;
     private FakeExecutor mUiBgExecutor = new FakeExecutor(new FakeSystemClock());
 
     private NotificationTestHelper mNotificationTestHelper;
@@ -213,11 +215,14 @@
                         mock(MetricsLogger.class),
                         mock(StatusBarNotificationActivityStarterLogger.class),
                         mOnUserInteractionCallback)
-                .setStatusBar(mStatusBar)
-                .setNotificationPresenter(mock(NotificationPresenter.class))
-                .setNotificationPanelViewController(mock(NotificationPanelViewController.class))
-                .setActivityLaunchAnimator(mock(ActivityLaunchAnimator.class))
-                .build();
+                        .setStatusBar(mStatusBar)
+                        .setNotificationPresenter(mock(NotificationPresenter.class))
+                        .setNotificationPanelViewController(
+                                mock(NotificationPanelViewController.class))
+                        .setActivityLaunchAnimator(mActivityLaunchAnimator)
+                        .setNotificationAnimatorControllerProvider(
+                                mock(NotificationLaunchAnimatorControllerProvider.class))
+                        .build();
 
         // set up dismissKeyguardThenExecute to synchronously invoke the OnDismissAction arg
         doAnswer(mCallOnDismiss).when(mActivityStarter).dismissKeyguardThenExecute(
@@ -254,14 +259,7 @@
         // Then
         verify(mShadeController, atLeastOnce()).collapsePanel();
 
-        verify(mContentIntent).sendAndReturnResult(
-                any(Context.class),
-                anyInt() /* code */,
-                any() /* fillInIntent */,
-                any() /* PendingIntent.OnFinished */,
-                any() /* Handler */,
-                any() /* requiredPermission */,
-                any() /* Bundle options */);
+        verify(mActivityLaunchAnimator).startPendingIntentWithAnimation(eq(null), any());
 
         verify(mAssistManager).hideAssist();
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.java
index c0ebfad..8601de5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.java
@@ -47,7 +47,6 @@
 import com.android.systemui.statusbar.NotificationViewHierarchyManager;
 import com.android.systemui.statusbar.RemoteInputController;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
-import com.android.systemui.statusbar.notification.ActivityLaunchAnimator;
 import com.android.systemui.statusbar.notification.DynamicPrivacyController;
 import com.android.systemui.statusbar.notification.NotificationEntryManager;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
@@ -125,7 +124,7 @@
                 mock(NotificationPanelViewController.class), mock(HeadsUpManagerPhone.class),
                 notificationShadeWindowView, stackScrollLayoutController,
                 mock(DozeScrimController.class), mock(ScrimController.class),
-                mock(ActivityLaunchAnimator.class), mock(DynamicPrivacyController.class),
+                mock(NotificationShadeWindowController.class), mock(DynamicPrivacyController.class),
                 mock(KeyguardStateController.class),
                 mock(KeyguardIndicationController.class), mStatusBar,
                 mock(ShadeControllerImpl.class), mCommandQueue, mInitController,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SmartReplyViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SmartReplyViewTest.java
index 5de62b9..3d07eb1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SmartReplyViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SmartReplyViewTest.java
@@ -703,6 +703,10 @@
     // ============================= Smart Action tests ============================================
     // =============================================================================================
 
+    private View anyView() {
+        return any();
+    }
+
     @Test
     public void testTapSmartAction_waitsForKeyguard() throws InterruptedException {
         setSmartActions(TEST_ACTION_TITLES);
@@ -710,7 +714,7 @@
         mView.getChildAt(2).performClick();
 
         verify(mActivityStarter, times(1)).startPendingIntentDismissingKeyguard(any(), any(),
-                any());
+                anyView());
     }
 
     @Test
@@ -721,7 +725,8 @@
 
         mView.getChildAt(2).performClick();
 
-        verify(mActivityStarter, never()).startPendingIntentDismissingKeyguard(any(), any(), any());
+        verify(mActivityStarter, never()).startPendingIntentDismissingKeyguard(any(), any(),
+                anyView());
     }
 
     @Test
@@ -734,7 +739,7 @@
         mView.getChildAt(2).performClick();
 
         verify(mActivityStarter, times(1))
-                .startPendingIntentDismissingKeyguard(any(), any(), any());
+                .startPendingIntentDismissingKeyguard(any(), any(), anyView());
     }
 
     @Test
@@ -746,7 +751,7 @@
         mView.getChildAt(2).performClick();
 
         verify(mActivityStarter, times(1))
-                .startPendingIntentDismissingKeyguard(any(), any(), any());
+                .startPendingIntentDismissingKeyguard(any(), any(), anyView());
     }
 
     @Test