Add return animation functionality to TransitionAnimator.

Bug: 323863002
Flag: ACONFIG com.android.systemui.shared.return_animation_framework_library DISABLED
Test: Manually tested existing use cases with the flag on and off (same
behavior), added unit test

Change-Id: I391c6ae784871e17262649d0f17343ac87b6a6d4
diff --git a/packages/SystemUI/animation/Android.bp b/packages/SystemUI/animation/Android.bp
index 2268d16..dce2c65 100644
--- a/packages/SystemUI/animation/Android.bp
+++ b/packages/SystemUI/animation/Android.bp
@@ -48,6 +48,7 @@
         "SystemUIShaderLib",
         "WindowManager-Shell-shared",
         "animationlib",
+        "com_android_systemui_shared_flags_lib",
     ],
 
     manifest: "AndroidManifest.xml",
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt
index ea1cb34..9ce30fd 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt
@@ -790,7 +790,7 @@
                     controller,
                     endState,
                     windowBackgroundColor,
-                    fadeOutWindowBackgroundLayer = !controller.isBelowAnimatingWindow,
+                    fadeWindowBackgroundLayer = !controller.isBelowAnimatingWindow,
                     drawHole = !controller.isBelowAnimatingWindow,
                 )
         }
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogTransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogTransitionAnimator.kt
index dbdf970..20087c4 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogTransitionAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogTransitionAnimator.kt
@@ -917,6 +917,12 @@
                         endController.transitionContainer = value
                     }
 
+                // We tell TransitionController that this is always a launch, and handle the launch
+                // vs return logic internally.
+                // TODO(b/323863002): maybe move the launch vs return logic out of this class and
+                //     delegate it to TransitionController?
+                override val isLaunching: Boolean = true
+
                 override fun createAnimatorState(): TransitionAnimator.State {
                     return startController.createAnimatorState()
                 }
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewTransitionAnimatorController.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewTransitionAnimatorController.kt
index 3f57f88..9ad0fc5 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewTransitionAnimatorController.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewTransitionAnimatorController.kt
@@ -64,6 +64,7 @@
     private var interactionJankMonitor: InteractionJankMonitor =
         InteractionJankMonitor.getInstance(),
 ) : ActivityTransitionAnimator.Controller {
+    override val isLaunching: Boolean = true
 
     /** The container to which we will add the ghost view and expanding background. */
     override var transitionContainer = ghostedView.rootView as ViewGroup
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt
index 5e4276c..9bf6b34 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt
@@ -28,7 +28,9 @@
 import android.view.View
 import android.view.ViewGroup
 import android.view.animation.Interpolator
+import androidx.annotation.VisibleForTesting
 import com.android.app.animation.Interpolators.LINEAR
+import com.android.systemui.shared.Flags.returnAnimationFrameworkLibrary
 import kotlin.math.roundToInt
 
 private const val TAG = "TransitionAnimator"
@@ -70,13 +72,14 @@
     interface Controller {
         /**
          * The container in which the view that started the animation will be animating together
-         * with the opening window.
+         * with the opening or closing window.
          *
          * This will be used to:
          * - Get the associated [Context].
-         * - Compute whether we are expanding fully above the transition container.
-         * - Get to overlay to which we initially put the window background layer, until the opening
-         *   window is made visible (see [openingWindowSyncView]).
+         * - Compute whether we are expanding to or contracting from fully above the transition
+         *   container.
+         * - Get the overlay into which we put the window background layer, while the animating
+         *   window is not visible (see [openingWindowSyncView]).
          *
          * This container can be changed to force this [Controller] to animate the expanding view
          * inside a different location, for instance to ensure correct layering during the
@@ -84,12 +87,17 @@
          */
         var transitionContainer: ViewGroup
 
+        /** Whether the animation being controlled is a launch or a return. */
+        val isLaunching: Boolean
+
         /**
-         * The [View] with which the opening app window should be synchronized with once it starts
-         * to be visible.
+         * If [isLaunching], the [View] with which the opening app window should be synchronized
+         * once it starts to be visible. Otherwise, the [View] with which the closing app window
+         * should be synchronized until it stops being visible.
          *
          * We will also move the window background layer to this view's overlay once the opening
-         * window is visible.
+         * window is visible (if [isLaunching]), or from this view's overlay once the closing window
+         * stop being visible (if ![isLaunching]).
          *
          * If null, this will default to [transitionContainer].
          */
@@ -203,17 +211,56 @@
      * layer with [windowBackgroundColor] will fade in then (optionally) fade out above the
      * expanding view, and should be the same background color as the opening (or closing) window.
      *
-     * If [fadeOutWindowBackgroundLayer] is true, then this intermediary layer will fade out during
-     * the second half of the animation, and will have SRC blending mode (ultimately punching a hole
-     * in the [transition container][Controller.transitionContainer]) iff [drawHole] is true.
+     * If [fadeWindowBackgroundLayer] is true, then this intermediary layer will fade out during the
+     * second half of the animation (if [Controller.isLaunching] or fade in during the first half of
+     * the animation (if ![Controller.isLaunching]), and will have SRC blending mode (ultimately
+     * punching a hole in the [transition container][Controller.transitionContainer]) iff [drawHole]
+     * is true.
      */
     fun startAnimation(
         controller: Controller,
         endState: State,
         windowBackgroundColor: Int,
-        fadeOutWindowBackgroundLayer: Boolean = true,
+        fadeWindowBackgroundLayer: Boolean = true,
         drawHole: Boolean = false,
     ): Animation {
+        if (!controller.isLaunching) checkReturnAnimationFrameworkFlag()
+
+        // We add an extra layer with the same color as the dialog/app splash screen background
+        // color, which is usually the same color of the app background. We first fade in this layer
+        // to hide the expanding view, then we fade it out with SRC mode to draw a hole in the
+        // transition container and reveal the opening window.
+        val windowBackgroundLayer =
+            GradientDrawable().apply {
+                setColor(windowBackgroundColor)
+                alpha = 0
+            }
+
+        val animator =
+            createAnimator(
+                controller,
+                endState,
+                windowBackgroundLayer,
+                fadeWindowBackgroundLayer,
+                drawHole
+            )
+        animator.start()
+
+        return object : Animation {
+            override fun cancel() {
+                animator.cancel()
+            }
+        }
+    }
+
+    @VisibleForTesting
+    fun createAnimator(
+        controller: Controller,
+        endState: State,
+        windowBackgroundLayer: GradientDrawable,
+        fadeWindowBackgroundLayer: Boolean = true,
+        drawHole: Boolean = false
+    ): ValueAnimator {
         val state = controller.createAnimatorState()
 
         // Start state.
@@ -255,31 +302,24 @@
         val transitionContainer = controller.transitionContainer
         val isExpandingFullyAbove = isExpandingFullyAbove(transitionContainer, endState)
 
-        // We add an extra layer with the same color as the dialog/app splash screen background
-        // color, which is usually the same color of the app background. We first fade in this layer
-        // to hide the expanding view, then we fade it out with SRC mode to draw a hole in the
-        // transition container and reveal the opening window.
-        val windowBackgroundLayer =
-            GradientDrawable().apply {
-                setColor(windowBackgroundColor)
-                alpha = 0
-            }
-
         // Update state.
         val animator = ValueAnimator.ofFloat(0f, 1f)
         animator.duration = timings.totalDuration
         animator.interpolator = LINEAR
 
         // Whether we should move the [windowBackgroundLayer] into the overlay of
-        // [Controller.openingWindowSyncView] once the opening app window starts to be visible.
+        // [Controller.openingWindowSyncView] once the opening app window starts to be visible, or
+        // from it once the closing app window stops being visible.
+        // This is necessary as a one-off sync so we can avoid syncing at every frame, especially
+        // in complex interactions like launching an activity from a dialog. See
+        // b/214961273#comment2 for more details.
         val openingWindowSyncView = controller.openingWindowSyncView
         val openingWindowSyncViewOverlay = openingWindowSyncView?.overlay
-        val moveBackgroundLayerWhenAppIsVisible =
+        val moveBackgroundLayerWhenAppVisibilityChanges =
             openingWindowSyncView != null &&
                 openingWindowSyncView.viewRootImpl != controller.transitionContainer.viewRootImpl
 
         val transitionContainerOverlay = transitionContainer.overlay
-        var cancelled = false
         var movedBackgroundLayer = false
 
         animator.addListener(
@@ -293,7 +333,11 @@
                     // Add the drawable to the transition container overlay. Overlays always draw
                     // drawables after views, so we know that it will be drawn above any view added
                     // by the controller.
-                    transitionContainerOverlay.add(windowBackgroundLayer)
+                    if (controller.isLaunching || openingWindowSyncViewOverlay == null) {
+                        transitionContainerOverlay.add(windowBackgroundLayer)
+                    } else {
+                        openingWindowSyncViewOverlay.add(windowBackgroundLayer)
+                    }
                 }
 
                 override fun onAnimationEnd(animation: Animator) {
@@ -303,7 +347,7 @@
                     controller.onTransitionAnimationEnd(isExpandingFullyAbove)
                     transitionContainerOverlay.remove(windowBackgroundLayer)
 
-                    if (moveBackgroundLayerWhenAppIsVisible) {
+                    if (moveBackgroundLayerWhenAppVisibilityChanges && controller.isLaunching) {
                         openingWindowSyncViewOverlay?.remove(windowBackgroundLayer)
                     }
                 }
@@ -311,12 +355,6 @@
         )
 
         animator.addUpdateListener { animation ->
-            if (cancelled) {
-                // TODO(b/184121838): Cancel the animator directly instead of just skipping the
-                // update.
-                return@addUpdateListener
-            }
-
             maybeUpdateEndState()
 
             // TODO(b/184121838): Use reverse interpolators to get the same path/arc as the non
@@ -338,20 +376,34 @@
             state.bottomCornerRadius =
                 MathUtils.lerp(startBottomCornerRadius, endBottomCornerRadius, progress)
 
-            // The expanding view can/should be hidden once it is completely covered by the opening
-            // window.
             state.visible =
-                getProgress(
-                    timings,
-                    linearProgress,
-                    timings.contentBeforeFadeOutDelay,
-                    timings.contentBeforeFadeOutDuration
-                ) < 1
+                if (controller.isLaunching) {
+                    // The expanding view can/should be hidden once it is completely covered by the
+                    // opening window.
+                    getProgress(
+                        timings,
+                        linearProgress,
+                        timings.contentBeforeFadeOutDelay,
+                        timings.contentBeforeFadeOutDuration
+                    ) < 1
+                } else {
+                    getProgress(
+                        timings,
+                        linearProgress,
+                        timings.contentAfterFadeInDelay,
+                        timings.contentAfterFadeInDuration
+                    ) > 0
+                }
 
-            if (moveBackgroundLayerWhenAppIsVisible && !state.visible && !movedBackgroundLayer) {
-                // The expanding view is not visible, so the opening app is visible. If this is the
-                // first frame when it happens, trigger a one-off sync and move the background layer
-                // in its new container.
+            if (
+                controller.isLaunching &&
+                    moveBackgroundLayerWhenAppVisibilityChanges &&
+                    !state.visible &&
+                    !movedBackgroundLayer
+            ) {
+                // The expanding view is not visible, so the opening app is visible. If this is
+                // the first frame when it happens, trigger a one-off sync and move the
+                // background layer in its new container.
                 movedBackgroundLayer = true
 
                 transitionContainerOverlay.remove(windowBackgroundLayer)
@@ -362,6 +414,25 @@
                     openingWindowSyncView,
                     then = {}
                 )
+            } else if (
+                !controller.isLaunching &&
+                    moveBackgroundLayerWhenAppVisibilityChanges &&
+                    state.visible &&
+                    !movedBackgroundLayer
+            ) {
+                // The contracting view is now visible, so the closing app is not. If this is
+                // the first frame when it happens, trigger a one-off sync and move the
+                // background layer in its new container.
+                movedBackgroundLayer = true
+
+                openingWindowSyncViewOverlay!!.remove(windowBackgroundLayer)
+                transitionContainerOverlay.add(windowBackgroundLayer)
+
+                ViewRootSync.synchronizeNextDraw(
+                    openingWindowSyncView,
+                    transitionContainer,
+                    then = {}
+                )
             }
 
             val container =
@@ -376,19 +447,14 @@
                 state,
                 linearProgress,
                 container,
-                fadeOutWindowBackgroundLayer,
-                drawHole
+                fadeWindowBackgroundLayer,
+                drawHole,
+                controller.isLaunching
             )
             controller.onTransitionAnimationProgress(state, progress, linearProgress)
         }
 
-        animator.start()
-        return object : Animation {
-            override fun cancel() {
-                cancelled = true
-                animator.cancel()
-            }
-        }
+        return animator
     }
 
     /** Return whether we are expanding fully above the [transitionContainer]. */
@@ -405,8 +471,9 @@
         state: State,
         linearProgress: Float,
         transitionContainer: View,
-        fadeOutWindowBackgroundLayer: Boolean,
-        drawHole: Boolean
+        fadeWindowBackgroundLayer: Boolean,
+        drawHole: Boolean,
+        isLaunching: Boolean
     ) {
         // Update position.
         transitionContainer.getLocationOnScreen(transitionContainerLocation)
@@ -437,27 +504,64 @@
                 timings.contentBeforeFadeOutDelay,
                 timings.contentBeforeFadeOutDuration
             )
-        if (fadeInProgress < 1) {
-            val alpha =
-                interpolators.contentBeforeFadeOutInterpolator.getInterpolation(fadeInProgress)
-            drawable.alpha = (alpha * 0xFF).roundToInt()
-        } else if (fadeOutWindowBackgroundLayer) {
-            val fadeOutProgress =
-                getProgress(
-                    timings,
-                    linearProgress,
-                    timings.contentAfterFadeInDelay,
-                    timings.contentAfterFadeInDuration
-                )
-            val alpha =
-                1 - interpolators.contentAfterFadeInInterpolator.getInterpolation(fadeOutProgress)
-            drawable.alpha = (alpha * 0xFF).roundToInt()
 
-            if (drawHole) {
-                drawable.setXfermode(SRC_MODE)
+        if (isLaunching) {
+            if (fadeInProgress < 1) {
+                val alpha =
+                    interpolators.contentBeforeFadeOutInterpolator.getInterpolation(fadeInProgress)
+                drawable.alpha = (alpha * 0xFF).roundToInt()
+            } else if (fadeWindowBackgroundLayer) {
+                val fadeOutProgress =
+                    getProgress(
+                        timings,
+                        linearProgress,
+                        timings.contentAfterFadeInDelay,
+                        timings.contentAfterFadeInDuration
+                    )
+                val alpha =
+                    1 -
+                        interpolators.contentAfterFadeInInterpolator.getInterpolation(
+                            fadeOutProgress
+                        )
+                drawable.alpha = (alpha * 0xFF).roundToInt()
+
+                if (drawHole) {
+                    drawable.setXfermode(SRC_MODE)
+                }
+            } else {
+                drawable.alpha = 0xFF
             }
         } else {
-            drawable.alpha = 0xFF
+            if (fadeInProgress < 1 && fadeWindowBackgroundLayer) {
+                val alpha =
+                    interpolators.contentBeforeFadeOutInterpolator.getInterpolation(fadeInProgress)
+                drawable.alpha = (alpha * 0xFF).roundToInt()
+
+                if (drawHole) {
+                    drawable.setXfermode(SRC_MODE)
+                }
+            } else {
+                val fadeOutProgress =
+                    getProgress(
+                        timings,
+                        linearProgress,
+                        timings.contentAfterFadeInDelay,
+                        timings.contentAfterFadeInDuration
+                    )
+                val alpha =
+                    1 -
+                        interpolators.contentAfterFadeInInterpolator.getInterpolation(
+                            fadeOutProgress
+                        )
+                drawable.alpha = (alpha * 0xFF).roundToInt()
+                drawable.setXfermode(null)
+            }
+        }
+    }
+
+    private fun checkReturnAnimationFrameworkFlag() {
+        check(returnAnimationFrameworkLibrary()) {
+            "isLaunching cannot be false when the returnAnimationFrameworkLibrary flag is disabled"
         }
     }
 }