Merge "Add ime back button logic to the init block of NavbarButtonsViewController" into 24D1-dev
diff --git a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
index 57b0a09..1d7c11b 100644
--- a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
+++ b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
@@ -23,6 +23,7 @@
 import android.animation.ObjectAnimator
 import android.animation.ValueAnimator
 import android.app.ActivityManager.RunningTaskInfo
+import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
 import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW
 import android.content.Context
 import android.graphics.Bitmap
@@ -51,6 +52,7 @@
 import com.android.launcher3.apppairs.AppPairIcon
 import com.android.launcher3.config.FeatureFlags
 import com.android.launcher3.logging.StatsLogManager.EventEnum
+import com.android.launcher3.model.data.WorkspaceItemInfo
 import com.android.launcher3.statehandlers.DepthController
 import com.android.launcher3.statemanager.StateManager
 import com.android.launcher3.statemanager.StatefulActivity
@@ -69,6 +71,7 @@
 import com.android.quickstep.views.TaskView
 import com.android.quickstep.views.TaskView.TaskIdAttributeContainer
 import com.android.quickstep.views.TaskViewIcon
+import com.android.wm.shell.shared.TransitionUtil
 import java.util.Optional
 import java.util.function.Supplier
 
@@ -532,8 +535,14 @@
             check(info != null && t != null) {
                 "trying to launch an app pair icon, but encountered an unexpected null"
             }
-
-            composeIconSplitLaunchAnimator(launchingIconView, info, t, finishCallback)
+            val appPairLaunchingAppIndex = hasChangesForBothAppPairs(launchingIconView, info)
+            if (appPairLaunchingAppIndex == -1) {
+                // Launch split app pair animation
+                composeIconSplitLaunchAnimator(launchingIconView, info, t, finishCallback)
+            } else {
+                composeFullscreenIconSplitLaunchAnimator(launchingIconView, info, t,
+                        finishCallback, appPairLaunchingAppIndex)
+            }
         } else {
             // Fallback case: simple fade-in animation
             check(info != null && t != null) {
@@ -598,6 +607,39 @@
     }
 
     /**
+     * @return -1 if [transitionInfo] contains both apps of the app pair to be animated, otherwise
+     *         the integer index corresponding to [launchingIconView]'s contents for the single app
+     *         to be animated
+     */
+    fun hasChangesForBothAppPairs(launchingIconView: AppPairIcon,
+                                          transitionInfo: TransitionInfo) : Int {
+        val intent1 = launchingIconView.info.getFirstApp().intent.component?.packageName
+        val intent2 = launchingIconView.info.getSecondApp().intent.component?.packageName
+        var launchFullscreenAppIndex = -1
+        for (change in transitionInfo.changes) {
+            val taskInfo: RunningTaskInfo = change.taskInfo ?: continue
+            if (TransitionUtil.isOpeningType(change.mode) &&
+                    taskInfo.windowingMode == WINDOWING_MODE_FULLSCREEN) {
+                val baseIntent = taskInfo.baseIntent.component?.packageName
+                if (baseIntent == intent1) {
+                    if (launchFullscreenAppIndex > -1) {
+                        launchFullscreenAppIndex = -1
+                        break
+                    }
+                    launchFullscreenAppIndex = 0
+                } else if (baseIntent == intent2) {
+                    if (launchFullscreenAppIndex > -1) {
+                        launchFullscreenAppIndex = -1
+                        break
+                    }
+                    launchFullscreenAppIndex = 1
+                }
+            }
+        }
+        return launchFullscreenAppIndex
+    }
+
+    /**
      * When the user taps an app pair icon to launch split, this will play the tasks' launch
      * animation from the position of the icon.
      *
@@ -632,7 +674,8 @@
         // If launching an app pair from Taskbar inside of an app context (no access to Launcher),
         // use the scale-up animation
         if (launchingIconView.context is TaskbarActivityContext) {
-            composeScaleUpLaunchAnimation(transitionInfo, t, finishCallback)
+            composeScaleUpLaunchAnimation(transitionInfo, t, finishCallback,
+                    WINDOWING_MODE_MULTI_WINDOW)
             return
         }
 
@@ -642,11 +685,6 @@
 
         // Create an AnimatorSet that will run both shell and launcher transitions together
         val launchAnimation = AnimatorSet()
-        val progressUpdater = ValueAnimator.ofFloat(0f, 1f)
-        val timings = AnimUtils.getDeviceAppPairLaunchTimings(dp.isTablet)
-        progressUpdater.setDuration(timings.getDuration().toLong())
-        progressUpdater.interpolator = Interpolators.LINEAR
-
         var rootCandidate: Change? = null
 
         for (change in transitionInfo.changes) {
@@ -690,27 +728,13 @@
         // Make sure nothing weird happened, like getChange() returning null.
         check(rootCandidate != null) { "Failed to find a root leash" }
 
-        // Shell animation: the apps are revealed toward end of the launch animation
-        progressUpdater.addUpdateListener { valueAnimator: ValueAnimator ->
-            val progress =
-                Interpolators.clampToProgress(
-                    Interpolators.LINEAR,
-                    valueAnimator.animatedFraction,
-                    timings.appRevealStartOffset,
-                    timings.appRevealEndOffset
-                )
-
-            // Set the alpha of the shell layer (2 apps + divider)
-            t.setAlpha(rootCandidate.leash, progress)
-            t.apply()
-        }
-
         // Create a new floating view in Launcher, positioned above the launching icon
         val drawableArea = launchingIconView.iconDrawableArea
         val appIcon1 = launchingIconView.info.getFirstApp().newIcon(launchingIconView.context)
         val appIcon2 = launchingIconView.info.getSecondApp().newIcon(launchingIconView.context)
         appIcon1.setBounds(0, 0, dp.iconSizePx, dp.iconSizePx)
         appIcon2.setBounds(0, 0, dp.iconSizePx, dp.iconSizePx)
+
         val floatingView =
             FloatingAppPairView.getFloatingAppPairView(
                 launcher,
@@ -721,84 +745,189 @@
             )
         floatingView.bringToFront()
 
-        // Launcher animation: animate the floating view, expanding to fill the display surface
-        progressUpdater.addUpdateListener(
-            object : MultiValueUpdateListener() {
-                var mDx =
-                    FloatProp(
-                        floatingView.startingPosition.left,
-                        dp.widthPx / 2f - floatingView.startingPosition.width() / 2f,
-                        Interpolators.clampToProgress(
-                            timings.getStagedRectXInterpolator(),
-                            timings.stagedRectSlideStartOffset,
-                            timings.stagedRectSlideEndOffset
-                        )
-                    )
-                var mDy =
-                    FloatProp(
-                        floatingView.startingPosition.top,
-                        dp.heightPx / 2f - floatingView.startingPosition.height() / 2f,
-                        Interpolators.clampToProgress(
-                            Interpolators.EMPHASIZED,
-                            timings.stagedRectSlideStartOffset,
-                            timings.stagedRectSlideEndOffset
-                        )
-                    )
-                var mScaleX =
-                    FloatProp(
-                        1f /* start */,
-                        dp.widthPx / floatingView.startingPosition.width(),
-                        Interpolators.clampToProgress(
-                            Interpolators.EMPHASIZED,
-                            timings.stagedRectSlideStartOffset,
-                            timings.stagedRectSlideEndOffset
-                        )
-                    )
-                var mScaleY =
-                    FloatProp(
-                        1f /* start */,
-                        dp.heightPx / floatingView.startingPosition.height(),
-                        Interpolators.clampToProgress(
-                            Interpolators.EMPHASIZED,
-                            timings.stagedRectSlideStartOffset,
-                            timings.stagedRectSlideEndOffset
-                        )
-                    )
-
-                override fun onUpdate(percent: Float, initOnly: Boolean) {
-                    floatingView.progress = percent
-                    floatingView.x = mDx.value
-                    floatingView.y = mDy.value
-                    floatingView.scaleX = mScaleX.value
-                    floatingView.scaleY = mScaleY.value
-                    floatingView.invalidate()
-                }
-            }
-        )
-
-        // When animation ends, remove the floating view and run finishCallback
-        progressUpdater.addListener(
-            object : AnimatorListenerAdapter() {
-                override fun onAnimationEnd(animation: Animator) {
-                    safeRemoveViewFromDragLayer(launcher, floatingView)
-                    finishCallback.run()
-                }
-            }
-        )
-
-        launchAnimation.play(progressUpdater)
+        launchAnimation.play(
+                getIconLaunchValueAnimator(t, dp, finishCallback, launcher, floatingView,
+                        rootCandidate))
         launchAnimation.start()
     }
 
     /**
+     * Similar to [composeIconSplitLaunchAnimator], but instructs [FloatingAppPairView] to animate
+     * a single fullscreen icon + background instead of for a pair
+     */
+    @VisibleForTesting
+    fun composeFullscreenIconSplitLaunchAnimator(
+            launchingIconView: AppPairIcon,
+            transitionInfo: TransitionInfo,
+            t: Transaction,
+            finishCallback: Runnable,
+            launchFullscreenIndex: Int
+    ) {
+        // If launching an app pair from Taskbar inside of an app context (no access to Launcher),
+        // use the scale-up animation
+        if (launchingIconView.context is TaskbarActivityContext) {
+            composeScaleUpLaunchAnimation(transitionInfo, t, finishCallback,
+                    WINDOWING_MODE_FULLSCREEN)
+            return
+        }
+
+        // Else we are in Launcher and can launch with the full icon stretch-and-split animation.
+        val launcher = Launcher.getLauncher(launchingIconView.context)
+        val dp = launcher.deviceProfile
+
+        // Create an AnimatorSet that will run both shell and launcher transitions together
+        val launchAnimation = AnimatorSet()
+
+        val appInfo = launchingIconView.info
+                .getContents()[launchFullscreenIndex] as WorkspaceItemInfo
+        val intentToLaunch = appInfo.intent.component?.packageName
+        var rootCandidate: Change? = null
+        for (change in transitionInfo.changes) {
+            val taskInfo: RunningTaskInfo = change.taskInfo ?: continue
+            val baseIntent = taskInfo.baseIntent.component?.packageName
+            if (TransitionUtil.isOpeningType(change.mode) &&
+                    taskInfo.windowingMode == WINDOWING_MODE_FULLSCREEN &&
+                    baseIntent == intentToLaunch) {
+                rootCandidate = change
+            }
+        }
+
+        // If we could not find a proper root candidate, something went wrong.
+        check(rootCandidate != null) { "Could not find a split root candidate" }
+
+        // Recurse up the tree until parent is null, then we've found our root.
+        var parentToken: WindowContainerToken? = rootCandidate.parent
+        while (parentToken != null) {
+            rootCandidate = transitionInfo.getChange(parentToken) ?: break
+            parentToken = rootCandidate.parent
+        }
+
+        // Make sure nothing weird happened, like getChange() returning null.
+        check(rootCandidate != null) { "Failed to find a root leash" }
+
+        // Create a new floating view in Launcher, positioned above the launching icon
+        val drawableArea = launchingIconView.iconDrawableArea
+        val appIcon = appInfo.newIcon(launchingIconView.context)
+        appIcon.setBounds(0, 0, dp.iconSizePx, dp.iconSizePx)
+
+        val floatingView =
+                FloatingAppPairView.getFloatingAppPairView(
+                        launcher,
+                        drawableArea,
+                        appIcon,
+                        null /*appIcon2*/,
+                        0 /*dividerPos*/
+                )
+        floatingView.bringToFront()
+        launchAnimation.play(
+                getIconLaunchValueAnimator(t, dp, finishCallback, launcher, floatingView,
+                        rootCandidate))
+        launchAnimation.start()
+    }
+
+    private fun getIconLaunchValueAnimator(t: Transaction,
+                                           dp: com.android.launcher3.DeviceProfile,
+                                           finishCallback: Runnable,
+                                           launcher: Launcher,
+                                           floatingView: FloatingAppPairView,
+                                           rootCandidate: Change) : ValueAnimator {
+        val progressUpdater = ValueAnimator.ofFloat(0f, 1f)
+        val timings = AnimUtils.getDeviceAppPairLaunchTimings(dp.isTablet)
+        progressUpdater.setDuration(timings.getDuration().toLong())
+        progressUpdater.interpolator = Interpolators.LINEAR
+
+        // Shell animation: the apps are revealed toward end of the launch animation
+        progressUpdater.addUpdateListener { valueAnimator: ValueAnimator ->
+            val progress =
+                    Interpolators.clampToProgress(
+                            Interpolators.LINEAR,
+                            valueAnimator.animatedFraction,
+                            timings.appRevealStartOffset,
+                            timings.appRevealEndOffset
+                    )
+
+            // Set the alpha of the shell layer (2 apps + divider)
+            t.setAlpha(rootCandidate.leash, progress)
+            t.apply()
+        }
+
+        progressUpdater.addUpdateListener(
+                object : MultiValueUpdateListener() {
+                    var mDx =
+                            FloatProp(
+                                    floatingView.startingPosition.left,
+                                    dp.widthPx / 2f - floatingView.startingPosition.width() / 2f,
+                                    Interpolators.clampToProgress(
+                                            timings.getStagedRectXInterpolator(),
+                                            timings.stagedRectSlideStartOffset,
+                                            timings.stagedRectSlideEndOffset
+                                    )
+                            )
+                    var mDy =
+                            FloatProp(
+                                    floatingView.startingPosition.top,
+                                    dp.heightPx / 2f - floatingView.startingPosition.height() / 2f,
+                                    Interpolators.clampToProgress(
+                                            Interpolators.EMPHASIZED,
+                                            timings.stagedRectSlideStartOffset,
+                                            timings.stagedRectSlideEndOffset
+                                    )
+                            )
+                    var mScaleX =
+                            FloatProp(
+                                    1f /* start */,
+                                    dp.widthPx / floatingView.startingPosition.width(),
+                                    Interpolators.clampToProgress(
+                                            Interpolators.EMPHASIZED,
+                                            timings.stagedRectSlideStartOffset,
+                                            timings.stagedRectSlideEndOffset
+                                    )
+                            )
+                    var mScaleY =
+                            FloatProp(
+                                    1f /* start */,
+                                    dp.heightPx / floatingView.startingPosition.height(),
+                                    Interpolators.clampToProgress(
+                                            Interpolators.EMPHASIZED,
+                                            timings.stagedRectSlideStartOffset,
+                                            timings.stagedRectSlideEndOffset
+                                    )
+                            )
+
+                    override fun onUpdate(percent: Float, initOnly: Boolean) {
+                        floatingView.progress = percent
+                        floatingView.x = mDx.value
+                        floatingView.y = mDy.value
+                        floatingView.scaleX = mScaleX.value
+                        floatingView.scaleY = mScaleY.value
+                        floatingView.invalidate()
+                    }
+                }
+        )
+        progressUpdater.addListener(
+                object : AnimatorListenerAdapter() {
+                    override fun onAnimationEnd(animation: Animator) {
+                        safeRemoveViewFromDragLayer(launcher, floatingView)
+                        finishCallback.run()
+                    }
+                }
+        )
+
+        return progressUpdater
+    }
+
+    /**
      * This is a scale-up-and-fade-in animation (34% to 100%) for launching an app in Overview when
      * there is no visible associated tile to expand from.
+     * [windowingMode] helps determine whether we are looking for a split or a single fullscreen
+     * [Change]
      */
     @VisibleForTesting
     fun composeScaleUpLaunchAnimation(
         transitionInfo: TransitionInfo,
         t: Transaction,
-        finishCallback: Runnable
+        finishCallback: Runnable,
+        windowingMode: Int
     ) {
         val launchAnimation = AnimatorSet()
         val progressUpdater = ValueAnimator.ofFloat(0f, 1f)
@@ -812,9 +941,8 @@
 
             // TODO (b/316490565): Replace this logic when SplitBounds is available to
             //  startAnimation() and we can know the precise taskIds of launching tasks.
-            // Find a change that has WINDOWING_MODE_MULTI_WINDOW.
             if (
-                taskInfo.windowingMode == WINDOWING_MODE_MULTI_WINDOW &&
+                taskInfo.windowingMode == windowingMode &&
                     (change.mode == TRANSIT_OPEN || change.mode == TRANSIT_TO_FRONT)
             ) {
                 // Found one!
diff --git a/quickstep/src/com/android/quickstep/views/FloatingAppPairBackground.kt b/quickstep/src/com/android/quickstep/views/FloatingAppPairBackground.kt
index 1c1e167..40937b3 100644
--- a/quickstep/src/com/android/quickstep/views/FloatingAppPairBackground.kt
+++ b/quickstep/src/com/android/quickstep/views/FloatingAppPairBackground.kt
@@ -37,17 +37,18 @@
  * animation. Consists of a rectangular background that splits into two, and two app icons that
  * increase in size during the animation.
  */
-class FloatingAppPairBackground(
-    context: Context,
-    private val floatingView: FloatingAppPairView, // the view that we will draw this background on
-    private val appIcon1: Drawable,
-    private val appIcon2: Drawable,
-    dividerPos: Int
+open class FloatingAppPairBackground(
+        context: Context,
+        // the view that we will draw this background on
+        protected val floatingView: FloatingAppPairView,
+        private val appIcon1: Drawable,
+        private val appIcon2: Drawable?,
+        dividerPos: Int
 ) : Drawable() {
     companion object {
         // Design specs -- app icons start small and expand during the animation
-        private val STARTING_ICON_SIZE_PX = Utilities.dpToPx(22f)
-        private val ENDING_ICON_SIZE_PX = Utilities.dpToPx(66f)
+        internal val STARTING_ICON_SIZE_PX = Utilities.dpToPx(22f)
+        internal val ENDING_ICON_SIZE_PX = Utilities.dpToPx(66f)
 
         // Null values to use with drawDoubleRoundRect(), since there doesn't seem to be any other
         // API for drawing rectangles with 4 different corner radii.
@@ -59,13 +60,13 @@
     private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG)
 
     // Animation interpolators
-    private val expandXInterpolator: Interpolator
-    private val expandYInterpolator: Interpolator
+    protected val expandXInterpolator: Interpolator
+    protected val expandYInterpolator: Interpolator
     private val cellSplitInterpolator: Interpolator
-    private val iconFadeInterpolator: Interpolator
+    protected val iconFadeInterpolator: Interpolator
 
     // Device-specific measurements
-    private val deviceCornerRadius: Float
+    protected val deviceCornerRadius: Float
     private val deviceHalfDividerSize: Float
     private val desiredSplitRatio: Float
 
@@ -217,7 +218,7 @@
         canvas.save()
         canvas.translate(changingIcon2Left, changingIconTop)
         canvas.scale(changingIconScaleX, changingIconScaleY)
-        appIcon2.alpha = changingIconAlpha
+        appIcon2!!.alpha = changingIconAlpha
         appIcon2.draw(canvas)
         canvas.restore()
     }
@@ -317,7 +318,7 @@
         canvas.save()
         canvas.translate(changingIconLeft, changingIcon2Top)
         canvas.scale(changingIconScaleX, changingIconScaleY)
-        appIcon2.alpha = changingIconAlpha
+        appIcon2!!.alpha = changingIconAlpha
         appIcon2.draw(canvas)
         canvas.restore()
     }
@@ -330,7 +331,7 @@
      * @param radii An array of 8 radii for the corners: top left x, top left y, top right x, top
      *   right y, bottom right x, and so on.
      */
-    private fun drawCustomRoundedRect(c: Canvas, rect: RectF, radii: FloatArray) {
+    protected fun drawCustomRoundedRect(c: Canvas, rect: RectF, radii: FloatArray) {
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
             // Canvas.drawDoubleRoundRect is supported from Q onward
             c.drawDoubleRoundRect(rect, radii, EMPTY_RECT, ARRAY_OF_ZEROES, backgroundPaint)
diff --git a/quickstep/src/com/android/quickstep/views/FloatingAppPairView.kt b/quickstep/src/com/android/quickstep/views/FloatingAppPairView.kt
index e90aa13..e8d1cc1 100644
--- a/quickstep/src/com/android/quickstep/views/FloatingAppPairView.kt
+++ b/quickstep/src/com/android/quickstep/views/FloatingAppPairView.kt
@@ -40,8 +40,8 @@
         fun getFloatingAppPairView(
             launcher: StatefulActivity<*>,
             originalView: View,
-            appIcon1: Drawable,
-            appIcon2: Drawable,
+            appIcon1: Drawable?,
+            appIcon2: Drawable?,
             dividerPos: Int
         ): FloatingAppPairView {
             val dragLayer: ViewGroup = launcher.getDragLayer()
@@ -64,8 +64,8 @@
     fun init(
         launcher: StatefulActivity<*>,
         originalView: View,
-        appIcon1: Drawable,
-        appIcon2: Drawable,
+        appIcon1: Drawable?,
+        appIcon2: Drawable?,
         dividerPos: Int
     ) {
         val viewBounds = Rect(0, 0, originalView.width, originalView.height)
@@ -92,7 +92,14 @@
         layoutParams = lp
 
         // Prepare to draw app pair icon background
-        background = FloatingAppPairBackground(context, this, appIcon1, appIcon2, dividerPos)
+        background = if (appIcon1 == null || appIcon2 == null) {
+            val iconToAnimate = appIcon1 ?: appIcon2
+            checkNotNull(iconToAnimate)
+            FloatingFullscreenAppPairBackground(context, this, iconToAnimate,
+                    dividerPos)
+        } else {
+            FloatingAppPairBackground(context, this, appIcon1, appIcon2, dividerPos)
+        }
         background.setBounds(0, 0, lp.width, lp.height)
     }
 
diff --git a/quickstep/src/com/android/quickstep/views/FloatingFullscreenAppPairBackground.kt b/quickstep/src/com/android/quickstep/views/FloatingFullscreenAppPairBackground.kt
new file mode 100644
index 0000000..8cd997f
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/FloatingFullscreenAppPairBackground.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2024 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.views
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.RectF
+import android.graphics.drawable.Drawable
+
+class FloatingFullscreenAppPairBackground(
+        context: Context,
+        floatingView: FloatingAppPairView,
+        private val iconToLaunch: Drawable,
+        dividerPos: Int) :
+        FloatingAppPairBackground(
+                context,
+                floatingView,
+                iconToLaunch,
+                null /*appIcon2*/,
+                dividerPos
+) {
+
+    /** Animates the background as if launching a fullscreen task. */
+    override fun draw(canvas: Canvas) {
+        val progress = floatingView.progress
+
+        // Since the entire floating app pair surface is scaling up during this animation, we
+        // scale down most of these drawn elements so that they appear the proper size on-screen.
+        val scaleFactorX = floatingView.scaleX
+        val scaleFactorY = floatingView.scaleY
+
+        // Get the bounds where we will draw the background image
+        val width = bounds.width().toFloat()
+        val height = bounds.height().toFloat()
+
+        // Get device-specific measurements
+        val cornerRadiusX = deviceCornerRadius / scaleFactorX
+        val cornerRadiusY = deviceCornerRadius / scaleFactorY
+
+        // Draw background
+        drawCustomRoundedRect(
+                canvas,
+                RectF(0f, 0f, width, height),
+                floatArrayOf(
+                        cornerRadiusX,
+                        cornerRadiusY,
+                        cornerRadiusX,
+                        cornerRadiusY,
+                        cornerRadiusX,
+                        cornerRadiusY,
+                        cornerRadiusX,
+                        cornerRadiusY,
+                )
+        )
+
+        // Calculate changing measurements for icon.
+        val changingIconSizeX =
+                (STARTING_ICON_SIZE_PX +
+                        ((ENDING_ICON_SIZE_PX - STARTING_ICON_SIZE_PX) *
+                                expandXInterpolator.getInterpolation(progress))) / scaleFactorX
+        val changingIconSizeY =
+                (STARTING_ICON_SIZE_PX +
+                        ((ENDING_ICON_SIZE_PX - STARTING_ICON_SIZE_PX) *
+                                expandYInterpolator.getInterpolation(progress))) / scaleFactorY
+
+        val changingIcon1Left = (width / 2f) - (changingIconSizeX / 2f)
+        val changingIconTop = (height / 2f) - (changingIconSizeY / 2f)
+        val changingIconScaleX = changingIconSizeX / iconToLaunch.bounds.width()
+        val changingIconScaleY = changingIconSizeY / iconToLaunch.bounds.height()
+        val changingIconAlpha =
+                (255 - (255 * iconFadeInterpolator.getInterpolation(progress))).toInt()
+
+        // Draw icon
+        canvas.save()
+        canvas.translate(changingIcon1Left, changingIconTop)
+        canvas.scale(changingIconScaleX, changingIconScaleY)
+        iconToLaunch.alpha = changingIconAlpha
+        iconToLaunch.draw(canvas)
+        canvas.restore()
+    }
+}
\ No newline at end of file
diff --git a/quickstep/tests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt b/quickstep/tests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt
index 68c9bf9..4bed7a0 100644
--- a/quickstep/tests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt
+++ b/quickstep/tests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt
@@ -17,6 +17,8 @@
 
 package com.android.quickstep.util
 
+import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
+import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW
 import android.graphics.Bitmap
 import android.graphics.drawable.Drawable
 import android.view.ContextThemeWrapper
@@ -39,8 +41,10 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.eq
 import org.mockito.kotlin.any
 import org.mockito.kotlin.doNothing
+import org.mockito.kotlin.doReturn
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.spy
 import org.mockito.kotlin.verify
@@ -255,6 +259,9 @@
         doNothing()
             .whenever(spySplitAnimationController)
             .composeIconSplitLaunchAnimator(any(), any(), any(), any())
+        doReturn(-1)
+                .whenever(spySplitAnimationController)
+                .hasChangesForBothAppPairs(any(), any())
 
         spySplitAnimationController.playSplitLaunchAnimation(
             null /* launchingTaskView */,
@@ -276,13 +283,45 @@
     }
 
     @Test
-    fun playsAppropriateSplitLaunchAnimation_playsIconLaunchFromTaskbarContextCorrectly() {
+    fun playsAppropriateSplitLaunchAnimation_playsIconFullscreenLaunchCorrectly() {
+        val spySplitAnimationController = spy(splitAnimationController)
+        whenever(mockAppPairIcon.context).thenReturn(mockContextThemeWrapper)
+        doNothing()
+                .whenever(spySplitAnimationController)
+                .composeFullscreenIconSplitLaunchAnimator(any(), any(), any(), any(), any())
+        doReturn(0)
+                .whenever(spySplitAnimationController)
+                .hasChangesForBothAppPairs(any(), any())
+
+        spySplitAnimationController.playSplitLaunchAnimation(
+                null /* launchingTaskView */,
+                mockAppPairIcon,
+                taskId,
+                taskId2,
+                null /* apps */,
+                null /* wallpapers */,
+                null /* nonApps */,
+                stateManager,
+                depthController,
+                transitionInfo,
+                transaction,
+                {} /* finishCallback */
+        )
+
+        verify(spySplitAnimationController)
+                .composeFullscreenIconSplitLaunchAnimator(any(), any(), any(), any(), eq(0))
+    }
+
+    @Test
+    fun playsAppropriateSplitLaunchAnimation_playsIconLaunchFromTaskbarCMultiWindow() {
         val spySplitAnimationController = spy(splitAnimationController)
         whenever(mockAppPairIcon.context).thenReturn(mockTaskbarActivityContext)
         doNothing()
             .whenever(spySplitAnimationController)
-            .composeScaleUpLaunchAnimation(any(), any(), any())
-
+            .composeScaleUpLaunchAnimation(any(), any(), any(), any())
+        doReturn(-1)
+                .whenever(spySplitAnimationController)
+                .hasChangesForBothAppPairs(any(), any())
         spySplitAnimationController.playSplitLaunchAnimation(
             null /* launchingTaskView */,
             mockAppPairIcon,
@@ -298,7 +337,37 @@
             {} /* finishCallback */
         )
 
-        verify(spySplitAnimationController).composeScaleUpLaunchAnimation(any(), any(), any())
+        verify(spySplitAnimationController).composeScaleUpLaunchAnimation(any(), any(), any(),
+                eq(WINDOWING_MODE_MULTI_WINDOW))
+    }
+
+    @Test
+    fun playsAppropriateSplitLaunchAnimation_playsIconLaunchFromTaskbarFullscreen() {
+        val spySplitAnimationController = spy(splitAnimationController)
+        whenever(mockAppPairIcon.context).thenReturn(mockTaskbarActivityContext)
+        doNothing()
+                .whenever(spySplitAnimationController)
+                .composeScaleUpLaunchAnimation(any(), any(), any(), any())
+        doReturn(0)
+                .whenever(spySplitAnimationController)
+                .hasChangesForBothAppPairs(any(), any())
+        spySplitAnimationController.playSplitLaunchAnimation(
+                null /* launchingTaskView */,
+                mockAppPairIcon,
+                taskId,
+                taskId2,
+                null /* apps */,
+                null /* wallpapers */,
+                null /* nonApps */,
+                stateManager,
+                depthController,
+                transitionInfo,
+                transaction,
+                {} /* finishCallback */
+        )
+
+        verify(spySplitAnimationController).composeScaleUpLaunchAnimation(any(), any(), any(),
+                eq(WINDOWING_MODE_FULLSCREEN))
     }
 
     @Test