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
+        }
+    }
+}