Merge changes Ia5accb74,Ic4d7def4,I30bd56fc into sc-v2-dev am: 7339a87bf3

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/16357603

Change-Id: I31d6bbad7f8c03f58ba8b6193aba944f9cdccbbf
diff --git a/packages/SystemUI/animation/res/interpolator/launch_animation_interpolator_x.xml b/packages/SystemUI/animation/res/interpolator/launch_animation_interpolator_x.xml
deleted file mode 100644
index 620dd48..0000000
--- a/packages/SystemUI/animation/res/interpolator/launch_animation_interpolator_x.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-     Copyright (C) 2021 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.
--->
-<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
-    android:pathData="M 0, 0 C 0.1217, 0.0462, 0.15, 0.4686, 0.1667, 0.66 C 0.1834, 0.8878, 0.1667, 1, 1, 1" />
\ No newline at end of file
diff --git a/packages/SystemUI/animation/res/interpolator/launch_animation_interpolator_y.xml b/packages/SystemUI/animation/res/interpolator/launch_animation_interpolator_y.xml
deleted file mode 100644
index a268abc..0000000
--- a/packages/SystemUI/animation/res/interpolator/launch_animation_interpolator_y.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-     Copyright (C) 2021 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.
--->
-<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
-    android:pathData="M 0,0 C 0.05, 0, 0.133333, 0.06, 0.166666, 0.4 C 0.208333, 0.82, 0.25, 1, 1, 1" />
\ No newline at end of file
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt
index fb80f1c..a0d335d 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt
@@ -21,6 +21,7 @@
 import android.app.PendingIntent
 import android.app.TaskInfo
 import android.graphics.Matrix
+import android.graphics.Path
 import android.graphics.Rect
 import android.graphics.RectF
 import android.os.Looper
@@ -34,6 +35,7 @@
 import android.view.View
 import android.view.ViewGroup
 import android.view.WindowManager
+import android.view.animation.Interpolator
 import android.view.animation.PathInterpolator
 import com.android.internal.annotations.VisibleForTesting
 import com.android.internal.policy.ScreenDecorationsUtils
@@ -45,16 +47,46 @@
  * 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(private val launchAnimator: LaunchAnimator) {
+class ActivityLaunchAnimator(
+    private val launchAnimator: LaunchAnimator = LaunchAnimator(TIMINGS, INTERPOLATORS)
+) {
     companion object {
+        @JvmField
+        val TIMINGS = LaunchAnimator.Timings(
+            totalDuration = 500L,
+            contentBeforeFadeOutDelay = 0L,
+            contentBeforeFadeOutDuration = 150L,
+            contentAfterFadeInDelay = 150L,
+            contentAfterFadeInDuration = 183L
+        )
+
+        val INTERPOLATORS = LaunchAnimator.Interpolators(
+            positionInterpolator = Interpolators.EMPHASIZED,
+            positionXInterpolator = createPositionXInterpolator(),
+            contentBeforeFadeOutInterpolator = Interpolators.LINEAR_OUT_SLOW_IN,
+            contentAfterFadeInInterpolator = PathInterpolator(0f, 0f, 0.6f, 1f)
+        )
+
+        /** Durations & interpolators for the navigation bar fading in & out. */
         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 =
-            LaunchAnimator.ANIMATION_DURATION - ANIMATION_DURATION_NAV_FADE_IN
+        private val ANIMATION_DELAY_NAV_FADE_IN =
+            TIMINGS.totalDuration - ANIMATION_DURATION_NAV_FADE_IN
+
+        private val NAV_FADE_IN_INTERPOLATOR = Interpolators.STANDARD_DECELERATE
+        private val NAV_FADE_OUT_INTERPOLATOR = PathInterpolator(0.2f, 0f, 1f, 1f)
+
+        /** The time we wait before timing out the remote animation after starting the intent. */
         private const val LAUNCH_TIMEOUT = 1000L
 
-        private val NAV_FADE_IN_INTERPOLATOR = PathInterpolator(0f, 0f, 0f, 1f)
-        private val NAV_FADE_OUT_INTERPOLATOR = PathInterpolator(0.2f, 0f, 1f, 1f)
+        private fun createPositionXInterpolator(): Interpolator {
+            val path = Path().apply {
+                moveTo(0f, 0f)
+                cubicTo(0.1217f, 0.0462f, 0.15f, 0.4686f, 0.1667f, 0.66f)
+                cubicTo(0.1834f, 0.8878f, 0.1667f, 1f, 1f, 1f)
+            }
+            return PathInterpolator(path)
+        }
     }
 
     /**
@@ -107,8 +139,8 @@
         val animationAdapter = if (!hideKeyguardWithAnimation) {
             RemoteAnimationAdapter(
                 runner,
-                LaunchAnimator.ANIMATION_DURATION,
-                LaunchAnimator.ANIMATION_DURATION - 150 /* statusBarTransitionDelay */
+                TIMINGS.totalDuration,
+                TIMINGS.totalDuration - 150 /* statusBarTransitionDelay */
             )
         } else {
             null
@@ -448,7 +480,7 @@
             state: LaunchAnimator.State,
             linearProgress: Float
         ) {
-            val fadeInProgress = LaunchAnimator.getProgress(linearProgress,
+            val fadeInProgress = LaunchAnimator.getProgress(TIMINGS, linearProgress,
                 ANIMATION_DELAY_NAV_FADE_IN, ANIMATION_DURATION_NAV_FADE_OUT)
 
             val params = SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(navigationBar.leash)
@@ -463,7 +495,7 @@
                     .withWindowCrop(windowCrop)
                     .withVisibility(true)
             } else {
-                val fadeOutProgress = LaunchAnimator.getProgress(linearProgress, 0,
+                val fadeOutProgress = LaunchAnimator.getProgress(TIMINGS, linearProgress, 0,
                     ANIMATION_DURATION_NAV_FADE_OUT)
                 params.withAlpha(1f - NAV_FADE_OUT_INTERPOLATOR.getInterpolation(fadeOutProgress))
             }
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
index de82ebd..066e169 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
@@ -20,7 +20,6 @@
 import android.animation.AnimatorListenerAdapter
 import android.animation.ValueAnimator
 import android.app.Dialog
-import android.content.Context
 import android.graphics.Color
 import android.graphics.Rect
 import android.os.Looper
@@ -28,10 +27,11 @@
 import android.util.Log
 import android.util.MathUtils
 import android.view.GhostView
+import android.view.SurfaceControl
 import android.view.View
 import android.view.ViewGroup
 import android.view.ViewGroup.LayoutParams.MATCH_PARENT
-import android.view.ViewTreeObserver.OnPreDrawListener
+import android.view.ViewRootImpl
 import android.view.WindowManager
 import android.widget.FrameLayout
 import kotlin.math.roundToInt
@@ -42,12 +42,20 @@
  * A class that allows dialogs to be started in a seamless way from a view that is transforming
  * nicely into the starting dialog.
  */
-class DialogLaunchAnimator(
-    private val context: Context,
-    private val launchAnimator: LaunchAnimator,
-    private val dreamManager: IDreamManager
+class DialogLaunchAnimator @JvmOverloads constructor(
+    private val dreamManager: IDreamManager,
+    private val launchAnimator: LaunchAnimator = LaunchAnimator(TIMINGS, INTERPOLATORS),
+    private var isForTesting: Boolean = false
 ) {
     private companion object {
+        private val TIMINGS = ActivityLaunchAnimator.TIMINGS
+
+        // We use the same interpolator for X and Y axis to make sure the dialog does not move out
+        // of the screen bounds during the animation.
+        private val INTERPOLATORS = ActivityLaunchAnimator.INTERPOLATORS.copy(
+            positionXInterpolator = ActivityLaunchAnimator.INTERPOLATORS.positionInterpolator
+        )
+
         private val TAG_LAUNCH_ANIMATION_RUNNING = R.id.launch_animation_running
     }
 
@@ -96,14 +104,14 @@
         animateFrom.setTag(TAG_LAUNCH_ANIMATION_RUNNING, true)
 
         val animatedDialog = AnimatedDialog(
-                context,
                 launchAnimator,
                 dreamManager,
                 animateFrom,
                 onDialogDismissed = { openedDialogs.remove(it) },
                 dialog = dialog,
                 animateBackgroundBoundsChange,
-                animatedParent
+                animatedParent,
+                isForTesting
         )
 
         openedDialogs.add(animatedDialog)
@@ -157,7 +165,6 @@
 }
 
 private class AnimatedDialog(
-    private val context: Context,
     private val launchAnimator: LaunchAnimator,
     private val dreamManager: IDreamManager,
 
@@ -174,10 +181,16 @@
     val dialog: Dialog,
 
     /** Whether we should animate the dialog background when its bounds change. */
-    private val animateBackgroundBoundsChange: Boolean,
+    animateBackgroundBoundsChange: Boolean,
 
     /** Launch animation corresponding to the parent [AnimatedDialog]. */
-    private val parentAnimatedDialog: AnimatedDialog? = null
+    private val parentAnimatedDialog: AnimatedDialog? = null,
+
+    /**
+     * Whether we are currently running in a test, in which case we need to disable
+     * synchronization.
+     */
+    private val isForTesting: Boolean
 ) {
     /**
      * The DecorView of this dialog window.
@@ -266,14 +279,14 @@
             // and the view that we added so that we can dismiss the dialog when this view is
             // clicked. This is necessary because DecorView overrides onTouchEvent and therefore we
             // can't set the click listener directly on the (now fullscreen) DecorView.
-            val fullscreenTransparentBackground = FrameLayout(context)
+            val fullscreenTransparentBackground = FrameLayout(dialog.context)
             decorView.addView(
                 fullscreenTransparentBackground,
                 0 /* index */,
                 FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
             )
 
-            val dialogContentWithBackground = FrameLayout(context)
+            val dialogContentWithBackground = FrameLayout(dialog.context)
             dialogContentWithBackground.background = decorView.background
 
             // Make the window background transparent. Note that setting the window (or DecorView)
@@ -365,59 +378,77 @@
         // Show the dialog.
         dialog.show()
 
-        // Add a temporary touch surface ghost as soon as the window is ready to draw. This
-        // temporary ghost will be drawn together with the touch surface, but in the dialog
-        // window. Once it is drawn, we will make the touch surface invisible, and then start the
-        // animation. We do all this synchronization to avoid flicker that would occur if we made
-        // the touch surface invisible too early (before its ghost is drawn), leading to one or more
-        // frames with a hole instead of the touch surface (or its ghost).
-        decorView.viewTreeObserver.addOnPreDrawListener(object : OnPreDrawListener {
-            override fun onPreDraw(): Boolean {
-                decorView.viewTreeObserver.removeOnPreDrawListener(this)
-                addTemporaryTouchSurfaceGhost()
-                return true
-            }
-        })
-        decorView.invalidate()
+        addTouchSurfaceGhost()
     }
 
-    private fun addTemporaryTouchSurfaceGhost() {
+    private fun addTouchSurfaceGhost() {
+        if (decorView.viewRootImpl == null) {
+            // Make sure that we have access to the dialog view root to synchronize the creation of
+            // the ghost.
+            decorView.post(::addTouchSurfaceGhost)
+            return
+        }
+
         // Create a ghost of the touch surface (which will make the touch surface invisible) and add
-        // it to the dialog. We will wait for this ghost to be drawn before starting the animation.
-        val ghost = GhostView.addGhost(touchSurface, decorView)
-
-        // The ghost of the touch surface was just created, so the touch surface was made invisible.
-        // We make it visible again until the ghost is actually drawn.
-        touchSurface.visibility = View.VISIBLE
-
-        // Wait for the ghost to be drawn before continuing.
-        ghost.viewTreeObserver.addOnPreDrawListener(object : OnPreDrawListener {
-            override fun onPreDraw(): Boolean {
-                ghost.viewTreeObserver.removeOnPreDrawListener(this)
-                onTouchSurfaceGhostDrawn()
-                return true
-            }
+        // it to the host dialog. We trigger a one off synchronization to make sure that this is
+        // done in sync between the two different windows.
+        synchronizeNextDraw(then = {
+            isTouchSurfaceGhostDrawn = true
+            maybeStartLaunchAnimation()
         })
-        ghost.invalidate()
+        GhostView.addGhost(touchSurface, decorView)
+
+        // The ghost of the touch surface was just created, so the touch surface is currently
+        // invisible. We need to make sure that it stays invisible as long as the dialog is shown or
+        // animating.
+        (touchSurface as? LaunchableView)?.setShouldBlockVisibilityChanges(true)
     }
 
-    private fun onTouchSurfaceGhostDrawn() {
-        // Make the touch surface invisible and make sure that it stays invisible as long as the
-        // dialog is shown or animating.
-        touchSurface.visibility = View.INVISIBLE
-        (touchSurface as? LaunchableView)?.setShouldBlockVisibilityChanges(true)
+    /**
+     * Synchronize the next draw of the touch surface and dialog view roots so that they are
+     * performed at the same time, in the same transaction. This is necessary to make sure that the
+     * ghost of the touch surface is drawn at the same time as the touch surface is made invisible
+     * (or inversely, removed from the UI when the touch surface is made visible).
+     */
+    private fun synchronizeNextDraw(then: () -> Unit) {
+        if (isForTesting || !touchSurface.isAttachedToWindow || touchSurface.viewRootImpl == null ||
+            !decorView.isAttachedToWindow || decorView.viewRootImpl == null) {
+            // No need to synchronize if either the touch surface or dialog view is not attached
+            // to a window.
+            then()
+            return
+        }
 
-        // Add a pre draw listener to (maybe) start the animation once the touch surface is
-        // actually invisible.
-        touchSurface.viewTreeObserver.addOnPreDrawListener(object : OnPreDrawListener {
-            override fun onPreDraw(): Boolean {
-                touchSurface.viewTreeObserver.removeOnPreDrawListener(this)
-                isTouchSurfaceGhostDrawn = true
-                maybeStartLaunchAnimation()
-                return true
+        // Consume the next frames of both view roots to make sure the ghost view is drawn at
+        // exactly the same time as when the touch surface is made invisible.
+        var remainingTransactions = 0
+        val mergedTransactions = SurfaceControl.Transaction()
+
+        fun onTransaction(transaction: SurfaceControl.Transaction?) {
+            remainingTransactions--
+            transaction?.let { mergedTransactions.merge(it) }
+
+            if (remainingTransactions == 0) {
+                mergedTransactions.apply()
+                then()
             }
-        })
-        touchSurface.invalidate()
+        }
+
+        fun consumeNextDraw(viewRootImpl: ViewRootImpl) {
+            if (viewRootImpl.consumeNextDraw(::onTransaction)) {
+                remainingTransactions++
+
+                // Make sure we trigger a traversal.
+                viewRootImpl.view.invalidate()
+            }
+        }
+
+        consumeNextDraw(touchSurface.viewRootImpl)
+        consumeNextDraw(decorView.viewRootImpl)
+
+        if (remainingTransactions == 0) {
+            then()
+        }
     }
 
     private fun findFirstViewGroupWithBackground(view: View): ViewGroup? {
@@ -483,7 +514,7 @@
 
     private fun onDialogDismissed() {
         if (Looper.myLooper() != Looper.getMainLooper()) {
-            context.mainExecutor.execute { onDialogDismissed() }
+            dialog.context.mainExecutor.execute { onDialogDismissed() }
             return
         }
 
@@ -556,25 +587,12 @@
                         .removeOnLayoutChangeListener(backgroundLayoutListener)
                 }
 
-                // The animated ghost was just removed. We create a temporary ghost that will be
-                // removed only once we draw the touch surface, to avoid flickering that would
-                // happen when removing the ghost too early (before the touch surface is drawn).
-                GhostView.addGhost(touchSurface, decorView)
-
-                touchSurface.viewTreeObserver.addOnPreDrawListener(object : OnPreDrawListener {
-                    override fun onPreDraw(): Boolean {
-                        touchSurface.viewTreeObserver.removeOnPreDrawListener(this)
-
-                        // Now that the touch surface was drawn, we can remove the temporary ghost
-                        // and instantly dismiss the dialog.
-                        GhostView.removeGhost(touchSurface)
-                        onAnimationFinished(true /* instantDismiss */)
-                        onDialogDismissed(this@AnimatedDialog)
-
-                        return true
-                    }
+                // Make sure that the removal of the ghost and making the touch surface visible is
+                // done at the same time.
+                synchronizeNextDraw(then = {
+                    onAnimationFinished(true /* instantDismiss */)
+                    onDialogDismissed(this@AnimatedDialog)
                 })
-                touchSurface.invalidate()
             }
         )
     }
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchAnimator.kt
index 3bf6c5e..ebe96eb 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchAnimator.kt
@@ -27,25 +27,19 @@
 import android.util.MathUtils
 import android.view.View
 import android.view.ViewGroup
-import android.view.animation.AnimationUtils
-import android.view.animation.PathInterpolator
+import android.view.animation.Interpolator
+import com.android.systemui.animation.Interpolators.LINEAR
 import kotlin.math.roundToInt
 
 private const val TAG = "LaunchAnimator"
 
 /** A base class to animate a window launch (activity or dialog) from a view . */
-class LaunchAnimator @JvmOverloads constructor(
-    context: Context,
-    private val isForTesting: Boolean = false
+class LaunchAnimator(
+    private val timings: Timings,
+    private val interpolators: Interpolators
 ) {
     companion object {
         internal const val DEBUG = false
-        const val ANIMATION_DURATION = 500L
-        private const val ANIMATION_DURATION_FADE_OUT_CONTENT = 150L
-        private const val ANIMATION_DURATION_FADE_IN_WINDOW = 183L
-        private const val ANIMATION_DELAY_FADE_IN_WINDOW = ANIMATION_DURATION_FADE_OUT_CONTENT
-
-        private val WINDOW_FADE_IN_INTERPOLATOR = PathInterpolator(0f, 0f, 0.6f, 1f)
         private val SRC_MODE = PorterDuffXfermode(PorterDuff.Mode.SRC)
 
         /**
@@ -53,23 +47,20 @@
          * sub-animation starting [delay] ms after the launch animation and that lasts [duration].
          */
         @JvmStatic
-        fun getProgress(linearProgress: Float, delay: Long, duration: Long): Float {
+        fun getProgress(
+            timings: Timings,
+            linearProgress: Float,
+            delay: Long,
+            duration: Long
+        ): Float {
             return MathUtils.constrain(
-                (linearProgress * ANIMATION_DURATION - delay) / duration,
+                (linearProgress * timings.totalDuration - delay) / duration,
                 0.0f,
                 1.0f
             )
         }
     }
 
-    /** The interpolator used for the width, height, Y position and corner radius. */
-    private val animationInterpolator = AnimationUtils.loadInterpolator(context,
-        R.interpolator.launch_animation_interpolator_y)
-
-    /** The interpolator used for the X position. */
-    private val animationInterpolatorX = AnimationUtils.loadInterpolator(context,
-        R.interpolator.launch_animation_interpolator_x)
-
     private val launchContainerLocation = IntArray(2)
     private val cornerRadii = FloatArray(8)
 
@@ -159,6 +150,45 @@
         fun cancel()
     }
 
+    /** The timings (durations and delays) used by this animator. */
+    class Timings(
+        /** The total duration of the animation. */
+        val totalDuration: Long,
+
+        /** The time to wait before fading out the expanding content. */
+        val contentBeforeFadeOutDelay: Long,
+
+        /** The duration of the expanding content fade out. */
+        val contentBeforeFadeOutDuration: Long,
+
+        /**
+         * The time to wait before fading in the expanded content (usually an activity or dialog
+         * window).
+         */
+        val contentAfterFadeInDelay: Long,
+
+        /** The duration of the expanded content fade in. */
+        val contentAfterFadeInDuration: Long
+    )
+
+    /** The interpolators used by this animator. */
+    data class Interpolators(
+        /** The interpolator used for the Y position, width, height and corner radius. */
+        val positionInterpolator: Interpolator,
+
+        /**
+         * The interpolator used for the X position. This can be different than
+         * [positionInterpolator] to create an arc-path during the animation.
+         */
+        val positionXInterpolator: Interpolator = positionInterpolator,
+
+        /** The interpolator used when fading out the expanding content. */
+        val contentBeforeFadeOutInterpolator: Interpolator,
+
+        /** The interpolator used when fading in the expanded content. */
+        val contentAfterFadeInInterpolator: Interpolator
+    )
+
     /**
      * Start a launch animation controlled by [controller] towards [endState]. An intermediary
      * layer with [windowBackgroundColor] will fade in then fade out above the expanding view, and
@@ -221,8 +251,8 @@
 
         // Update state.
         val animator = ValueAnimator.ofFloat(0f, 1f)
-        animator.duration = if (isForTesting) 0 else ANIMATION_DURATION
-        animator.interpolator = Interpolators.LINEAR
+        animator.duration = timings.totalDuration
+        animator.interpolator = LINEAR
 
         val launchContainerOverlay = launchContainer.overlay
         var cancelled = false
@@ -260,8 +290,8 @@
             // TODO(b/184121838): Use reverse interpolators to get the same path/arc as the non
             // reversed animation.
             val linearProgress = animation.animatedFraction
-            val progress = animationInterpolator.getInterpolation(linearProgress)
-            val xProgress = animationInterpolatorX.getInterpolation(linearProgress)
+            val progress = interpolators.positionInterpolator.getInterpolation(linearProgress)
+            val xProgress = interpolators.positionXInterpolator.getInterpolation(linearProgress)
 
             val xCenter = MathUtils.lerp(startCenterX, endCenterX, xProgress)
             val halfWidth = MathUtils.lerp(startWidth, endWidth, progress) / 2f
@@ -278,7 +308,12 @@
 
             // The expanding view can/should be hidden once it is completely covered by the opening
             // window.
-            state.visible = getProgress(linearProgress, 0, ANIMATION_DURATION_FADE_OUT_CONTENT) < 1
+            state.visible = getProgress(
+                timings,
+                linearProgress,
+                timings.contentBeforeFadeOutDelay,
+                timings.contentBeforeFadeOutDuration
+            ) < 1
 
             applyStateToWindowBackgroundLayer(
                 windowBackgroundLayer,
@@ -337,14 +372,25 @@
 
         // We first fade in the background layer to hide the expanding view, then fade it out
         // with SRC mode to draw a hole punch in the status bar and reveal the opening window.
-        val fadeInProgress = getProgress(linearProgress, 0, ANIMATION_DURATION_FADE_OUT_CONTENT)
+        val fadeInProgress = getProgress(
+            timings,
+            linearProgress,
+            timings.contentBeforeFadeOutDelay,
+            timings.contentBeforeFadeOutDuration
+        )
         if (fadeInProgress < 1) {
-            val alpha = Interpolators.LINEAR_OUT_SLOW_IN.getInterpolation(fadeInProgress)
+            val alpha =
+                interpolators.contentBeforeFadeOutInterpolator.getInterpolation(fadeInProgress)
             drawable.alpha = (alpha * 0xFF).roundToInt()
         } else {
             val fadeOutProgress = getProgress(
-                linearProgress, ANIMATION_DELAY_FADE_IN_WINDOW, ANIMATION_DURATION_FADE_IN_WINDOW)
-            val alpha = 1 - WINDOW_FADE_IN_INTERPOLATOR.getInterpolation(fadeOutProgress)
+                timings,
+                linearProgress,
+                timings.contentAfterFadeInDelay,
+                timings.contentAfterFadeInDuration
+            )
+            val alpha =
+                1 - interpolators.contentAfterFadeInInterpolator.getInterpolation(fadeOutProgress)
             drawable.alpha = (alpha * 0xFF).roundToInt()
 
             if (drawHole) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java
index 1d92170..f2d926d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java
@@ -25,7 +25,6 @@
 import com.android.internal.statusbar.IStatusBarService;
 import com.android.systemui.animation.ActivityLaunchAnimator;
 import com.android.systemui.animation.DialogLaunchAnimator;
-import com.android.systemui.animation.LaunchAnimator;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpManager;
@@ -301,24 +300,15 @@
      */
     @Provides
     @SysUISingleton
-    static LaunchAnimator provideLaunchAnimator(Context context) {
-        return new LaunchAnimator(context);
+    static ActivityLaunchAnimator provideActivityLaunchAnimator() {
+        return new ActivityLaunchAnimator();
     }
 
     /**
      */
     @Provides
     @SysUISingleton
-    static ActivityLaunchAnimator provideActivityLaunchAnimator(LaunchAnimator launchAnimator) {
-        return new ActivityLaunchAnimator(launchAnimator);
-    }
-
-    /**
-     */
-    @Provides
-    @SysUISingleton
-    static DialogLaunchAnimator provideDialogLaunchAnimator(Context context,
-            LaunchAnimator launchAnimator, IDreamManager dreamManager) {
-        return new DialogLaunchAnimator(context, launchAnimator, dreamManager);
+    static DialogLaunchAnimator provideDialogLaunchAnimator(IDreamManager dreamManager) {
+        return new DialogLaunchAnimator(dreamManager);
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ExpandAnimationParameters.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ExpandAnimationParameters.kt
index 64a7305..349b191 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ExpandAnimationParameters.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ExpandAnimationParameters.kt
@@ -2,6 +2,7 @@
 
 import android.util.MathUtils
 import com.android.internal.annotations.VisibleForTesting
+import com.android.systemui.animation.ActivityLaunchAnimator
 import com.android.systemui.animation.Interpolators
 import com.android.systemui.animation.LaunchAnimator
 import kotlin.math.min
@@ -55,6 +56,7 @@
         }
 
     fun getProgress(delay: Long, duration: Long): Float {
-        return LaunchAnimator.getProgress(linearProgress, delay, duration)
+        return LaunchAnimator.getProgress(ActivityLaunchAnimator.TIMINGS, linearProgress, delay,
+            duration)
     }
 }
\ No newline at end of file
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 20a771f..261b5db 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
@@ -110,6 +110,7 @@
 import com.android.systemui.DejankUtils;
 import com.android.systemui.Dependency;
 import com.android.systemui.R;
+import com.android.systemui.animation.ActivityLaunchAnimator;
 import com.android.systemui.animation.Interpolators;
 import com.android.systemui.animation.LaunchAnimator;
 import com.android.systemui.biometrics.AuthController;
@@ -225,7 +226,8 @@
      */
     private static final int FLING_HIDE = 2;
     private static final long ANIMATION_DELAY_ICON_FADE_IN =
-            LaunchAnimator.ANIMATION_DURATION - CollapsedStatusBarFragment.FADE_IN_DURATION
+            ActivityLaunchAnimator.TIMINGS.getTotalDuration()
+                    - CollapsedStatusBarFragment.FADE_IN_DURATION
                     - CollapsedStatusBarFragment.FADE_IN_DELAY - 48;
 
     private final DozeParameters mDozeParameters;
@@ -3629,8 +3631,8 @@
     }
 
     public void applyLaunchAnimationProgress(float linearProgress) {
-        boolean hideIcons = LaunchAnimator.getProgress(linearProgress,
-                ANIMATION_DELAY_ICON_FADE_IN, 100) == 0.0f;
+        boolean hideIcons = LaunchAnimator.getProgress(ActivityLaunchAnimator.TIMINGS,
+                linearProgress, ANIMATION_DELAY_ICON_FADE_IN, 100) == 0.0f;
         if (hideIcons != mHideIconsDuringLaunchAnimation) {
             mHideIconsDuringLaunchAnimation = hideIcons;
             if (!hideIcons) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt
index 32aae6c..2ba37c2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt
@@ -23,7 +23,8 @@
         delegate.onLaunchAnimationStart(isExpandingFullyAbove)
         statusBar.notificationPanelViewController.setIsLaunchAnimationRunning(true)
         if (!isExpandingFullyAbove) {
-            statusBar.collapsePanelWithDuration(LaunchAnimator.ANIMATION_DURATION.toInt())
+            statusBar.collapsePanelWithDuration(
+                ActivityLaunchAnimator.TIMINGS.totalDuration.toInt())
         }
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt
index d819fa2..1fe3d44 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt
@@ -46,7 +46,7 @@
 @RunWithLooper
 class ActivityLaunchAnimatorTest : SysuiTestCase() {
     private val launchContainer = LinearLayout(mContext)
-    private val launchAnimator = LaunchAnimator(mContext, isForTesting = true)
+    private val launchAnimator = LaunchAnimator(TEST_TIMINGS, TEST_INTERPOLATORS)
     @Mock lateinit var callback: ActivityLaunchAnimator.Callback
     @Spy private val controller = TestLaunchAnimatorController(launchContainer)
     @Mock lateinit var iCallback: IRemoteAnimationFinishedCallback
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt
index f9ad740..b951345 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt
@@ -33,7 +33,7 @@
 @RunWith(AndroidTestingRunner::class)
 @TestableLooper.RunWithLooper
 class DialogLaunchAnimatorTest : SysuiTestCase() {
-    private val launchAnimator = LaunchAnimator(context, isForTesting = true)
+    private val launchAnimator = LaunchAnimator(TEST_TIMINGS, TEST_INTERPOLATORS)
     private lateinit var dialogLaunchAnimator: DialogLaunchAnimator
     private val attachedViews = mutableSetOf<View>()
 
@@ -42,7 +42,8 @@
 
     @Before
     fun setUp() {
-        dialogLaunchAnimator = DialogLaunchAnimator(context, launchAnimator, dreamManager)
+        dialogLaunchAnimator = DialogLaunchAnimator(
+            dreamManager, launchAnimator, isForTesting = true)
     }
 
     @After
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/TestValues.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/TestValues.kt
new file mode 100644
index 0000000..dadf94e
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/TestValues.kt
@@ -0,0 +1,23 @@
+package com.android.systemui.animation
+
+/**
+ * A [LaunchAnimator.Timings] to be used in tests.
+ *
+ * Note that all timings except the total duration are non-zero to avoid divide-by-zero exceptions
+ * when computing the progress of a sub-animation (the contents fade in/out).
+ */
+val TEST_TIMINGS = LaunchAnimator.Timings(
+    totalDuration = 0L,
+    contentBeforeFadeOutDelay = 1L,
+    contentBeforeFadeOutDuration = 1L,
+    contentAfterFadeInDelay = 1L,
+    contentAfterFadeInDuration = 1L
+)
+
+/** A [LaunchAnimator.Interpolators] to be used in tests. */
+val TEST_INTERPOLATORS = LaunchAnimator.Interpolators(
+    positionInterpolator = Interpolators.STANDARD,
+    positionXInterpolator = Interpolators.STANDARD,
+    contentBeforeFadeOutInterpolator = Interpolators.STANDARD,
+    contentAfterFadeInInterpolator = Interpolators.STANDARD
+)
\ No newline at end of file