Open and close animation for trampoline task

Moving most of the logic from DesktopAppLaunchTransition to DesktopAppLaunchAnimatorHelper to make it more testable

Bug: 391548553
Flag: com.android.window.flags.enable_desktop_trampoline_close_animation_bugfix
Test: DesktopAppLaunchAnimatorHelperTest

Change-Id: I606fc3fc25250ea4128f3089b701a1ae153dc47a
diff --git a/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchAnimatorHelper.kt b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchAnimatorHelper.kt
new file mode 100644
index 0000000..adbcc75
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchAnimatorHelper.kt
@@ -0,0 +1,192 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.desktop
+
+import android.animation.Animator
+import android.animation.AnimatorSet
+import android.animation.ValueAnimator
+import android.content.Context
+import android.graphics.Rect
+import android.view.Choreographer
+import android.view.SurfaceControl.Transaction
+import android.view.WindowManager.TRANSIT_CLOSE
+import android.view.WindowManager.TRANSIT_OPEN
+import android.view.WindowManager.TRANSIT_TO_BACK
+import android.window.DesktopModeFlags
+import android.window.TransitionInfo
+import android.window.TransitionInfo.Change
+import androidx.core.animation.addListener
+import androidx.core.util.Supplier
+import com.android.app.animation.Interpolators
+import com.android.internal.jank.Cuj
+import com.android.internal.jank.InteractionJankMonitor
+import com.android.internal.policy.ScreenDecorationsUtils
+import com.android.launcher3.desktop.DesktopAppLaunchTransition.AppLaunchType
+import com.android.launcher3.desktop.DesktopAppLaunchTransition.Companion.LAUNCH_CHANGE_MODES
+import com.android.wm.shell.shared.animation.MinimizeAnimator
+import com.android.wm.shell.shared.animation.WindowAnimator
+
+/**
+ * Helper class responsible for creating and managing animators for desktop app launch and related
+ * transitions.
+ *
+ * <p>This class handles the complex logic of creating various animators, including launch,
+ * minimize, and trampoline close animations, based on the provided transition information and
+ * launch type. It also utilizes {@link InteractionJankMonitor} to monitor animation jank.
+ *
+ * @param context The application context.
+ * @param launchType The type of app launch, containing animation parameters.
+ * @param cujType The CUJ (Critical User Journey) type for jank monitoring.
+ */
+class DesktopAppLaunchAnimatorHelper(
+    private val context: Context,
+    private val launchType: AppLaunchType,
+    @Cuj.CujType private val cujType: Int,
+    private val transactionSupplier: Supplier<Transaction>,
+) {
+
+    private val interactionJankMonitor = InteractionJankMonitor.getInstance()
+
+    fun createAnimators(info: TransitionInfo, finishCallback: (Animator) -> Unit): List<Animator> {
+        val launchChange = getLaunchChange(info)
+        requireNotNull(launchChange) { "expected an app launch Change" }
+
+        val transaction = transactionSupplier.get()
+
+        val minimizeChange = getMinimizeChange(info)
+        val trampolineCloseChange = getTrampolineCloseChange(info)
+
+        val launchAnimator =
+            createLaunchAnimator(
+                launchChange,
+                transaction,
+                finishCallback,
+                isTrampoline = trampolineCloseChange != null,
+            )
+        val animatorsList = mutableListOf(launchAnimator)
+        if (minimizeChange != null) {
+            val minimizeAnimator =
+                createMinimizeAnimator(minimizeChange, transaction, finishCallback)
+            animatorsList.add(minimizeAnimator)
+        }
+        if (trampolineCloseChange != null) {
+            val trampolineCloseAnimator =
+                createTrampolineCloseAnimator(trampolineCloseChange, transaction)
+            animatorsList.add(trampolineCloseAnimator)
+        }
+        return animatorsList
+    }
+
+    private fun getLaunchChange(info: TransitionInfo): Change? =
+        info.changes.firstOrNull { change -> change.mode in LAUNCH_CHANGE_MODES }
+
+    private fun getMinimizeChange(info: TransitionInfo): Change? =
+        info.changes.firstOrNull { change -> change.mode == TRANSIT_TO_BACK }
+
+    private fun getTrampolineCloseChange(info: TransitionInfo): Change? {
+        if (
+            info.changes.size < 2 ||
+                !DesktopModeFlags.ENABLE_DESKTOP_TRAMPOLINE_CLOSE_ANIMATION_BUGFIX.isTrue
+        ) {
+            return null
+        }
+        val openChange =
+            info.changes.firstOrNull { change ->
+                change.mode == TRANSIT_OPEN && change.taskInfo?.isFreeform == true
+            }
+        val closeChange =
+            info.changes.firstOrNull { change ->
+                change.mode == TRANSIT_CLOSE && change.taskInfo?.isFreeform == true
+            }
+        val openPackage = openChange?.taskInfo?.baseIntent?.component?.packageName
+        val closePackage = closeChange?.taskInfo?.baseIntent?.component?.packageName
+        return if (openPackage != null && closePackage != null && openPackage == closePackage) {
+            closeChange
+        } else {
+            null
+        }
+    }
+
+    private fun createLaunchAnimator(
+        change: Change,
+        transaction: Transaction,
+        onAnimFinish: (Animator) -> Unit,
+        isTrampoline: Boolean,
+    ): Animator {
+        val boundsAnimator =
+            WindowAnimator.createBoundsAnimator(
+                context.resources.displayMetrics,
+                launchType.boundsAnimationParams,
+                change,
+                transaction,
+            )
+        val alphaAnimator =
+            ValueAnimator.ofFloat(0f, 1f).apply {
+                duration = launchType.alphaDurationMs
+                interpolator = Interpolators.LINEAR
+                addUpdateListener { animation ->
+                    transaction
+                        .setAlpha(change.leash, animation.animatedValue as Float)
+                        .setFrameTimeline(Choreographer.getInstance().vsyncId)
+                        .apply()
+                }
+            }
+        val clipRect = Rect(change.endAbsBounds).apply { offsetTo(0, 0) }
+        transaction.setCrop(change.leash, clipRect)
+        transaction.setCornerRadius(
+            change.leash,
+            ScreenDecorationsUtils.getWindowCornerRadius(context),
+        )
+        return AnimatorSet().apply {
+            interactionJankMonitor.begin(change.leash, context, context.mainThreadHandler, cujType)
+            if (isTrampoline) {
+                play(alphaAnimator)
+            } else {
+                playTogether(boundsAnimator, alphaAnimator)
+            }
+            addListener(
+                onEnd = { animation ->
+                    onAnimFinish(animation)
+                    interactionJankMonitor.end(cujType)
+                }
+            )
+        }
+    }
+
+    private fun createMinimizeAnimator(
+        change: Change,
+        transaction: Transaction,
+        onAnimFinish: (Animator) -> Unit,
+    ): Animator {
+        return MinimizeAnimator.create(
+            context.resources.displayMetrics,
+            change,
+            transaction,
+            onAnimFinish,
+        )
+    }
+
+    private fun createTrampolineCloseAnimator(change: Change, transaction: Transaction): Animator {
+        return ValueAnimator.ofFloat(1f, 0f).apply {
+            duration = 100L
+            interpolator = Interpolators.LINEAR
+            addUpdateListener { animation ->
+                transaction.setAlpha(change.leash, animation.animatedValue as Float).apply()
+            }
+        }
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransition.kt b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransition.kt
index 2406fb6..578bba5 100644
--- a/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransition.kt
+++ b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransition.kt
@@ -17,27 +17,18 @@
 package com.android.launcher3.desktop
 
 import android.animation.Animator
-import android.animation.AnimatorSet
-import android.animation.ValueAnimator
 import android.content.Context
-import android.graphics.Rect
 import android.os.IBinder
-import android.view.Choreographer
 import android.view.SurfaceControl.Transaction
 import android.view.WindowManager.TRANSIT_OPEN
-import android.view.WindowManager.TRANSIT_TO_BACK
 import android.view.WindowManager.TRANSIT_TO_FRONT
 import android.window.IRemoteTransitionFinishedCallback
 import android.window.RemoteTransitionStub
 import android.window.TransitionInfo
-import android.window.TransitionInfo.Change
-import androidx.core.animation.addListener
+import androidx.core.util.Supplier
 import com.android.app.animation.Interpolators
 import com.android.internal.jank.Cuj
-import com.android.internal.jank.InteractionJankMonitor
-import com.android.internal.policy.ScreenDecorationsUtils
 import com.android.quickstep.RemoteRunnable
-import com.android.wm.shell.shared.animation.MinimizeAnimator
 import com.android.wm.shell.shared.animation.WindowAnimator
 import java.util.concurrent.Executor
 
@@ -48,14 +39,18 @@
  * ([android.view.WindowManager.TRANSIT_TO_BACK]) this transition will apply a minimize animation to
  * that window.
  */
-class DesktopAppLaunchTransition(
-    private val context: Context,
-    private val mainExecutor: Executor,
-    private val launchType: AppLaunchType,
+class DesktopAppLaunchTransition
+@JvmOverloads
+constructor(
+    context: Context,
+    launchType: AppLaunchType,
     @Cuj.CujType private val cujType: Int,
+    private val mainExecutor: Executor,
+    transactionSupplier: Supplier<Transaction> = Supplier { Transaction() },
 ) : RemoteTransitionStub() {
 
-    private val interactionJankMonitor = InteractionJankMonitor.getInstance()
+    private val animatorHelper: DesktopAppLaunchAnimatorHelper =
+        DesktopAppLaunchAnimatorHelper(context, launchType, cujType, transactionSupplier)
 
     enum class AppLaunchType(
         val boundsAnimationParams: WindowAnimator.BoundsAnimationParams,
@@ -68,7 +63,7 @@
     override fun startAnimation(
         token: IBinder,
         info: TransitionInfo,
-        t: Transaction,
+        transaction: Transaction,
         transitionFinishedCallback: IRemoteTransitionFinishedCallback,
     ) {
         val safeTransitionFinishedCallback = RemoteRunnable {
@@ -76,7 +71,7 @@
         }
         mainExecutor.execute {
             runAnimators(info, safeTransitionFinishedCallback)
-            t.apply()
+            transaction.apply()
         }
     }
 
@@ -86,77 +81,10 @@
             animators -= animator
             if (animators.isEmpty()) finishedCallback.run()
         }
-        animators += createAnimators(info, animatorFinishedCallback)
+        animators += animatorHelper.createAnimators(info, animatorFinishedCallback)
         animators.forEach { it.start() }
     }
 
-    private fun createAnimators(
-        info: TransitionInfo,
-        finishCallback: (Animator) -> Unit,
-    ): List<Animator> {
-        val transaction = Transaction()
-        val launchAnimator =
-            createLaunchAnimator(getLaunchChange(info), transaction, finishCallback)
-        val minimizeChange = getMinimizeChange(info) ?: return listOf(launchAnimator)
-        val minimizeAnimator =
-            MinimizeAnimator.create(
-                context.resources.displayMetrics,
-                minimizeChange,
-                transaction,
-                finishCallback,
-            )
-        return listOf(launchAnimator, minimizeAnimator)
-    }
-
-    private fun getLaunchChange(info: TransitionInfo): Change =
-        requireNotNull(info.changes.firstOrNull { change -> change.mode in LAUNCH_CHANGE_MODES }) {
-            "expected an app launch Change"
-        }
-
-    private fun getMinimizeChange(info: TransitionInfo): Change? =
-        info.changes.firstOrNull { change -> change.mode == TRANSIT_TO_BACK }
-
-    private fun createLaunchAnimator(
-        change: Change,
-        transaction: Transaction,
-        onAnimFinish: (Animator) -> Unit,
-    ): Animator {
-        val boundsAnimator =
-            WindowAnimator.createBoundsAnimator(
-                context.resources.displayMetrics,
-                launchType.boundsAnimationParams,
-                change,
-                transaction,
-            )
-        val alphaAnimator =
-            ValueAnimator.ofFloat(0f, 1f).apply {
-                duration = launchType.alphaDurationMs
-                interpolator = Interpolators.LINEAR
-                addUpdateListener { animation ->
-                    transaction
-                        .setAlpha(change.leash, animation.animatedValue as Float)
-                        .setFrameTimeline(Choreographer.getInstance().vsyncId)
-                        .apply()
-                }
-            }
-        val clipRect = Rect(change.endAbsBounds).apply { offsetTo(0, 0) }
-        transaction.setCrop(change.leash, clipRect)
-        transaction.setCornerRadius(
-            change.leash,
-            ScreenDecorationsUtils.getWindowCornerRadius(context),
-        )
-        return AnimatorSet().apply {
-            interactionJankMonitor.begin(change.leash, context, context.mainThreadHandler, cujType)
-            playTogether(boundsAnimator, alphaAnimator)
-            addListener(
-                onEnd = { animation ->
-                    onAnimFinish(animation)
-                    interactionJankMonitor.end(cujType)
-                }
-            )
-        }
-    }
-
     companion object {
         /** Change modes that represent a task becoming visible / launching in Desktop mode. */
         val LAUNCH_CHANGE_MODES = intArrayOf(TRANSIT_OPEN, TRANSIT_TO_FRONT)
diff --git a/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransitionManager.kt b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransitionManager.kt
index 36c5fba..a72b5c4 100644
--- a/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransitionManager.kt
+++ b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransitionManager.kt
@@ -48,9 +48,9 @@
             RemoteTransition(
                 DesktopAppLaunchTransition(
                     context,
-                    MAIN_EXECUTOR,
                     AppLaunchType.UNMINIMIZE,
                     Cuj.CUJ_DESKTOP_MODE_APP_LAUNCH_FROM_INTENT,
+                    MAIN_EXECUTOR,
                 ),
                 "DesktopWindowLimitUnminimize",
             )
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
index 5af7ff8..5f7a026 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
@@ -290,9 +290,9 @@
             remoteTransition = new RemoteTransition(
                     new DesktopAppLaunchTransition(
                             context,
-                            MAIN_EXECUTOR,
                             UNMINIMIZE,
-                            Cuj.CUJ_DESKTOP_MODE_KEYBOARD_QUICK_SWITCH_APP_LAUNCH
+                            Cuj.CUJ_DESKTOP_MODE_KEYBOARD_QUICK_SWITCH_APP_LAUNCH,
+                            MAIN_EXECUTOR
                     ),
                     "DesktopKeyboardQuickSwitchUnminimize");
         }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index b9d1275..8c807ee 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -1533,9 +1533,9 @@
         return new RemoteTransition(
                 new DesktopAppLaunchTransition(
                         this,
-                        getMainExecutor(),
                         appLaunchType,
-                        cujType
+                        cujType,
+                        getMainExecutor()
                 ),
                 "TaskbarDesktopAppLaunch");
     }
diff --git a/quickstep/tests/src/com/android/quickstep/desktop/DesktopAppLaunchAnimatorHelperTest.kt b/quickstep/tests/src/com/android/quickstep/desktop/DesktopAppLaunchAnimatorHelperTest.kt
new file mode 100644
index 0000000..b4d9f5b
--- /dev/null
+++ b/quickstep/tests/src/com/android/quickstep/desktop/DesktopAppLaunchAnimatorHelperTest.kt
@@ -0,0 +1,279 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.desktop
+
+import android.animation.Animator
+import android.animation.AnimatorSet
+import android.animation.ValueAnimator
+import android.app.ActivityManager
+import android.app.WindowConfiguration
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.res.Resources
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
+import android.util.DisplayMetrics
+import android.view.SurfaceControl
+import android.view.WindowManager
+import android.window.TransitionInfo
+import androidx.core.util.Supplier
+import com.android.app.animation.Interpolators
+import com.android.internal.jank.Cuj
+import com.android.launcher3.desktop.DesktopAppLaunchAnimatorHelper
+import com.android.launcher3.desktop.DesktopAppLaunchTransition.AppLaunchType
+import com.android.launcher3.util.Executors.MAIN_EXECUTOR
+import com.android.window.flags.Flags
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+class DesktopAppLaunchAnimatorHelperTest {
+
+    @get:Rule val setFlagsRule = SetFlagsRule()
+
+    private val context = mock<Context>()
+    private val resources = mock<Resources>()
+    private val transaction = mock<SurfaceControl.Transaction>()
+    private val transactionSupplier = mock<Supplier<SurfaceControl.Transaction>>()
+
+    private lateinit var helper: DesktopAppLaunchAnimatorHelper
+
+    @Before
+    fun setUp() {
+        helper =
+            DesktopAppLaunchAnimatorHelper(
+                context = context,
+                launchType = AppLaunchType.LAUNCH,
+                cujType = Cuj.CUJ_DESKTOP_MODE_APP_LAUNCH_FROM_INTENT,
+                transactionSupplier = transactionSupplier,
+            )
+        whenever(transactionSupplier.get()).thenReturn(transaction)
+        whenever(transaction.setCrop(any(), any())).thenReturn(transaction)
+        whenever(transaction.setCornerRadius(any(), any())).thenReturn(transaction)
+
+        whenever(context.resources).thenReturn(resources)
+        whenever(resources.displayMetrics).thenReturn(DisplayMetrics())
+        whenever(context.mainThreadHandler).thenReturn(MAIN_EXECUTOR.handler)
+    }
+
+    @Test
+    fun launchTransition_returnsLaunchAnimator() {
+        val openChange =
+            TransitionInfo.Change(mock(), mock()).apply {
+                mode = WindowManager.TRANSIT_OPEN
+                taskInfo = TASK_INFO_FREEFORM
+            }
+        val transitionInfo = TransitionInfo(WindowManager.TRANSIT_NONE, 0)
+        transitionInfo.addChange(openChange)
+
+        val actual = helper.createAnimators(transitionInfo, finishCallback = {})
+
+        assertThat(actual).hasSize(1)
+        assertLaunchAnimator(actual[0])
+    }
+
+    @Test
+    fun minimizeTransition_returnsLaunchAndMinimizeAnimator() {
+        val openChange =
+            TransitionInfo.Change(mock(), mock()).apply {
+                mode = WindowManager.TRANSIT_OPEN
+                taskInfo = TASK_INFO_FREEFORM
+            }
+        val minimizeChange =
+            TransitionInfo.Change(mock(), mock()).apply {
+                mode = WindowManager.TRANSIT_TO_BACK
+                taskInfo = TASK_INFO_FREEFORM
+            }
+        val transitionInfo = TransitionInfo(WindowManager.TRANSIT_NONE, 0)
+        transitionInfo.addChange(openChange)
+        transitionInfo.addChange(minimizeChange)
+
+        val actual = helper.createAnimators(transitionInfo, finishCallback = {})
+
+        assertThat(actual).hasSize(2)
+        assertLaunchAnimator(actual[0])
+        assertMinimizeAnimator(actual[1])
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_TRAMPOLINE_CLOSE_ANIMATION_BUGFIX)
+    fun trampolineTransition_flagEnabled_returnsLaunchAndCloseAnimator() {
+        val openChange =
+            TransitionInfo.Change(mock(), mock()).apply {
+                mode = WindowManager.TRANSIT_OPEN
+                taskInfo = TASK_INFO_FREEFORM
+            }
+        val closeChange =
+            TransitionInfo.Change(mock(), mock()).apply {
+                mode = WindowManager.TRANSIT_CLOSE
+                taskInfo = TASK_INFO_FREEFORM
+            }
+        val transitionInfo = TransitionInfo(WindowManager.TRANSIT_NONE, 0)
+        transitionInfo.addChange(openChange)
+        transitionInfo.addChange(closeChange)
+
+        val actual = helper.createAnimators(transitionInfo, finishCallback = {})
+
+        assertThat(actual).hasSize(2)
+        assertTrampolineLaunchAnimator(actual[0])
+        assertCloseAnimator(actual[1])
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_TRAMPOLINE_CLOSE_ANIMATION_BUGFIX)
+    fun trampolineTransition_flagDisabled_returnsLaunchAnimator() {
+        val openChange =
+            TransitionInfo.Change(mock(), mock()).apply {
+                mode = WindowManager.TRANSIT_OPEN
+                taskInfo = TASK_INFO_FREEFORM
+            }
+        val closeChange =
+            TransitionInfo.Change(mock(), mock()).apply {
+                mode = WindowManager.TRANSIT_CLOSE
+                taskInfo = TASK_INFO_FREEFORM
+            }
+        val transitionInfo = TransitionInfo(WindowManager.TRANSIT_NONE, 0)
+        transitionInfo.addChange(openChange)
+        transitionInfo.addChange(closeChange)
+
+        val actual = helper.createAnimators(transitionInfo, finishCallback = {})
+
+        assertThat(actual).hasSize(1)
+        assertLaunchAnimator(actual[0])
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_TRAMPOLINE_CLOSE_ANIMATION_BUGFIX)
+    fun trampolineTransition_flagEnabled_hitDesktopWindowLimit_returnsLaunchMinimizeCloseAnimator() {
+        val openChange =
+            TransitionInfo.Change(mock(), mock()).apply {
+                mode = WindowManager.TRANSIT_OPEN
+                taskInfo = TASK_INFO_FREEFORM
+            }
+        val minimizeChange =
+            TransitionInfo.Change(mock(), mock()).apply {
+                mode = WindowManager.TRANSIT_TO_BACK
+                taskInfo = TASK_INFO_FREEFORM
+            }
+        val closeChange =
+            TransitionInfo.Change(mock(), mock()).apply {
+                mode = WindowManager.TRANSIT_CLOSE
+                taskInfo = TASK_INFO_FREEFORM
+            }
+        val transitionInfo = TransitionInfo(WindowManager.TRANSIT_NONE, 0)
+        transitionInfo.addChange(openChange)
+        transitionInfo.addChange(minimizeChange)
+        transitionInfo.addChange(closeChange)
+
+        val actual = helper.createAnimators(transitionInfo, finishCallback = {})
+
+        assertThat(actual).hasSize(3)
+        assertTrampolineLaunchAnimator(actual[0])
+        assertMinimizeAnimator(actual[1])
+        assertCloseAnimator(actual[2])
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_TRAMPOLINE_CLOSE_ANIMATION_BUGFIX)
+    fun trampolineTransition_flagDisabled_hitDesktopWindowLimit_returnsLaunchMinimizeAnimator() {
+        val openChange =
+            TransitionInfo.Change(mock(), mock()).apply {
+                mode = WindowManager.TRANSIT_OPEN
+                taskInfo = TASK_INFO_FREEFORM
+            }
+        val minimizeChange =
+            TransitionInfo.Change(mock(), mock()).apply {
+                mode = WindowManager.TRANSIT_TO_BACK
+                taskInfo = TASK_INFO_FREEFORM
+            }
+        val closeChange =
+            TransitionInfo.Change(mock(), mock()).apply {
+                mode = WindowManager.TRANSIT_CLOSE
+                taskInfo = TASK_INFO_FREEFORM
+            }
+        val transitionInfo = TransitionInfo(WindowManager.TRANSIT_NONE, 0)
+        transitionInfo.addChange(openChange)
+        transitionInfo.addChange(minimizeChange)
+        transitionInfo.addChange(closeChange)
+
+        val actual = helper.createAnimators(transitionInfo, finishCallback = {})
+
+        assertThat(actual).hasSize(2)
+        assertLaunchAnimator(actual[0])
+        assertMinimizeAnimator(actual[1])
+    }
+
+    private fun assertLaunchAnimator(animator: Animator) {
+        assertThat(animator).isInstanceOf(AnimatorSet::class.java)
+        assertThat((animator as AnimatorSet).childAnimations.size).isEqualTo(2)
+        assertThat(animator.childAnimations[0]).isInstanceOf(ValueAnimator::class.java)
+        assertThat(animator.childAnimations[0].interpolator)
+            .isEqualTo(AppLaunchType.LAUNCH.boundsAnimationParams.interpolator)
+        assertThat(animator.childAnimations[0].duration)
+            .isEqualTo(AppLaunchType.LAUNCH.boundsAnimationParams.durationMs)
+        assertThat(animator.childAnimations[1]).isInstanceOf(ValueAnimator::class.java)
+        assertThat(animator.childAnimations[1].interpolator).isEqualTo(Interpolators.LINEAR)
+        assertThat(animator.childAnimations[1].duration)
+            .isEqualTo(AppLaunchType.LAUNCH.alphaDurationMs)
+    }
+
+    private fun assertTrampolineLaunchAnimator(animator: Animator) {
+        assertThat(animator).isInstanceOf(AnimatorSet::class.java)
+        assertThat((animator as AnimatorSet).childAnimations.size).isEqualTo(1)
+        assertThat(animator.childAnimations[0]).isInstanceOf(ValueAnimator::class.java)
+        assertThat(animator.childAnimations[0].interpolator).isEqualTo(Interpolators.LINEAR)
+        assertThat(animator.childAnimations[0].duration)
+            .isEqualTo(AppLaunchType.LAUNCH.alphaDurationMs)
+    }
+
+    private fun assertMinimizeAnimator(animator: Animator) {
+        assertThat(animator).isInstanceOf(AnimatorSet::class.java)
+        assertThat((animator as AnimatorSet).childAnimations.size).isEqualTo(2)
+        assertThat(animator.childAnimations[0]).isInstanceOf(ValueAnimator::class.java)
+        assertThat(animator.childAnimations[0].interpolator)
+            .isInstanceOf(Interpolators.STANDARD_ACCELERATE::class.java)
+        assertThat(animator.childAnimations[0].duration).isEqualTo(200)
+        assertThat(animator.childAnimations[1]).isInstanceOf(ValueAnimator::class.java)
+        assertThat(animator.childAnimations[1].interpolator)
+            .isInstanceOf(Interpolators.LINEAR::class.java)
+        assertThat(animator.childAnimations[1].duration).isEqualTo(100)
+    }
+
+    private fun assertCloseAnimator(animator: Animator) {
+        assertThat(animator).isInstanceOf(ValueAnimator::class.java)
+        assertThat(animator.interpolator).isInstanceOf(Interpolators.LINEAR::class.java)
+        assertThat(animator.duration).isEqualTo(100)
+    }
+
+    private companion object {
+        val TASK_INFO_FREEFORM =
+            ActivityManager.RunningTaskInfo().apply {
+                baseIntent =
+                    Intent().apply {
+                        component = ComponentName("com.example.app", "com.example.app.MainActivity")
+                    }
+                configuration.windowConfiguration.windowingMode =
+                    WindowConfiguration.WINDOWING_MODE_FREEFORM
+            }
+    }
+}