Merge "Release predict_back animaton leash in Launcher." into main
diff --git a/aconfig/launcher.aconfig b/aconfig/launcher.aconfig
index d4cea8d..4f5b1a0 100644
--- a/aconfig/launcher.aconfig
+++ b/aconfig/launcher.aconfig
@@ -622,3 +622,10 @@
   description: "Enables custom theme manager in Launcher"
   bug: "381897614"
 }
+
+flag {
+  name: "enable_alt_tab_kqs_flatenning"
+  namespace: "lse_desktop_experience"
+  description: "Enable Alt + Tab KQS view to show apps in flattened structure"
+  bug: "382769617"
+}
diff --git a/quickstep/res/layout/keyboard_quick_switch_desktop_taskview.xml b/quickstep/res/layout/keyboard_quick_switch_desktop_taskview.xml
index 71c782d..db47ff0 100644
--- a/quickstep/res/layout/keyboard_quick_switch_desktop_taskview.xml
+++ b/quickstep/res/layout/keyboard_quick_switch_desktop_taskview.xml
@@ -48,13 +48,13 @@
 
             app:layout_constraintVertical_chainStyle="packed"
             app:layout_constraintTop_toTopOf="parent"
-            app:layout_constraintBottom_toTopOf="@id/text"
+            app:layout_constraintBottom_toTopOf="@id/small_text"
             app:layout_constraintStart_toStartOf="parent"
             app:layout_constraintEnd_toEndOf="parent"/>
 
         <TextView
             style="@style/KeyboardQuickSwitchText.OnTaskView"
-            android:id="@+id/text"
+            android:id="@+id/small_text"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:textAlignment="center"
diff --git a/quickstep/res/layout/taskbar_edu_features.xml b/quickstep/res/layout/taskbar_edu_features.xml
index a7bd184..aa1312e 100644
--- a/quickstep/res/layout/taskbar_edu_features.xml
+++ b/quickstep/res/layout/taskbar_edu_features.xml
@@ -20,7 +20,7 @@
     android:layout_height="wrap_content">
 
     <TextView
-        android:id="@+id/title"
+        android:id="@+id/taskbar_edu_title"
         style="@style/TextAppearance.TaskbarEduTooltip.Title"
         android:layout_width="0dp"
         android:layout_height="wrap_content"
@@ -42,7 +42,7 @@
 
         app:layout_constraintEnd_toEndOf="@id/splitscreen_text"
         app:layout_constraintStart_toStartOf="@id/splitscreen_text"
-        app:layout_constraintTop_toBottomOf="@id/title" />
+        app:layout_constraintTop_toBottomOf="@id/taskbar_edu_title" />
 
     <TextView
         android:id="@+id/splitscreen_text"
@@ -72,7 +72,7 @@
 
         app:layout_constraintEnd_toEndOf="@id/pinning_text"
         app:layout_constraintStart_toStartOf="@id/pinning_text"
-        app:layout_constraintTop_toBottomOf="@id/title" />
+        app:layout_constraintTop_toBottomOf="@id/taskbar_edu_title" />
 
     <TextView
         android:id="@+id/pinning_text"
@@ -96,7 +96,7 @@
 
         app:layout_constraintEnd_toEndOf="@id/suggestions_text"
         app:layout_constraintStart_toStartOf="@id/suggestions_text"
-        app:layout_constraintTop_toBottomOf="@id/title" />
+        app:layout_constraintTop_toBottomOf="@id/taskbar_edu_title" />
 
     <TextView
         android:id="@+id/suggestions_text"
diff --git a/quickstep/res/layout/taskbar_edu_pinning.xml b/quickstep/res/layout/taskbar_edu_pinning.xml
index 27a7b23..5937d62 100644
--- a/quickstep/res/layout/taskbar_edu_pinning.xml
+++ b/quickstep/res/layout/taskbar_edu_pinning.xml
@@ -19,7 +19,7 @@
     android:layout_height="wrap_content">
 
     <TextView
-        android:id="@+id/title"
+        android:id="@+id/taskbar_edu_title"
         style="@style/TextAppearance.TaskbarEduTooltip.Title"
         android:layout_width="0dp"
         android:layout_height="wrap_content"
@@ -37,7 +37,7 @@
         app:layout_constraintBottom_toTopOf="@id/pinning_text"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toBottomOf="@id/title"
+        app:layout_constraintTop_toBottomOf="@id/taskbar_edu_title"
         app:lottie_rawRes="@raw/taskbar_edu_pinning"
         app:lottie_autoPlay="true"
         app:lottie_loop="true" />
diff --git a/quickstep/res/layout/taskbar_edu_search.xml b/quickstep/res/layout/taskbar_edu_search.xml
index ca84f35..ec4d4b4 100644
--- a/quickstep/res/layout/taskbar_edu_search.xml
+++ b/quickstep/res/layout/taskbar_edu_search.xml
@@ -19,7 +19,7 @@
     android:layout_height="wrap_content">
 
     <TextView
-        android:id="@+id/title"
+        android:id="@+id/taskbar_edu_title"
         style="@style/TextAppearance.TaskbarEduTooltip.Title"
         android:layout_width="0dp"
         android:layout_height="wrap_content"
@@ -37,7 +37,7 @@
         app:layout_constraintBottom_toTopOf="@id/search_edu_text"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toBottomOf="@id/title"
+        app:layout_constraintTop_toBottomOf="@id/taskbar_edu_title"
         app:lottie_rawRes="@raw/taskbar_edu_search"
         app:lottie_autoPlay="true"
         app:lottie_loop="true" />
diff --git a/quickstep/res/layout/taskbar_edu_swipe.xml b/quickstep/res/layout/taskbar_edu_swipe.xml
index 3f5e819..9b4809e 100644
--- a/quickstep/res/layout/taskbar_edu_swipe.xml
+++ b/quickstep/res/layout/taskbar_edu_swipe.xml
@@ -19,7 +19,7 @@
     android:layout_height="wrap_content">
 
     <TextView
-        android:id="@+id/title"
+        android:id="@+id/taskbar_edu_title"
         style="@style/TextAppearance.TaskbarEduTooltip.Title"
         android:layout_width="0dp"
         android:layout_height="wrap_content"
@@ -36,7 +36,7 @@
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toBottomOf="@id/title"
+        app:layout_constraintTop_toBottomOf="@id/taskbar_edu_title"
         app:lottie_autoPlay="true"
         app:lottie_loop="true"
         app:lottie_rawRes="@raw/taskbar_edu_stashing" />
diff --git a/quickstep/res/values-de/strings.xml b/quickstep/res/values-de/strings.xml
index e937410..7986a55 100644
--- a/quickstep/res/values-de/strings.xml
+++ b/quickstep/res/values-de/strings.xml
@@ -118,7 +118,7 @@
     <string name="taskbar_edu_pinning_title" msgid="210102174154211712">"Taskleiste immer anzeigen"</string>
     <string name="taskbar_edu_pinning_standalone" msgid="2636919474366410467">"Damit die Taskleiste immer unten angezeigt wird, halte den Teiler gedrückt"</string>
     <string name="taskbar_search_edu_title" msgid="5569194922234364530">"Aktionstaste gedrückt halten, um auf dem Bildschirm zu suchen"</string>
-    <string name="taskbar_edu_search_disclosure" msgid="8734536088447779686">"Dieses Produkt verwendet den ausgewählten Teil deines Bildschirms für die Suche. Es gelten die <xliff:g id="BEGIN_PRIVACY_LINK">&lt;a href="%1$s"&gt;</xliff:g>Datenschutzerklärung<xliff:g id="END_PRIVACY_LINK">&lt;/a&gt;</xliff:g> und die <xliff:g id="BEGIN_TOS_LINK">&lt;a href="%2$s"&gt;</xliff:g>Nutzungsbedingungen<xliff:g id="END_TOS_LINK">&lt;/a&gt;</xliff:g> von Google."</string>
+    <string name="taskbar_edu_search_disclosure" msgid="8734536088447779686">"Dieses Produkt sucht dann anhand des ausgewählten Teils deines Displays nach weiteren Informationen. Es gelten die <xliff:g id="BEGIN_PRIVACY_LINK">&lt;a href="%1$s"&gt;</xliff:g>Datenschutzerklärung<xliff:g id="END_PRIVACY_LINK">&lt;/a&gt;</xliff:g> und die <xliff:g id="BEGIN_TOS_LINK">&lt;a href="%2$s"&gt;</xliff:g>Nutzungsbedingungen<xliff:g id="END_TOS_LINK">&lt;/a&gt;</xliff:g> von Google."</string>
     <string name="taskbar_edu_close" msgid="887022990168191073">"Schließen"</string>
     <string name="taskbar_edu_done" msgid="6880178093977704569">"Fertig"</string>
     <string name="taskbar_button_home" msgid="2151398979630664652">"Startbildschirm"</string>
diff --git a/quickstep/res/values-es-rUS/strings.xml b/quickstep/res/values-es-rUS/strings.xml
index 5ba3212..6cd544c 100644
--- a/quickstep/res/values-es-rUS/strings.xml
+++ b/quickstep/res/values-es-rUS/strings.xml
@@ -118,7 +118,7 @@
     <string name="taskbar_edu_pinning_title" msgid="210102174154211712">"Mostrar siempre la Barra de tareas"</string>
     <string name="taskbar_edu_pinning_standalone" msgid="2636919474366410467">"Mantén presionado el divisor para mostrar siempre la Barra de tareas en la parte inferior de la pantalla"</string>
     <string name="taskbar_search_edu_title" msgid="5569194922234364530">"Mantén presionada la tecla de acción para buscar qué hay en la pantalla"</string>
-    <string name="taskbar_edu_search_disclosure" msgid="8734536088447779686">"Este producto usa la parte seleccionada de la pantalla para buscar. Se aplican la <xliff:g id="BEGIN_PRIVACY_LINK">&lt;a href="%1$s"&gt;</xliff:g>Política de Privacidad<xliff:g id="END_PRIVACY_LINK">&lt;/a&gt;</xliff:g> y las <xliff:g id="BEGIN_TOS_LINK">&lt;a href="%2$s"&gt;</xliff:g>Condiciones del Servicio<xliff:g id="END_TOS_LINK">&lt;/a&gt;</xliff:g> de Google."</string>
+    <string name="taskbar_edu_search_disclosure" msgid="8734536088447779686">"Este producto usa la parte seleccionada de la pantalla para hacer búsquedas. Se aplican la <xliff:g id="BEGIN_PRIVACY_LINK">&lt;a href="%1$s"&gt;</xliff:g>Política de Privacidad<xliff:g id="END_PRIVACY_LINK">&lt;/a&gt;</xliff:g> y las <xliff:g id="BEGIN_TOS_LINK">&lt;a href="%2$s"&gt;</xliff:g>Condiciones del Servicio<xliff:g id="END_TOS_LINK">&lt;/a&gt;</xliff:g> de Google."</string>
     <string name="taskbar_edu_close" msgid="887022990168191073">"Cerrar"</string>
     <string name="taskbar_edu_done" msgid="6880178093977704569">"Listo"</string>
     <string name="taskbar_button_home" msgid="2151398979630664652">"Botón de inicio"</string>
diff --git a/quickstep/res/values-gl/strings.xml b/quickstep/res/values-gl/strings.xml
index edd55d5..df665ed 100644
--- a/quickstep/res/values-gl/strings.xml
+++ b/quickstep/res/values-gl/strings.xml
@@ -96,7 +96,7 @@
     <string name="action_share" msgid="2648470652637092375">"Compartir"</string>
     <string name="action_screenshot" msgid="8171125848358142917">"Facer captura"</string>
     <string name="action_split" msgid="2098009717623550676">"Dividir"</string>
-    <string name="action_save_app_pair" msgid="5974823919237645229">"Gardar parella apps"</string>
+    <string name="action_save_app_pair" msgid="5974823919237645229">"Gardar parella de apps"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Para usar a pantalla dividida, toca outra app"</string>
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Escolle outra aplicación para usar a pantalla dividida."</string>
     <string name="toast_split_select_app_cancel" msgid="1939025102486630426">"Cancelar"</string>
diff --git a/quickstep/res/values-pt/strings.xml b/quickstep/res/values-pt/strings.xml
index ede50cc..fad3164 100644
--- a/quickstep/res/values-pt/strings.xml
+++ b/quickstep/res/values-pt/strings.xml
@@ -118,7 +118,7 @@
     <string name="taskbar_edu_pinning_title" msgid="210102174154211712">"Sempre mostrar a Barra de tarefas"</string>
     <string name="taskbar_edu_pinning_standalone" msgid="2636919474366410467">"Toque e pressione o divisor para sempre mostrar a Barra de tarefas na parte de baixo da tela"</string>
     <string name="taskbar_search_edu_title" msgid="5569194922234364530">"Toque na tecla de ação e pressione para pesquisar o que está na tela"</string>
-    <string name="taskbar_edu_search_disclosure" msgid="8734536088447779686">"O produto usa a parte selecionada da tela para pesquisar. O uso desses dados está sujeito à <xliff:g id="BEGIN_PRIVACY_LINK">&lt;a href="%1$s"&gt;</xliff:g>Política de Privacidade<xliff:g id="END_PRIVACY_LINK">&lt;/a&gt;</xliff:g> e aos <xliff:g id="BEGIN_TOS_LINK">&lt;a href="%2$s"&gt;</xliff:g>Termos de Serviço<xliff:g id="END_TOS_LINK">&lt;/a&gt;</xliff:g> do Google."</string>
+    <string name="taskbar_edu_search_disclosure" msgid="8734536088447779686">"Este produto usa a parte selecionada da tela para pesquisar. O uso desses dados está sujeito à <xliff:g id="BEGIN_PRIVACY_LINK">&lt;a href="%1$s"&gt;</xliff:g>Política de Privacidade<xliff:g id="END_PRIVACY_LINK">&lt;/a&gt;</xliff:g> e aos <xliff:g id="BEGIN_TOS_LINK">&lt;a href="%2$s"&gt;</xliff:g>Termos de Serviço<xliff:g id="END_TOS_LINK">&lt;/a&gt;</xliff:g> do Google."</string>
     <string name="taskbar_edu_close" msgid="887022990168191073">"Fechar"</string>
     <string name="taskbar_edu_done" msgid="6880178093977704569">"Concluído"</string>
     <string name="taskbar_button_home" msgid="2151398979630664652">"Início"</string>
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/statehandlers/DesktopVisibilityController.kt b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.kt
index 3773d02..37e8d4d 100644
--- a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.kt
+++ b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.kt
@@ -18,6 +18,7 @@
 import android.content.Context
 import android.os.Debug
 import android.util.Log
+import android.util.Slog
 import android.util.SparseArray
 import android.view.Display.DEFAULT_DISPLAY
 import android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY
@@ -152,7 +153,8 @@
             return areDesktopTasksVisible()
         }
 
-        val isInDesktopMode = displaysDesksConfigsMap[displayId].activeDeskId != INACTIVE_DESK_ID
+        val activeDeskId = getDisplayDeskConfig(displayId)?.activeDeskId ?: INACTIVE_DESK_ID
+        val isInDesktopMode = activeDeskId != INACTIVE_DESK_ID
         if (DEBUG) {
             Log.d(TAG, "isInDesktopMode: $isInDesktopMode")
         }
@@ -409,18 +411,16 @@
         }
     }
 
-    private fun getDisplayDeskConfig(displayId: Int): DisplayDeskConfig {
-        return checkNotNull(displaysDesksConfigsMap[displayId]) {
-            "Expected non-null desk config for display: $displayId"
-        }
-    }
+    private fun getDisplayDeskConfig(displayId: Int) =
+        displaysDesksConfigsMap[displayId]
+            ?: null.also { Slog.e(TAG, "Expected non-null desk config for display: $displayId") }
 
     private fun onCanCreateDesksChanged(displayId: Int, canCreateDesks: Boolean) {
         if (!DesktopModeStatus.enableMultipleDesktops(context)) {
             return
         }
 
-        getDisplayDeskConfig(displayId).canCreateDesks = canCreateDesks
+        getDisplayDeskConfig(displayId)?.canCreateDesks = canCreateDesks
     }
 
     private fun onDeskAdded(displayId: Int, deskId: Int) {
@@ -428,7 +428,7 @@
             return
         }
 
-        getDisplayDeskConfig(displayId).also {
+        getDisplayDeskConfig(displayId)?.also {
             check(it.deskIds.add(deskId)) {
                 "Found a duplicate desk Id: $deskId on display: $displayId"
             }
@@ -440,7 +440,7 @@
             return
         }
 
-        getDisplayDeskConfig(displayId).also {
+        getDisplayDeskConfig(displayId)?.also {
             check(it.deskIds.remove(deskId)) {
                 "Removing non-existing desk Id: $deskId on display: $displayId"
             }
@@ -457,7 +457,7 @@
 
         val wasInDesktopMode = isInDesktopModeAndNotInOverview(displayId)
 
-        getDisplayDeskConfig(displayId).also {
+        getDisplayDeskConfig(displayId)?.also {
             check(oldActiveDesk == it.activeDeskId) {
                 "Mismatch between the Shell's oldActiveDesk: $oldActiveDesk, and Launcher's: ${it.activeDeskId}"
             }
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java
index ce96556..bf5c0c8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java
@@ -104,9 +104,18 @@
         mIcon2 = findViewById(R.id.icon_2);
         mContent = findViewById(R.id.content);
 
-        Resources resources = mContext.getResources();
-
         Preconditions.assertNotNull(mContent);
+
+        TypefaceUtils.setTypeface(
+                mContent.findViewById(R.id.large_text),
+                TypefaceUtils.FONT_FAMILY_HEADLINE_LARGE_EMPHASIZED
+        );
+        TypefaceUtils.setTypeface(
+                mContent.findViewById(R.id.small_text),
+                TypefaceUtils.FONT_FAMILY_LABEL_LARGE_BASELINE
+        );
+
+        Resources resources = mContext.getResources();
         mBorderAnimator = BorderAnimator.createScalingBorderAnimator(
                 /* borderRadiusPx= */ mBorderRadius != INVALID_BORDER_RADIUS
                         ? mBorderRadius
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
index 4581119..4b4d68d 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
@@ -163,6 +163,10 @@
                 R.dimen.keyboard_quick_switch_view_small_spacing);
         mOutlineRadius = resources.getDimensionPixelSize(R.dimen.keyboard_quick_switch_view_radius);
         mIsRtl = Utilities.isRtl(resources);
+
+        TypefaceUtils.setTypeface(
+                mNoRecentItemsPane.findViewById(R.id.no_recent_items_text),
+                TypefaceUtils.FONT_FAMILY_LABEL_LARGE_BASELINE);
     }
 
     private void registerOnBackInvokedCallback() {
@@ -310,7 +314,7 @@
                     layoutInflater,
                     previousTaskView);
 
-            desktopButton.<TextView>findViewById(R.id.text).setText(
+            desktopButton.<TextView>findViewById(R.id.small_text).setText(
                     resources.getString(R.string.quick_switch_desktop));
         }
         mDisplayingRecentTasks = !groupTasks.isEmpty() || useDesktopTaskView;
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/StashedHandleViewController.java b/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java
index b6b090c..2e5bebc 100644
--- a/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java
@@ -119,7 +119,8 @@
         mControllers = controllers;
         DeviceProfile deviceProfile = mActivity.getDeviceProfile();
         Resources resources = mActivity.getResources();
-        if (mActivity.isPhoneGestureNavMode() || mActivity.isTinyTaskbar()) {
+        if (mActivity.isPhoneGestureNavMode() || mActivity.isTinyTaskbar()
+                || mActivity.isBubbleBarOnPhone()) {
             mTaskbarSize = resources.getDimensionPixelSize(R.dimen.taskbar_phone_size);
             mStashedHandleWidth =
                     resources.getDimensionPixelSize(R.dimen.taskbar_stashed_small_screen);
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index b9d1275..d1e63f3 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -47,7 +47,8 @@
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING;
 import static com.android.window.flags.Flags.enableStartLaunchTransitionFromTaskbarBugfix;
-import static com.android.window.flags.Flags.enableTaskbarConnectedDisplays;
+import static com.android.wm.shell.Flags.enableBubbleBar;
+import static com.android.wm.shell.Flags.enableBubbleBarOnPhones;
 import static com.android.wm.shell.Flags.enableTinyTaskbar;
 
 import static java.lang.invoke.MethodHandles.Lookup.PROTECTED;
@@ -77,6 +78,7 @@
 import android.view.WindowManager;
 import android.widget.FrameLayout;
 import android.widget.Toast;
+import android.window.DesktopExperienceFlags;
 import android.window.DesktopModeFlags;
 import android.window.RemoteTransition;
 
@@ -298,9 +300,10 @@
         // If Bubble bar is present, TaskbarControllers depends on it so build it first.
         Optional<BubbleControllers> bubbleControllersOptional = Optional.empty();
         BubbleBarController.onTaskbarRecreated();
+        final boolean deviceBubbleBarEnabled = enableBubbleBarOnPhones()
+                || (!mDeviceProfile.isPhone && !mDeviceProfile.isVerticalBarLayout());
         if (BubbleBarController.isBubbleBarEnabled()
-                && !mDeviceProfile.isPhone
-                && !mDeviceProfile.isVerticalBarLayout()
+                && deviceBubbleBarEnabled
                 && bubbleBarView != null
         ) {
             Optional<BubbleStashedHandleViewController> bubbleHandleController = Optional.empty();
@@ -434,8 +437,9 @@
                     .setIsTransientTaskbar(true)
                     .build();
         }
-        mNavMode = (enableTaskbarConnectedDisplays() && !mIsPrimaryDisplay)
-                ? NavigationMode.THREE_BUTTONS : DisplayController.getNavigationMode(this);
+        mNavMode = (DesktopExperienceFlags.ENABLE_TASKBAR_CONNECTED_DISPLAYS.isTrue()
+                && !mIsPrimaryDisplay) ? NavigationMode.THREE_BUTTONS
+                : DisplayController.getNavigationMode(this);
 
     }
 
@@ -511,6 +515,10 @@
         return enableTinyTaskbar() && mDeviceProfile.isPhone && mDeviceProfile.isTaskbarPresent;
     }
 
+    public boolean isBubbleBarOnPhone() {
+        return enableBubbleBarOnPhones() && enableBubbleBar() && mDeviceProfile.isPhone;
+    }
+
     /**
      * Returns {@code true} iff bubble bar is enabled (but not necessarily visible /
      * containing bubbles).
@@ -1533,9 +1541,9 @@
         return new RemoteTransition(
                 new DesktopAppLaunchTransition(
                         this,
-                        getMainExecutor(),
                         appLaunchType,
-                        cujType
+                        cujType,
+                        getMainExecutor()
                 ),
                 "TaskbarDesktopAppLaunch");
     }
@@ -1558,7 +1566,10 @@
      */
     private void launchFromInAppTaskbar(@Nullable RecentsView recents,
             @Nullable View launchingIconView, List<? extends ItemInfo> itemInfos) {
-        if (recents == null) {
+        boolean launchedFromExternalDisplay =
+                DesktopExperienceFlags.ENABLE_TASKBAR_CONNECTED_DISPLAYS.isTrue()
+                        && !mIsPrimaryDisplay;
+        if (recents == null && !launchedFromExternalDisplay) {
             return;
         }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt
index e44bce1..6d23853 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt
@@ -108,7 +108,7 @@
     fun updateStashedHandleWidth(context: TaskbarActivityContext, res: Resources) {
         stashedHandleWidth =
             res.getDimensionPixelSize(
-                if (context.isPhoneMode || context.isTinyTaskbar) {
+                if (context.isPhoneMode || context.isTinyTaskbar || context.isBubbleBarOnPhone) {
                     R.dimen.taskbar_stashed_small_screen
                 } else {
                     R.dimen.taskbar_stashed_handle_width
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
index b91f512..d531e2c 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
@@ -52,6 +52,7 @@
 import android.window.SurfaceSyncGroup;
 
 import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
 
 import com.android.app.animation.Interpolators;
 import com.android.internal.logging.InstanceId;
@@ -75,6 +76,7 @@
 import com.android.launcher3.popup.PopupContainerWithArrow;
 import com.android.launcher3.shortcuts.DeepShortcutView;
 import com.android.launcher3.shortcuts.ShortcutDragPreviewProvider;
+import com.android.launcher3.taskbar.bubbles.BubbleBarViewController;
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.shared.TestProtocol;
 import com.android.launcher3.util.DisplayController;
@@ -517,6 +519,7 @@
         return mIsSystemDragInProgress;
     }
 
+    @VisibleForTesting
     private void maybeOnDragEnd() {
         if (!isDragging()) {
             ((BubbleTextView) mDragObject.originalView).setIconDisabled(false);
@@ -524,17 +527,38 @@
                     TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_DRAGGING, false);
             mActivity.onDragEnd();
             if (mReturnAnimator == null) {
+                // If an item is dropped on the bubble bar, the bubble bar handles the drop,
+                // so it should not collapse along with the taskbar.
+                boolean droppedOnBubbleBar = notifyBubbleBarItemDropped();
                 // Upon successful drag, immediately stash taskbar.
                 // Note, this must be done last to ensure no AutohideSuspendFlags are active, as
                 // that will prevent us from stashing until the timeout.
-                mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true);
-
+                mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(
+                        /* stash = */ true,
+                        /* shouldBubblesFollow = */ !droppedOnBubbleBar
+                );
                 mActivity.getStatsLogManager().logger().withItemInfo(mDragObject.dragInfo)
                         .log(LAUNCHER_APP_LAUNCH_DRAGDROP);
             }
         }
     }
 
+    /**
+     * Exits the Bubble Bar drop target mode if applicable.
+     *
+     * @return {@code true} if drop target mode was active.
+     */
+    private boolean notifyBubbleBarItemDropped() {
+        return mControllers.bubbleControllers.map(bc -> {
+            BubbleBarViewController bubbleBarViewController = bc.bubbleBarViewController;
+            boolean showingDropTarget = bubbleBarViewController.isShowingDropTarget();
+            if (showingDropTarget) {
+                bubbleBarViewController.onItemDroppedInBubbleBarDragZone();
+            }
+            return showingDropTarget;
+        }).orElse(false);
+    }
+
     @Override
     protected void endDrag() {
         if (mDisallowGlobalDrag) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt
index 26a552e..b4ffb74 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt
@@ -141,6 +141,10 @@
         tooltipStep = TOOLTIP_STEP_FEATURES
         inflateTooltip(R.layout.taskbar_edu_swipe)
         tooltip?.run {
+            TypefaceUtils.setTypeface(
+                requireViewById(R.id.taskbar_edu_title),
+                TypefaceUtils.FONT_FAMILY_HEADLINE_SMALL_EMPHASIZED,
+            )
             requireViewById<LottieAnimationView>(R.id.swipe_animation).supportLightTheme()
             show()
         }
@@ -180,6 +184,23 @@
                 pinningEdu.visibility = GONE
             }
 
+            TypefaceUtils.setTypeface(
+                requireViewById(R.id.taskbar_edu_title),
+                TypefaceUtils.FONT_FAMILY_HEADLINE_SMALL_EMPHASIZED,
+            )
+            TypefaceUtils.setTypeface(
+                requireViewById(R.id.splitscreen_text),
+                TypefaceUtils.FONT_FAMILY_BODY_MEDIUM_BASELINE,
+            )
+            TypefaceUtils.setTypeface(
+                requireViewById(R.id.suggestions_text),
+                TypefaceUtils.FONT_FAMILY_BODY_MEDIUM_BASELINE,
+            )
+            TypefaceUtils.setTypeface(
+                requireViewById(R.id.pinning_text),
+                TypefaceUtils.FONT_FAMILY_BODY_MEDIUM_BASELINE,
+            )
+
             // Set up layout parameters.
             content.updateLayoutParams { width = MATCH_PARENT }
             updateLayoutParams<MarginLayoutParams> {
@@ -231,6 +252,15 @@
             requireViewById<LottieAnimationView>(R.id.standalone_pinning_animation)
                 .supportLightTheme()
 
+            TypefaceUtils.setTypeface(
+                requireViewById(R.id.taskbar_edu_title),
+                TypefaceUtils.FONT_FAMILY_HEADLINE_SMALL_EMPHASIZED,
+            )
+            TypefaceUtils.setTypeface(
+                requireViewById(R.id.pinning_text),
+                TypefaceUtils.FONT_FAMILY_BODY_MEDIUM_BASELINE,
+            )
+
             updateLayoutParams<BaseDragLayer.LayoutParams> {
                 if (DisplayController.isTransientTaskbar(activityContext)) {
                     bottomMargin += activityContext.deviceProfile.taskbarHeight
@@ -276,6 +306,13 @@
             allowTouchDismissal = true
             requireViewById<LottieAnimationView>(R.id.search_edu_animation).supportLightTheme()
             val eduSubtitle: TextView = requireViewById(R.id.search_edu_text)
+
+            TypefaceUtils.setTypeface(
+                requireViewById(R.id.taskbar_edu_title),
+                TypefaceUtils.FONT_FAMILY_HEADLINE_SMALL_EMPHASIZED,
+            )
+            TypefaceUtils.setTypeface(eduSubtitle, TypefaceUtils.FONT_FAMILY_BODY_SMALL_BASELINE)
+
             showDisclosureText(eduSubtitle)
             updateLayoutParams<BaseDragLayer.LayoutParams> {
                 if (DisplayController.isTransientTaskbar(activityContext)) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
index 96bcffd..46802c3 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
@@ -32,7 +32,6 @@
 import static com.android.launcher3.util.FlagDebugUtils.formatFlagChange;
 import static com.android.quickstep.util.SystemActionConstants.ACTION_SHOW_TASKBAR;
 import static com.android.quickstep.util.SystemActionConstants.SYSTEM_ACTION_ID_TASKBAR;
-import static com.android.window.flags.Flags.enableTaskbarConnectedDisplays;
 
 import android.annotation.SuppressLint;
 import android.app.PendingIntent;
@@ -53,6 +52,7 @@
 import android.view.MotionEvent;
 import android.view.WindowManager;
 import android.widget.FrameLayout;
+import android.window.DesktopExperienceFlags;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -118,11 +118,12 @@
             Settings.Secure.NAV_BAR_KIDS_MODE);
 
     private final Context mBaseContext;
+    private TaskbarNavButtonCallbacks mNavCallbacks;
     // TODO: Remove this during the connected displays lifecycle refactor.
     private final Context mPrimaryWindowContext;
     private WindowManager mPrimaryWindowManager;
-    private final TaskbarNavButtonController mDefaultNavButtonController;
-    private final ComponentCallbacks mDefaultComponentCallbacks;
+    private TaskbarNavButtonController mPrimaryNavButtonController;
+    private ComponentCallbacks mPrimaryComponentCallbacks;
 
     private final SimpleBroadcastReceiver mShutdownReceiver;
 
@@ -140,6 +141,11 @@
     private final SparseArray<FrameLayout> mRootLayouts = new SparseArray<>();
     /** DisplayId - {@link Boolean} map indicating if RootLayout was added to window. */
     private final SparseBooleanArray mAddedRootLayouts = new SparseBooleanArray();
+    /** DisplayId - {@link TaskbarNavButtonController} map for Connected Display. */
+    private final SparseArray<TaskbarNavButtonController> mNavButtonControllers =
+            new SparseArray<>();
+    /** DisplayId - {@link ComponentCallbacks} map for Connected Display. */
+    private final SparseArray<ComponentCallbacks> mComponentCallbacks = new SparseArray<>();
     private StatefulActivity mActivity;
     private RecentsViewContainer mRecentsViewContainer;
 
@@ -158,27 +164,35 @@
     private class RecreationListener implements DisplayController.DisplayInfoChangeListener {
         @Override
         public void onDisplayInfoChanged(Context context, DisplayController.Info info, int flags) {
-
             if ((flags & CHANGE_DENSITY) != 0) {
-                Log.d(NULL_TASKBAR_ROOT_LAYOUT_TAG, "Display density changed");
+                debugTaskbarManager("onDisplayInfoChanged - Display density changed",
+                        context.getDisplayId());
             }
             if ((flags & CHANGE_NAVIGATION_MODE) != 0) {
-                Log.d(NULL_TASKBAR_ROOT_LAYOUT_TAG, "Navigation mode changed");
+                debugTaskbarManager("onDisplayInfoChanged - Navigation mode changed",
+                        context.getDisplayId());
             }
             if ((flags & CHANGE_DESKTOP_MODE) != 0) {
-                Log.d(NULL_TASKBAR_ROOT_LAYOUT_TAG, "Desktop mode changed");
+                debugTaskbarManager("onDisplayInfoChanged - Desktop mode changed",
+                        context.getDisplayId());
             }
             if ((flags & CHANGE_TASKBAR_PINNING) != 0) {
-                Log.d(NULL_TASKBAR_ROOT_LAYOUT_TAG, "Taskbar pinning changed");
+                debugTaskbarManager("onDisplayInfoChanged - Taskbar pinning changed",
+                        context.getDisplayId());
             }
 
             if ((flags & (CHANGE_DENSITY | CHANGE_NAVIGATION_MODE | CHANGE_DESKTOP_MODE
                     | CHANGE_TASKBAR_PINNING)) != 0) {
+                debugTaskbarManager("onDisplayInfoChanged - Recreating Taskbar!",
+                        context.getDisplayId());
                 recreateTaskbar();
             }
         }
     }
-    private final SettingsCache.OnChangeListener mOnSettingsChangeListener = c -> recreateTaskbar();
+    private final SettingsCache.OnChangeListener mOnSettingsChangeListener = c -> {
+        debugTaskbarManager("Settings changed! Recreating Taskbar!");
+        recreateTaskbar();
+    };
 
     private boolean mUserUnlocked = false;
 
@@ -191,6 +205,7 @@
         @Override
         public void run() {
             int displayId = getDefaultDisplayId();
+            debugTaskbarManager("mActivityOnDestroyCallback running!", displayId);
             if (mActivity != null) {
                 displayId = mActivity.getDisplayId();
                 mActivity.removeOnDeviceProfileChangeListener(
@@ -204,7 +219,6 @@
                 mRecentsViewContainer = null;
             }
             mActivity = null;
-            debugWhyTaskbarNotDestroyed("clearActivity");
             TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId);
             if (taskbar != null) {
                 taskbar.setUIController(TaskbarUIController.DEFAULT);
@@ -217,28 +231,27 @@
             new UnfoldTransitionProgressProvider.TransitionProgressListener() {
                 @Override
                 public void onTransitionStarted() {
-                    Log.d(TASKBAR_NOT_DESTROYED_TAG,
-                            "fold/unfold transition started getting called.");
+                    debugTaskbarManager("fold/unfold transition started getting called.");
                 }
 
                 @Override
                 public void onTransitionProgress(float progress) {
-                    Log.d(TASKBAR_NOT_DESTROYED_TAG,
-                            "fold/unfold transition progress : " + progress);
+                    debugTaskbarManager(
+                            "fold/unfold transition progress getting called. | progress="
+                                    + progress);
                 }
 
                 @Override
                 public void onTransitionFinishing() {
-                    Log.d(TASKBAR_NOT_DESTROYED_TAG,
+                    debugTaskbarManager(
                             "fold/unfold transition finishing getting called.");
 
                 }
 
                 @Override
                 public void onTransitionFinished() {
-                    Log.d(TASKBAR_NOT_DESTROYED_TAG,
+                    debugTaskbarManager(
                             "fold/unfold transition finished getting called.");
-
                 }
             };
 
@@ -250,20 +263,22 @@
             RecentsDisplayModel recentsDisplayModel) {
         mBaseContext = context;
         mAllAppsActionManager = allAppsActionManager;
+        mNavCallbacks = navCallbacks;
         mRecentsDisplayModel = recentsDisplayModel;
-        mPrimaryWindowContext = createWindowContext(getDefaultDisplayId());
-        if (enableTaskbarNoRecreate()) {
-            mPrimaryWindowManager = mPrimaryWindowContext.getSystemService(WindowManager.class);
-            createTaskbarRootLayout(getDefaultDisplayId());
-        }
-        mDefaultNavButtonController = createDefaultNavButtonController(context, navCallbacks);
-        mDefaultComponentCallbacks = createDefaultComponentCallbacks();
+
+        // Set up primary display.
+        int primaryDisplayId = getDefaultDisplayId();
+        debugTaskbarManager("TaskbarManager constructor", primaryDisplayId);
+        mPrimaryWindowContext = createWindowContext(primaryDisplayId);
+        mPrimaryWindowManager = mPrimaryWindowContext.getSystemService(WindowManager.class);
+        createTaskbarRootLayout(primaryDisplayId);
+        createNavButtonController(primaryDisplayId);
+        createAndRegisterComponentCallbacks(primaryDisplayId);
+
         SettingsCache.INSTANCE.get(mPrimaryWindowContext)
                 .register(USER_SETUP_COMPLETE_URI, mOnSettingsChangeListener);
         SettingsCache.INSTANCE.get(mPrimaryWindowContext)
                 .register(NAV_BAR_KIDS_MODE, mOnSettingsChangeListener);
-        Log.d(TASKBAR_NOT_DESTROYED_TAG, "registering component callbacks from constructor.");
-        mPrimaryWindowContext.registerComponentCallbacks(mDefaultComponentCallbacks);
         mShutdownReceiver =
                 new SimpleBroadcastReceiver(
                         mPrimaryWindowContext, UI_HELPER_EXECUTOR, i -> destroyAllTaskbars());
@@ -281,82 +296,10 @@
             mTaskbarBroadcastReceiver.register(RECEIVER_NOT_EXPORTED, ACTION_SHOW_TASKBAR);
         });
 
-        debugWhyTaskbarNotDestroyed("TaskbarManager created");
+        debugTaskbarManager("TaskbarManager created");
         recreateTaskbar();
     }
 
-    @NonNull
-    private TaskbarNavButtonController createDefaultNavButtonController(Context context,
-            TaskbarNavButtonCallbacks navCallbacks) {
-        return new TaskbarNavButtonController(
-                context,
-                navCallbacks,
-                SystemUiProxy.INSTANCE.get(mPrimaryWindowContext),
-                new Handler(),
-                new ContextualSearchInvoker(mPrimaryWindowContext));
-    }
-
-    private ComponentCallbacks createDefaultComponentCallbacks() {
-        return new ComponentCallbacks() {
-            private Configuration mOldConfig =
-                    mPrimaryWindowContext.getResources().getConfiguration();
-
-            @Override
-            public void onConfigurationChanged(Configuration newConfig) {
-                Trace.instantForTrack(Trace.TRACE_TAG_APP, "TaskbarManager",
-                        "onConfigurationChanged: " + newConfig);
-                debugWhyTaskbarNotDestroyed(
-                        "TaskbarManager#mComponentCallbacks.onConfigurationChanged: " + newConfig);
-                // TODO: adapt this logic to be specific to different displays.
-                DeviceProfile dp = mUserUnlocked
-                        ? LauncherAppState.getIDP(mPrimaryWindowContext).getDeviceProfile(
-                        mPrimaryWindowContext)
-                        : null;
-                int configDiff = mOldConfig.diff(newConfig) & ~SKIP_RECREATE_CONFIG_CHANGES;
-
-                if ((configDiff & ActivityInfo.CONFIG_UI_MODE) != 0) {
-                    Log.d(ILLEGAL_ARGUMENT_WM_ADD_VIEW, "onConfigurationChanged: theme changed");
-                    // Only recreate for theme changes, not other UI mode changes such as docking.
-                    int oldUiNightMode = (mOldConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK);
-                    int newUiNightMode = (newConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK);
-                    if (oldUiNightMode == newUiNightMode) {
-                        configDiff &= ~ActivityInfo.CONFIG_UI_MODE;
-                    }
-                }
-
-                debugWhyTaskbarNotDestroyed("ComponentCallbacks#onConfigurationChanged() "
-                        + "configDiff=" + Configuration.configurationDiffToString(configDiff));
-                if (configDiff != 0 || getCurrentActivityContext() == null) {
-                    recreateTaskbar();
-                } else {
-                    // Config change might be handled without re-creating the taskbar
-                    if (dp != null && !isTaskbarEnabled(dp)) {
-                        destroyDefaultTaskbar();
-                    } else {
-                        if (dp != null && isTaskbarEnabled(dp)) {
-                            if (ENABLE_TASKBAR_NAVBAR_UNIFICATION) {
-                                // Re-initialize for screen size change? Should this be done
-                                // by looking at screen-size change flag in configDiff in the
-                                // block above?
-                                recreateTaskbar();
-                            } else {
-                                getCurrentActivityContext().updateDeviceProfile(dp);
-                            }
-                        }
-                        getCurrentActivityContext().onConfigurationChanged(configDiff);
-                    }
-                }
-                mOldConfig = new Configuration(newConfig);
-                // reset taskbar was pinned value, so we don't automatically unstash taskbar upon
-                // user unfolding the device.
-                mSharedState.setTaskbarWasPinned(false);
-            }
-
-            @Override
-            public void onLowMemory() { }
-        };
-    }
-
     private void destroyAllTaskbars() {
         for (int i = 0; i < mTaskbars.size(); i++) {
             int displayId = mTaskbars.keyAt(i);
@@ -365,23 +308,18 @@
         }
     }
 
-    private void destroyDefaultTaskbar() {
-        destroyTaskbarForDisplay(getDefaultDisplayId());
-    }
-
     private void destroyTaskbarForDisplay(int displayId) {
         Log.d(ILLEGAL_ARGUMENT_WM_ADD_VIEW, "destroyTaskbarForDisplay: " + displayId);
         TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId);
-        debugWhyTaskbarNotDestroyed("destroyTaskbarForDisplay: " + taskbar, displayId);
+        debugTaskbarManager("destroyTaskbarForDisplay: " + taskbar, displayId);
         if (taskbar != null) {
             taskbar.onDestroy();
             // remove all defaults that we store
             removeTaskbarFromMap(displayId);
         }
-        // make this display-specific
-        DeviceProfile dp = mUserUnlocked ?
-                LauncherAppState.getIDP(getWindowContext(displayId)).getDeviceProfile(
-                        getWindowContext(displayId)) : null;
+        // TODO (b/381113004): make this display-specific via getWindowContext()
+        DeviceProfile dp = mUserUnlocked ? LauncherAppState.getIDP(
+                mPrimaryWindowContext).getDeviceProfile(mPrimaryWindowContext) : null;
         if (dp == null || !isTaskbarEnabled(dp)) {
             removeTaskbarRootViewFromWindow(displayId);
         }
@@ -431,7 +369,10 @@
         DisplayController.INSTANCE.get(mPrimaryWindowContext).addChangeListener(
                 mRecreationListener);
         recreateTaskbar();
-        addTaskbarRootViewToWindow(getDefaultDisplayId());
+        for (int i = 0; i < mTaskbars.size(); i++) {
+            int displayId = mTaskbars.keyAt(i);
+            addTaskbarRootViewToWindow(displayId);
+        }
     }
 
     /**
@@ -443,7 +384,7 @@
         }
         removeActivityCallbacksAndListeners();
         mActivity = activity;
-        debugWhyTaskbarNotDestroyed("Set mActivity=" + mActivity);
+        debugTaskbarManager("Set mActivity=" + mActivity);
         mActivity.addOnDeviceProfileChangeListener(mDebugActivityDeviceProfileChanged);
         Log.d(TASKBAR_NOT_DESTROYED_TAG,
                 "registering activity lifecycle callbacks from setActivity().");
@@ -492,7 +433,7 @@
                 return ql.getUnfoldTransitionProgressProvider();
             }
         } else {
-            return SystemUiProxy.INSTANCE.get(mPrimaryWindowContext).getUnfoldTransitionProvider();
+            return SystemUiProxy.INSTANCE.get(mBaseContext).getUnfoldTransitionProvider();
         }
         return null;
     }
@@ -530,13 +471,21 @@
 
     /**
      * This method is called multiple times (ex. initial init, then when user unlocks) in which case
-     * we fully want to destroy the existing default display's taskbar and create a new one.
+     * we fully want to destroy existing taskbars and create all desired new ones.
      * In other case (folding/unfolding) we don't need to remove and add window.
      */
     @VisibleForTesting
     public synchronized void recreateTaskbar() {
-        // TODO: make this recreate all taskbars in map.
-        recreateTaskbarForDisplay(getDefaultDisplayId());
+        // Handles initial creation case.
+        if (mTaskbars.size() == 0) {
+            recreateTaskbarForDisplay(getDefaultDisplayId());
+            return;
+        }
+
+        for (int i = 0; i < mTaskbars.size(); i++) {
+            int displayId = mTaskbars.keyAt(i);
+            recreateTaskbarForDisplay(displayId);
+        }
     }
 
     /**
@@ -545,13 +494,12 @@
      * In other case (folding/unfolding) we don't need to remove and add window.
      */
     private void recreateTaskbarForDisplay(int displayId) {
-        Trace.beginSection("recreateTaskbar");
+        Trace.beginSection("recreateTaskbarForDisplay");
         try {
             Log.d(ILLEGAL_ARGUMENT_WM_ADD_VIEW, "recreateTaskbarForDisplay: " + displayId);
-            // TODO: make this code display specific
-            DeviceProfile dp = mUserUnlocked ?
-                    LauncherAppState.getIDP(getWindowContext(displayId)).getDeviceProfile(
-                            getWindowContext(displayId)) : null;
+            // TODO (b/381113004): make this display-specific via getWindowContext()
+            DeviceProfile dp = mUserUnlocked ? LauncherAppState.getIDP(
+                    mPrimaryWindowContext).getDeviceProfile(mPrimaryWindowContext) : null;
 
             // All Apps action is unrelated to navbar unification, so we only need to check DP.
             final boolean isLargeScreenTaskbar = dp != null && dp.isTaskbarPresent;
@@ -559,15 +507,17 @@
 
             destroyTaskbarForDisplay(displayId);
 
+            boolean displayExists = getDisplay(displayId) != null;
             boolean isTaskbarEnabled = dp != null && isTaskbarEnabled(dp);
-            debugWhyTaskbarNotDestroyed("recreateTaskbar: isTaskbarEnabled=" + isTaskbarEnabled
+            debugTaskbarManager("recreateTaskbarForDisplay: isTaskbarEnabled=" + isTaskbarEnabled
                 + " [dp != null (i.e. mUserUnlocked)]=" + (dp != null)
                 + " FLAG_HIDE_NAVBAR_WINDOW=" + ENABLE_TASKBAR_NAVBAR_UNIFICATION
-                + " dp.isTaskbarPresent=" + (dp == null ? "null" : dp.isTaskbarPresent));
-            if (!isTaskbarEnabled || !isLargeScreenTaskbar) {
-                SystemUiProxy.INSTANCE.get(mPrimaryWindowContext)
+                    + " dp.isTaskbarPresent=" + (dp == null ? "null" : dp.isTaskbarPresent)
+                    + " displayExists=" + displayExists);
+            if (!isTaskbarEnabled || !isLargeScreenTaskbar || !displayExists) {
+                SystemUiProxy.INSTANCE.get(mBaseContext)
                     .notifyTaskbarStatus(/* visible */ false, /* stashed */ false);
-                if (!isTaskbarEnabled) {
+                if (!isTaskbarEnabled || !displayExists) {
                     return;
                 }
             }
@@ -575,6 +525,11 @@
             TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId);
             if (enableTaskbarNoRecreate() || taskbar == null) {
                 taskbar = createTaskbarActivityContext(dp, displayId);
+                if (taskbar == null) {
+                    debugTaskbarManager(
+                            "recreateTaskbarForDisplay: new taskbar instance is null!", displayId);
+                    return;
+                }
             } else {
                 taskbar.updateDeviceProfile(dp);
             }
@@ -585,7 +540,8 @@
 
             // Non default displays should not use LauncherTaskbarUIController as they shouldn't
             // have access to the Launcher activity.
-            if (enableTaskbarConnectedDisplays() && !isDefaultDisplay(displayId)) {
+            if (!isDefaultDisplay(displayId)
+                    && DesktopExperienceFlags.ENABLE_TASKBAR_CONNECTED_DISPLAYS.isTrue()) {
                 taskbar.setUIController(createTaskbarUIControllerForNonDefaultDisplay(displayId));
             } else if (mRecentsViewContainer != null) {
                 taskbar.setUIController(
@@ -622,8 +578,8 @@
     }
 
     public void onLongPressHomeEnabled(boolean assistantLongPressEnabled) {
-        if (mDefaultNavButtonController != null) {
-            mDefaultNavButtonController.setAssistantLongPressEnabled(assistantLongPressEnabled);
+        if (mPrimaryNavButtonController != null) {
+            mPrimaryNavButtonController.setAssistantLongPressEnabled(assistantLongPressEnabled);
         }
     }
 
@@ -746,13 +702,26 @@
      * primary device or a previously mirroring display is switched to extended mode.
      */
     public void onDisplayAddSystemDecorations(int displayId) {
-        if (isDefaultDisplay(displayId) || !enableTaskbarConnectedDisplays()) {
+        Display display = getDisplay(displayId);
+        if (!DesktopExperienceFlags.ENABLE_TASKBAR_CONNECTED_DISPLAYS.isTrue() || isDefaultDisplay(
+                displayId) || display == null) {
+            debugTaskbarManager("onDisplayAddSystemDecorations: not adding display");
+            return;
+        }
+
+        // TODO (b/391965805): remove once onDisplayAddSystemDecorations is working.
+        WindowManager wm = getWindowManager(displayId);
+        if (wm == null || !wm.shouldShowSystemDecors(displayId)) {
             return;
         }
 
         Context newWindowContext = createWindowContext(displayId);
         if (newWindowContext != null) {
             addWindowContextToMap(displayId, newWindowContext);
+            createTaskbarRootLayout(displayId);
+            createNavButtonController(displayId);
+            createAndRegisterComponentCallbacks(displayId);
+            recreateTaskbarForDisplay(displayId);
         }
     }
 
@@ -761,12 +730,16 @@
      * removed from the primary device.
      */
     public void onDisplayRemoved(int displayId) {
-        if (isDefaultDisplay(displayId) || !enableTaskbarConnectedDisplays()) {
+        if (!DesktopExperienceFlags.ENABLE_TASKBAR_CONNECTED_DISPLAYS.isTrue() || isDefaultDisplay(
+                displayId)) {
             return;
         }
 
         Context windowContext = getWindowContext(displayId);
         if (windowContext != null) {
+            removeNavButtonController(displayId);
+            removeAndUnregisterComponentCallbacks(displayId);
+            destroyTaskbarForDisplay(displayId);
             removeWindowContextFromMap(displayId);
         }
     }
@@ -796,7 +769,7 @@
      */
     public void destroy() {
         mRecentsViewContainer = null;
-        debugWhyTaskbarNotDestroyed("TaskbarManager#destroy()");
+        debugTaskbarManager("TaskbarManager#destroy()");
         removeActivityCallbacksAndListeners();
         mTaskbarBroadcastReceiver.unregisterReceiverSafely();
 
@@ -809,7 +782,7 @@
         SettingsCache.INSTANCE.get(mPrimaryWindowContext)
                 .unregister(NAV_BAR_KIDS_MODE, mOnSettingsChangeListener);
         Log.d(TASKBAR_NOT_DESTROYED_TAG, "unregistering component callbacks from destroy().");
-        mPrimaryWindowContext.unregisterComponentCallbacks(mDefaultComponentCallbacks);
+        removeAndUnregisterComponentCallbacks(getDefaultDisplayId());
         mShutdownReceiver.unregisterReceiverSafely();
         destroyAllTaskbars();
     }
@@ -836,15 +809,20 @@
     private void addTaskbarRootViewToWindow(int displayId) {
         TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId);
         if (!enableTaskbarNoRecreate() || taskbar == null) {
-            Log.d(NULL_TASKBAR_ROOT_LAYOUT_TAG,
-                    "addTaskbarRootViewToWindow - taskbar null | displayId=" + displayId);
+            debugTaskbarManager("addTaskbarRootViewToWindow - taskbar null", displayId);
+            return;
+        }
+
+        if (getDisplay(displayId) == null) {
+            debugTaskbarManager("addTaskbarRootViewToWindow - display null", displayId);
             return;
         }
 
         if (!isTaskbarRootLayoutAddedForDisplay(displayId)) {
             FrameLayout rootLayout = getTaskbarRootLayoutForDisplay(displayId);
-            if (rootLayout != null) {
-                getWindowManager(displayId).addView(rootLayout, taskbar.getWindowLayoutParams());
+            WindowManager windowManager = getWindowManager(displayId);
+            if (rootLayout != null && windowManager != null) {
+                windowManager.addView(rootLayout, taskbar.getWindowLayoutParams());
                 mAddedRootLayouts.put(displayId, true);
             } else {
                 Log.d(ILLEGAL_ARGUMENT_WM_ADD_VIEW,
@@ -864,10 +842,14 @@
             return;
         }
 
-        if (isTaskbarRootLayoutAddedForDisplay(displayId)) {
-            getWindowManager(displayId).removeViewImmediate(rootLayout);
+        WindowManager windowManager = getWindowManager(displayId);
+        if (isTaskbarRootLayoutAddedForDisplay(displayId) && windowManager != null) {
+            windowManager.removeViewImmediate(rootLayout);
             mAddedRootLayouts.put(displayId, false);
             removeTaskbarRootLayoutFromMap(displayId);
+        } else {
+            debugTaskbarManager("removeTaskbarRootViewFromWindow - WindowManager is null",
+                    displayId);
         }
     }
 
@@ -913,24 +895,164 @@
 
     /**
      * Creates a {@link TaskbarActivityContext} for the given display and adds it to the map.
+     * @param dp The {@link DeviceProfile} for the display.
+     * @param displayId The ID of the display.
      */
-    private TaskbarActivityContext createTaskbarActivityContext(DeviceProfile dp, int displayId) {
-        Display display = mBaseContext.getSystemService(DisplayManager.class).getDisplay(
-                displayId);
-        Context navigationBarPanelContext = ENABLE_TASKBAR_NAVBAR_UNIFICATION
-                ? mBaseContext.createWindowContext(display, TYPE_NAVIGATION_BAR_PANEL, null)
-                : null;
+    private @Nullable TaskbarActivityContext createTaskbarActivityContext(DeviceProfile dp,
+            int displayId) {
+        Display display = getDisplay(displayId);
+        if (display == null) {
+            debugTaskbarManager("createTaskbarActivityContext: display null", displayId);
+            return null;
+        }
+
+        Context navigationBarPanelContext = null;
+        if (ENABLE_TASKBAR_NAVBAR_UNIFICATION) {
+            navigationBarPanelContext = mBaseContext.createWindowContext(display,
+                    TYPE_NAVIGATION_BAR_PANEL, null);
+        }
+
+        boolean isPrimaryDisplay = isDefaultDisplay(displayId)
+                || !DesktopExperienceFlags.ENABLE_TASKBAR_CONNECTED_DISPLAYS.isTrue();
 
         TaskbarActivityContext newTaskbar = new TaskbarActivityContext(getWindowContext(displayId),
-                navigationBarPanelContext, dp, mDefaultNavButtonController,
-                mUnfoldProgressProvider, isDefaultDisplay(displayId),
-                SystemUiProxy.INSTANCE.get(mPrimaryWindowContext));
+                navigationBarPanelContext, dp, getNavButtonController(displayId),
+                mUnfoldProgressProvider, isPrimaryDisplay,
+                SystemUiProxy.INSTANCE.get(mBaseContext));
 
         addTaskbarToMap(displayId, newTaskbar);
         return newTaskbar;
     }
 
     /**
+     * Create {@link ComponentCallbacks} for the given display and register it to the relevant
+     * WindowContext. For external displays, populate maps.
+     * @param displayId The ID of the display.
+     */
+    private void createAndRegisterComponentCallbacks(int displayId) {
+        ComponentCallbacks callbacks = new ComponentCallbacks() {
+            private Configuration mOldConfig =
+                    getWindowContext(displayId).getResources().getConfiguration();
+
+            @Override
+            public void onConfigurationChanged(Configuration newConfig) {
+                Trace.instantForTrack(Trace.TRACE_TAG_APP, "TaskbarManager",
+                        "onConfigurationChanged: " + newConfig);
+                debugTaskbarManager(
+                        "TaskbarManager#mComponentCallbacks.onConfigurationChanged: " + newConfig);
+                // TODO (b/381113004): make this display-specific via getWindowContext()
+                DeviceProfile dp = mUserUnlocked ? LauncherAppState.getIDP(
+                        mPrimaryWindowContext).getDeviceProfile(mPrimaryWindowContext) : null;
+                int configDiff = mOldConfig.diff(newConfig) & ~SKIP_RECREATE_CONFIG_CHANGES;
+
+                if ((configDiff & ActivityInfo.CONFIG_UI_MODE) != 0) {
+                    Log.d(ILLEGAL_ARGUMENT_WM_ADD_VIEW, "onConfigurationChanged: theme changed");
+                    // Only recreate for theme changes, not other UI mode changes such as docking.
+                    int oldUiNightMode = (mOldConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK);
+                    int newUiNightMode = (newConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK);
+                    if (oldUiNightMode == newUiNightMode) {
+                        configDiff &= ~ActivityInfo.CONFIG_UI_MODE;
+                    }
+                }
+
+                debugTaskbarManager("ComponentCallbacks#onConfigurationChanged() "
+                        + "configDiff=" + Configuration.configurationDiffToString(configDiff));
+                if (configDiff != 0 || getCurrentActivityContext() == null) {
+                    recreateTaskbar();
+                } else {
+                    // Config change might be handled without re-creating the taskbar
+                    if (dp != null && !isTaskbarEnabled(dp)) {
+                        destroyTaskbarForDisplay(getDefaultDisplayId());
+                    } else {
+                        if (dp != null && isTaskbarEnabled(dp)) {
+                            if (ENABLE_TASKBAR_NAVBAR_UNIFICATION) {
+                                // Re-initialize for screen size change? Should this be done
+                                // by looking at screen-size change flag in configDiff in the
+                                // block above?
+                                recreateTaskbar();
+                            } else {
+                                getCurrentActivityContext().updateDeviceProfile(dp);
+                            }
+                        }
+                        getCurrentActivityContext().onConfigurationChanged(configDiff);
+                    }
+                }
+                mOldConfig = new Configuration(newConfig);
+                // reset taskbar was pinned value, so we don't automatically unstash taskbar upon
+                // user unfolding the device.
+                mSharedState.setTaskbarWasPinned(false);
+            }
+
+            @Override
+            public void onLowMemory() { }
+        };
+        if (isDefaultDisplay(displayId)
+                || !DesktopExperienceFlags.ENABLE_TASKBAR_CONNECTED_DISPLAYS.isTrue()) {
+            mPrimaryComponentCallbacks = callbacks;
+            mPrimaryWindowContext.registerComponentCallbacks(callbacks);
+        } else {
+            mComponentCallbacks.put(displayId, callbacks);
+            getWindowContext(displayId).registerComponentCallbacks(callbacks);
+        }
+    }
+
+    /**
+     * Unregister {@link ComponentCallbacks} for the given display from its WindowContext. For
+     * external displays, remove from the map.
+     * @param displayId The ID of the display.
+     */
+    private void removeAndUnregisterComponentCallbacks(int displayId) {
+        if (isDefaultDisplay(displayId)
+                || !DesktopExperienceFlags.ENABLE_TASKBAR_CONNECTED_DISPLAYS.isTrue()) {
+            mPrimaryWindowContext.unregisterComponentCallbacks(mPrimaryComponentCallbacks);
+        } else {
+            ComponentCallbacks callbacks = mComponentCallbacks.get(displayId);
+            getWindowContext(displayId).unregisterComponentCallbacks(callbacks);
+            mComponentCallbacks.delete(displayId);
+        }
+    }
+
+    /**
+     * Creates a {@link TaskbarNavButtonController} for the given display and adds it to the map
+     * if it doesn't already exist.
+     * @param displayId The ID of the display
+     */
+    private void createNavButtonController(int displayId) {
+        if (isDefaultDisplay(displayId)
+                || !DesktopExperienceFlags.ENABLE_TASKBAR_CONNECTED_DISPLAYS.isTrue()) {
+            mPrimaryNavButtonController = new TaskbarNavButtonController(
+                    mPrimaryWindowContext,
+                    mNavCallbacks,
+                    SystemUiProxy.INSTANCE.get(mBaseContext),
+                    new Handler(),
+                    new ContextualSearchInvoker(mBaseContext));
+        } else {
+            TaskbarNavButtonController navButtonController = new TaskbarNavButtonController(
+                    getWindowContext(displayId),
+                    mNavCallbacks,
+                    SystemUiProxy.INSTANCE.get(mBaseContext),
+                    new Handler(),
+                    new ContextualSearchInvoker(mBaseContext));
+            mNavButtonControllers.put(displayId, navButtonController);
+        }
+    }
+
+    private TaskbarNavButtonController getNavButtonController(int displayId) {
+        return (isDefaultDisplay(displayId)
+                || !DesktopExperienceFlags.ENABLE_TASKBAR_CONNECTED_DISPLAYS.isTrue())
+                ? mPrimaryNavButtonController : mNavButtonControllers.get(displayId);
+    }
+
+    private void removeNavButtonController(int displayId) {
+        if (isDefaultDisplay(displayId)
+                || !DesktopExperienceFlags.ENABLE_TASKBAR_CONNECTED_DISPLAYS.isTrue()) {
+            mPrimaryNavButtonController = null;
+        } else {
+            mNavButtonControllers.delete(displayId);
+        }
+    }
+
+    /**
      * Adds the {@link TaskbarActivityContext} associated with the given display ID to taskbar
      * map if there is not already a taskbar mapped to that displayId.
      *
@@ -958,12 +1080,16 @@
      */
     private void createTaskbarRootLayout(int displayId) {
         Log.d(ILLEGAL_ARGUMENT_WM_ADD_VIEW, "createTaskbarRootLayout: " + displayId);
+        if (!enableTaskbarNoRecreate()) {
+            return;
+        }
+
         FrameLayout newTaskbarRootLayout = new FrameLayout(getWindowContext(displayId)) {
             @Override
             public boolean dispatchTouchEvent(MotionEvent ev) {
                 // The motion events can be outside the view bounds of task bar, and hence
                 // manually dispatching them to the drag layer here.
-                TaskbarActivityContext taskbar = getTaskbarForDisplay(getDefaultDisplayId());
+                TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId);
                 if (taskbar != null && taskbar.getDragLayer().isAttachedToWindow()) {
                     return taskbar.getDragLayer().dispatchTouchEvent(ev);
                 }
@@ -1029,13 +1155,10 @@
      * @param displayId The ID of the display for which to create the window context.
      */
     private @Nullable Context createWindowContext(int displayId) {
-        DisplayManager displayManager = mBaseContext.getSystemService(DisplayManager.class);
-        if (displayManager == null) {
-            return null;
-        }
-
-        Display display = displayManager.getDisplay(displayId);
+        debugTaskbarManager("createWindowContext: " + displayId);
+        Display display = getDisplay(displayId);
         if (display == null) {
+            debugTaskbarManager("createWindowContext: display null", displayId);
             return null;
         }
 
@@ -1043,10 +1166,29 @@
         if (ENABLE_TASKBAR_NAVBAR_UNIFICATION && isDefaultDisplay(displayId)) {
             windowType = TYPE_NAVIGATION_BAR;
         }
+        debugTaskbarManager(
+                "createWindowContext: windowType=" + ((windowType == TYPE_NAVIGATION_BAR)
+                        ? "TYPE_NAVIGATION_BAR" : "TYPE_NAVIGATION_BAR_PANEL"), displayId);
 
         return mBaseContext.createWindowContext(display, windowType, null);
     }
 
+    private @Nullable Display getDisplay(int displayId) {
+        DisplayManager displayManager = mBaseContext.getSystemService(DisplayManager.class);
+        if (displayManager == null) {
+            debugTaskbarManager("cannot get DisplayManager", displayId);
+            return null;
+        }
+
+        Display display = displayManager.getDisplay(displayId);
+        if (display == null) {
+            debugTaskbarManager("Cannot get display!", displayId);
+            return null;
+        }
+
+        return displayManager.getDisplay(displayId);
+    }
+
     /**
      * Retrieves the window context of the taskbar for the specified display.
      *
@@ -1054,7 +1196,8 @@
      * @return The Window Context {@link Context} for a given display or {@code null}.
      */
     private Context getWindowContext(int displayId) {
-        return (isDefaultDisplay(displayId) || !enableTaskbarConnectedDisplays())
+        return (isDefaultDisplay(displayId)
+                || !DesktopExperienceFlags.ENABLE_TASKBAR_CONNECTED_DISPLAYS.isTrue())
                 ? mPrimaryWindowContext : mWindowContexts.get(displayId);
     }
 
@@ -1070,12 +1213,15 @@
      * @return The window manager {@link WindowManager} for a given display or {@code null}.
      */
     private @Nullable WindowManager getWindowManager(int displayId) {
-        if (isDefaultDisplay(displayId) || !enableTaskbarConnectedDisplays()) {
+        if (isDefaultDisplay(displayId)
+                || !DesktopExperienceFlags.ENABLE_TASKBAR_CONNECTED_DISPLAYS.isTrue()) {
+            debugTaskbarManager("cannot get mPrimaryWindowManager", displayId);
             return mPrimaryWindowManager;
         }
 
         Context externalDisplayContext = getWindowContext(displayId);
         if (externalDisplayContext == null) {
+            debugTaskbarManager("cannot get externalDisplayContext", displayId);
             return null;
         }
 
@@ -1110,18 +1256,20 @@
     }
 
     /** Temp logs for b/254119092. */
-    public void debugWhyTaskbarNotDestroyed(String debugReason) {
-        debugWhyTaskbarNotDestroyed(debugReason, getDefaultDisplayId());
+    public void debugTaskbarManager(String debugReason) {
+        debugTaskbarManager(debugReason, getDefaultDisplayId());
     }
 
     /** Temp logs for b/254119092. */
-    public void debugWhyTaskbarNotDestroyed(String debugReason, int displayId) {
+    public void debugTaskbarManager(String debugReason, int displayId) {
         StringJoiner log = new StringJoiner("\n");
-        log.add(debugReason  + " displayId=" + displayId);
+        log.add(debugReason + " displayId=" + displayId + " isDefaultDisplay=" + isDefaultDisplay(
+                displayId));
 
         boolean activityTaskbarPresent = mActivity != null
                 && mActivity.getDeviceProfile().isTaskbarPresent;
-        Context windowContext = getWindowContext(displayId);
+        // TODO (b/381113004): make this display-specific via getWindowContext()
+        Context windowContext = mPrimaryWindowContext;
         if (windowContext == null) {
             log.add("window context for displayId" + displayId);
             return;
@@ -1160,6 +1308,6 @@
     }
 
     private final DeviceProfile.OnDeviceProfileChangeListener mDebugActivityDeviceProfileChanged =
-            dp -> debugWhyTaskbarNotDestroyed("mActivity onDeviceProfileChanged");
+            dp -> debugTaskbarManager("mActivity onDeviceProfileChanged");
 
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
index 6016394..95724ad 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
@@ -104,6 +104,8 @@
     private static final int FLAG_DELAY_TASKBAR_BG_TAG = 1 << 12;
     public static final int FLAG_STASHED_FOR_BUBBLES = 1 << 13; // show handle for stashed hotseat
     public static final int FLAG_TASKBAR_HIDDEN = 1 << 14; // taskbar hidden during dream, etc...
+    // taskbar should always be stashed for bubble bar on phone
+    public static final int FLAG_STASHED_BUBBLE_BAR_ON_PHONE = 1 << 15;
 
     // If any of these flags are enabled, isInApp should return true.
     private static final int FLAGS_IN_APP = FLAG_IN_APP | FLAG_IN_SETUP;
@@ -126,7 +128,7 @@
     // If any of these flags are enabled, the taskbar must be stashed.
     private static final int FLAGS_FORCE_STASHED = FLAG_STASHED_SYSUI | FLAG_STASHED_DEVICE_LOCKED
             | FLAG_STASHED_IN_TASKBAR_ALL_APPS | FLAG_STASHED_SMALL_SCREEN
-            | FLAG_STASHED_FOR_BUBBLES;
+            | FLAG_STASHED_FOR_BUBBLES | FLAG_STASHED_BUBBLE_BAR_ON_PHONE;
 
     /**
      * How long to stash/unstash when manually invoked via long press.
@@ -359,6 +361,7 @@
         // For now, assume we're in an app, since LauncherTaskbarUIController won't be able to tell
         // us that we're paused until a bit later. This avoids flickering upon recreating taskbar.
         updateStateForFlag(FLAG_IN_APP, true);
+        updateStateForFlag(FLAG_STASHED_BUBBLE_BAR_ON_PHONE, mActivity.isBubbleBarOnPhone());
 
         applyState(/* duration = */ 0);
 
@@ -574,7 +577,8 @@
      */
     public void updateAndAnimateTransientTaskbar(boolean stash, boolean shouldBubblesFollow,
             boolean delayTaskbarBackground) {
-        if (!DisplayController.isTransientTaskbar(mActivity)) {
+        if (!DisplayController.isTransientTaskbar(mActivity)
+                || mActivity.isBubbleBarOnPhone()) {
             return;
         }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/TypefaceUtils.kt b/quickstep/src/com/android/launcher3/taskbar/TypefaceUtils.kt
new file mode 100644
index 0000000..8b53ff1
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/TypefaceUtils.kt
@@ -0,0 +1,50 @@
+/*
+ * 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.taskbar
+
+import android.graphics.Typeface
+import android.widget.TextView
+import com.android.launcher3.Flags
+
+/**
+ * Helper util class to set pre-defined typefaces to textviews
+ *
+ * If the typeface font family is already defined here, you can just reuse it directly. Otherwise,
+ * please define it here for future use. You do not need to define the font style. If you need
+ * anything other than [Typeface.NORMAL], pass it inline when calling [setTypeface]
+ */
+class TypefaceUtils {
+
+    companion object {
+        const val FONT_FAMILY_HEADLINE_SMALL_EMPHASIZED = "variable-headline-small-emphasized"
+        const val FONT_FAMILY_HEADLINE_LARGE_EMPHASIZED = "variable-headline-large-emphasized"
+        const val FONT_FAMILY_BODY_SMALL_BASELINE = "variable-body-small"
+        const val FONT_FAMILY_BODY_MEDIUM_BASELINE = "variable-body-medium"
+        const val FONT_FAMILY_LABEL_LARGE_BASELINE = "variable-label-large"
+
+        @JvmStatic
+        @JvmOverloads
+        fun setTypeface(
+            textView: TextView?,
+            fontFamilyName: String,
+            fontStyle: Int = Typeface.NORMAL,
+        ) {
+            if (!Flags.expressiveThemeInTaskbarAndNavigation()) return
+            textView?.typeface = Typeface.create(fontFamilyName, fontStyle)
+        }
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt
index 249773d..97be2e8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt
@@ -44,10 +44,13 @@
     private val arrowVisibleHeight: Float
 
     private val strokeAlpha: Int
+    private val strokeColor: Int
+    private val strokeColorDropTarget: Int
     private val shadowAlpha: Int
     private val shadowBlur: Float
     private val keyShadowDistance: Float
     private var arrowHeightFraction = 1f
+    private var isShowingDropTarget: Boolean = false
 
     var arrowPositionX: Float = 0f
         private set
@@ -100,7 +103,9 @@
         fillPaint.flags = Paint.ANTI_ALIAS_FLAG
         fillPaint.style = Paint.Style.FILL
         // configure stroke paint
-        strokePaint.color = context.getColor(R.color.taskbar_stroke)
+        strokeColor = context.getColor(R.color.taskbar_stroke)
+        strokeColorDropTarget = context.getColor(com.android.internal.R.color.system_primary_fixed)
+        strokePaint.color = strokeColor
         strokePaint.flags = Paint.ANTI_ALIAS_FLAG
         strokePaint.style = Paint.Style.STROKE
         strokePaint.strokeWidth = res.getDimension(R.dimen.transient_taskbar_stroke_width)
@@ -235,9 +240,25 @@
         return max(0f, getScaledArrowHeight() - (arrowHeight - arrowVisibleHeight))
     }
 
+    /** Set whether the background should show the drop target */
+    fun showDropTarget(isDropTarget: Boolean) {
+        if (isShowingDropTarget == isDropTarget) {
+            return
+        }
+        isShowingDropTarget = isDropTarget
+        val strokeColor = if (isDropTarget) strokeColorDropTarget else strokeColor
+        val alpha = if (isDropTarget) DRAG_STROKE_ALPHA else strokeAlpha
+        strokePaint.color = strokeColor
+        strokePaint.alpha = alpha
+        invalidateSelf()
+    }
+
+    fun isShowingDropTarget() = isShowingDropTarget
+
     companion object {
         private const val DARK_THEME_STROKE_ALPHA = 51
         private const val LIGHT_THEME_STROKE_ALPHA = 41
+        private const val DRAG_STROKE_ALPHA = 255
         private const val DARK_THEME_SHADOW_ALPHA = 51
         private const val LIGHT_THEME_SHADOW_ALPHA = 25
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
index 4e029e3..5ddbe03 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
@@ -33,7 +33,8 @@
 import android.os.SystemProperties;
 import android.util.ArrayMap;
 import android.util.Log;
-import android.widget.Toast;
+
+import androidx.annotation.NonNull;
 
 import com.android.launcher3.taskbar.TaskbarSharedState;
 import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController;
@@ -589,15 +590,24 @@
     }
 
     @Override
-    public void onDragItemOverBubbleBarDragZone(BubbleBarLocation location) {
-        //TODO(b/388894910): add meaningful implementation
-        MAIN_EXECUTOR.execute(() ->
-                Toast.makeText(mContext, "onDragItemOver " + location, Toast.LENGTH_SHORT).show());
+    public void onDragItemOverBubbleBarDragZone(@NonNull BubbleBarLocation bubbleBarLocation) {
+        MAIN_EXECUTOR.execute(() -> {
+            mBubbleBarViewController.onDragItemOverBubbleBarDragZone(bubbleBarLocation);
+            if (mBubbleBarViewController.isLocationUpdatedForDropTarget()) {
+                mBubbleBarLocationListener.onBubbleBarLocationAnimated(bubbleBarLocation);
+            }
+        });
     }
 
     @Override
     public void onItemDraggedOutsideBubbleBarDropZone() {
-
+        MAIN_EXECUTOR.execute(() -> {
+            if (mBubbleBarViewController.isLocationUpdatedForDropTarget()) {
+                BubbleBarLocation original = mBubbleBarViewController.getBubbleBarLocation();
+                mBubbleBarLocationListener.onBubbleBarLocationAnimated(original);
+            }
+            mBubbleBarViewController.onItemDraggedOutsideBubbleBarDropZone();
+        });
     }
 
     /** Notifies WMShell to show the expanded view. */
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
index c001123..d43ebe2 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -536,6 +536,16 @@
         return (float) (displayWidth - getWidth() - margin);
     }
 
+    /** Set whether the background should show the drop target */
+    public void showDropTarget(boolean isDropTarget) {
+        mBubbleBarBackground.showDropTarget(isDropTarget);
+    }
+
+    /** Returns whether the Bubble Bar is currently displaying a drop target. */
+    public boolean isShowingDropTarget() {
+        return mBubbleBarBackground.isShowingDropTarget();
+    }
+
     /**
      * Animate bubble bar to the given location transiently. Does not modify the layout or the value
      * returned by {@link #getBubbleBarLocation()}.
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index 026f239..b90a5b0 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -131,12 +131,13 @@
     // Whether the bar is hidden when stashed
     private boolean mHiddenForStashed;
     private boolean mShouldShowEducation;
-
     public boolean mOverflowAdded;
+    private boolean mIsLocationUpdatedForDropTarget = false;
 
     private BubbleBarViewAnimator mBubbleBarViewAnimator;
     private final FrameLayout mBubbleBarContainer;
     private BubbleBarFlyoutController mBubbleBarFlyoutController;
+    private BubbleBarPinController mBubbleBarPinController;
     private TaskbarSharedState mTaskbarSharedState;
     private final TimeSource mTimeSource = System::currentTimeMillis;
     private final int mTaskbarTranslationDelta;
@@ -166,6 +167,7 @@
         mBubbleStashController = bubbleControllers.bubbleStashController;
         mBubbleBarController = bubbleControllers.bubbleBarController;
         mBubbleDragController = bubbleControllers.bubbleDragController;
+        mBubbleBarPinController = bubbleControllers.bubbleBarPinController;
         mTaskbarStashController = controllers.taskbarStashController;
         mTaskbarInsetsController = controllers.taskbarInsetsController;
         mBubbleBarFlyoutController = new BubbleBarFlyoutController(
@@ -274,7 +276,10 @@
 
             @Override
             public boolean isOnLeft() {
-                return mBarView.getBubbleBarLocation().isOnLeft(mBarView.isLayoutRtl());
+                boolean shouldRevertLocation =
+                        mBarView.isShowingDropTarget() && mIsLocationUpdatedForDropTarget;
+                boolean isOnLeft = mBarView.getBubbleBarLocation().isOnLeft(mBarView.isLayoutRtl());
+                return shouldRevertLocation != isOnLeft;
             }
 
             @Override
@@ -524,6 +529,61 @@
         mBarView.animateToBubbleBarLocation(bubbleBarLocation);
     }
 
+    /** Returns whether the Bubble Bar is currently displaying a drop target. */
+    public boolean isShowingDropTarget() {
+        return mBarView.isShowingDropTarget();
+    }
+
+    /**
+     * Notifies the controller that a drag event is over the Bubble Bar drop zone. The controller
+     * will display the appropriate drop target and enter drop target mode. The controller will also
+     * update the return value of {@link #isLocationUpdatedForDropTarget()} to true if location was
+     * updated.
+     */
+    public void onDragItemOverBubbleBarDragZone(@NonNull BubbleBarLocation bubbleBarLocation) {
+        mBarView.showDropTarget(/* isDropTarget = */ true);
+        mIsLocationUpdatedForDropTarget = getBubbleBarLocation() != bubbleBarLocation;
+        if (mIsLocationUpdatedForDropTarget) {
+            animateBubbleBarLocation(bubbleBarLocation);
+        }
+        if (!hasBubbles()) {
+            mBubbleBarPinController.showDropTarget(bubbleBarLocation);
+        }
+    }
+
+    /**
+     * Returns {@code true} if location was updated after most recent
+     * {@link #onDragItemOverBubbleBarDragZone}}.
+     */
+    public boolean isLocationUpdatedForDropTarget() {
+        return mIsLocationUpdatedForDropTarget;
+    }
+
+    /**
+     * Notifies the controller that the drag event is outside the Bubble Bar drop zone.
+     * This will hide the drop target zone if there are no bubbles or return the
+     * Bubble Bar to its original location. The controller will also exit drop target
+     * mode and reset the value returned from {@link #isLocationUpdatedForDropTarget()} to false.
+     */
+    public void onItemDraggedOutsideBubbleBarDropZone() {
+        mBarView.showDropTarget(/* isDropTarget = */ false);
+        if (mIsLocationUpdatedForDropTarget) {
+            animateBubbleBarLocation(getBubbleBarLocation());
+        }
+        mBubbleBarPinController.hideDropTarget();
+        mIsLocationUpdatedForDropTarget = false;
+    }
+
+    /**
+     * Notifies the controller that the drag has completed over the Bubble Bar drop zone.
+     * The controller will hide the drop target if there are no bubbles and exit drop target mode.
+     */
+    public void onItemDroppedInBubbleBarDragZone() {
+        mBarView.showDropTarget(/* isDropTarget = */ false);
+        mBubbleBarPinController.hideDropTarget();
+        mIsLocationUpdatedForDropTarget = false;
+    }
+
     /**
      * The bounds of the bubble bar.
      */
@@ -996,7 +1056,12 @@
         boolean isInApp = mTaskbarStashController.isInApp();
         // if this is the first bubble, animate to the initial state.
         if (mBarView.getBubbleChildCount() == 1 && !isUpdate) {
-            mBubbleBarViewAnimator.animateToInitialState(bubble, isInApp, isExpanding);
+            // If a drop target is visible and the first bubble is added, hide the empty drop target
+            if (mBarView.isShowingDropTarget()) {
+                mBubbleBarPinController.hideDropTarget();
+            }
+            mBubbleBarViewAnimator.animateToInitialState(bubble, isInApp, isExpanding,
+                    mBarView.isShowingDropTarget());
             return;
         }
         // if we're not stashed or we're in persistent taskbar, animate for collapsed state.
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
index 745c689..30cfafe 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
@@ -365,7 +365,12 @@
     }
 
     /** Animates to the initial state of the bubble bar, when there are no previous bubbles. */
-    fun animateToInitialState(b: BubbleBarBubble, isInApp: Boolean, isExpanding: Boolean) {
+    fun animateToInitialState(
+        b: BubbleBarBubble,
+        isInApp: Boolean,
+        isExpanding: Boolean,
+        isDragging: Boolean = false,
+    ) {
         val bubbleView = b.view
         val animator = PhysicsAnimator.getInstance(bubbleView)
         if (animator.isRunning()) animator.cancel()
@@ -374,15 +379,12 @@
         // bubble bar to the handle if we're in an app.
         val showAnimation = buildBubbleBarSpringInAnimation()
         val hideAnimation =
-            if (isInApp && !isExpanding) {
+            if (isInApp && !isExpanding && !isDragging) {
                 buildBubbleBarToHandleAnimation()
             } else {
                 Runnable {
-                    moveToState(AnimatingBubble.State.ANIMATING_OUT)
-                    bubbleBarFlyoutController.collapseFlyout {
-                        onFlyoutRemoved()
-                        clearAnimatingBubble()
-                    }
+                    collapseFlyoutAndUpdateState()
+                    if (isDragging) return@Runnable
                     bubbleStashController.showBubbleBarImmediate()
                     bubbleStashController.updateTaskbarTouchRegion()
                 }
@@ -440,11 +442,7 @@
         // first bounce the bubble bar and show the flyout. Then hide the flyout.
         val showAnimation = buildBubbleBarBounceAnimation()
         val hideAnimation = Runnable {
-            moveToState(AnimatingBubble.State.ANIMATING_OUT)
-            bubbleBarFlyoutController.collapseFlyout {
-                onFlyoutRemoved()
-                clearAnimatingBubble()
-            }
+            collapseFlyoutAndUpdateState()
             bubbleStashController.showBubbleBarImmediate()
             bubbleStashController.updateTaskbarTouchRegion()
         }
@@ -454,6 +452,14 @@
         scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation)
     }
 
+    private fun collapseFlyoutAndUpdateState() {
+        moveToState(AnimatingBubble.State.ANIMATING_OUT)
+        bubbleBarFlyoutController.collapseFlyout {
+            onFlyoutRemoved()
+            clearAnimatingBubble()
+        }
+    }
+
     /**
      * The bubble bar animation when it is collapsed is divided into 2 chained animations. The first
      * animation is a regular accelerate animation that moves the bubble bar upwards. When it ends
diff --git a/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java b/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
index b25f999..37b8dc7 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
@@ -51,7 +51,7 @@
 import com.android.launcher3.anim.AnimatorListeners;
 import com.android.launcher3.celllayout.CellLayoutLayoutParams;
 import com.android.launcher3.celllayout.DelegatedCellDrawing;
-import com.android.launcher3.graphics.IconShape;
+import com.android.launcher3.graphics.ThemeManager;
 import com.android.launcher3.icons.FastBitmapDrawable;
 import com.android.launcher3.icons.IconNormalizer;
 import com.android.launcher3.icons.LauncherIcons;
@@ -142,7 +142,7 @@
         int shadowSize = context.getResources().getDimensionPixelSize(
                 R.dimen.blur_size_thin_outline);
         mShadowFilter = new BlurMaskFilter(shadowSize, BlurMaskFilter.Blur.OUTER);
-        mShapePath = IconShape.INSTANCE.get(context).getShape().getPath(mNormalizedIconSize);
+        mShapePath = ThemeManager.INSTANCE.get(context).getIconShape().getPath(mNormalizedIconSize);
     }
 
     @Override
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index bb57d6e..019e746 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -1380,7 +1380,7 @@
         SystemUiProxy.INSTANCE.get(this).setLauncherAppIconSize(mDeviceProfile.iconSizePx);
         TaskbarManager taskbarManager = mTISBindHelper.getTaskbarManager();
         if (taskbarManager != null) {
-            taskbarManager.debugWhyTaskbarNotDestroyed("QuickstepLauncher#onDeviceProfileChanged");
+            taskbarManager.debugTaskbarManager("QuickstepLauncher#onDeviceProfileChanged");
         }
     }
 
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewDismissTouchController.kt b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewDismissTouchController.kt
index 98737a5..88b7155 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewDismissTouchController.kt
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewDismissTouchController.kt
@@ -16,9 +16,11 @@
 package com.android.launcher3.uioverrides.touchcontrollers
 
 import android.content.Context
+import android.graphics.Rect
 import android.view.MotionEvent
 import androidx.dynamicanimation.animation.SpringAnimation
 import com.android.app.animation.Interpolators.DECELERATE
+import com.android.app.animation.Interpolators.LINEAR
 import com.android.launcher3.AbstractFloatingView
 import com.android.launcher3.R
 import com.android.launcher3.Utilities.EDGE_NAV_BAR
@@ -29,6 +31,7 @@
 import com.android.launcher3.util.MSDLPlayerWrapper
 import com.android.launcher3.util.TouchController
 import com.android.quickstep.views.RecentsView
+import com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY
 import com.android.quickstep.views.RecentsViewContainer
 import com.android.quickstep.views.TaskView
 import com.google.android.msdl.data.model.MSDLToken
@@ -50,6 +53,7 @@
             recentsView.pagedOrientationHandler.upDownSwipeDirection,
         )
     private val isRtl = isRtl(container.resources)
+    private val tempTaskThumbnailBounds = Rect()
 
     private var taskBeingDragged: TaskView? = null
     private var springAnimation: SpringAnimation? = null
@@ -57,6 +61,7 @@
     private var verticalFactor: Int = 0
     private var hasDismissThresholdHapticRun = false
     private var initialDisplacement: Float = 0f
+    private var recentsScaleAnimation: SpringAnimation? = null
 
     private fun canInterceptTouch(ev: MotionEvent): Boolean =
         when {
@@ -98,6 +103,7 @@
 
     private fun onActionDown(ev: MotionEvent): Boolean {
         springAnimation?.cancel()
+        recentsScaleAnimation?.cancel()
         if (!canInterceptTouch(ev)) {
             return false
         }
@@ -108,7 +114,9 @@
                     recentsView.isTaskViewVisible(it) && container.dragLayer.isEventOverView(it, ev)
                 }
                 ?.also {
-                    dismissLength = recentsView.pagedOrientationHandler.getSecondaryDimension(it)
+                    // Dismiss length as bottom of task so it is fully off screen when dismissed.
+                    it.getThumbnailBounds(tempTaskThumbnailBounds, relativeToDragLayer = true)
+                    dismissLength = tempTaskThumbnailBounds.bottom
                     verticalFactor =
                         recentsView.pagedOrientationHandler.secondaryTranslationDirectionFactor
                 }
@@ -162,6 +170,8 @@
             }
             recentsView.redrawLiveTile()
         }
+        val dismissFraction = displacement / (dismissLength * verticalFactor).toFloat()
+        RECENTS_SCALE_PROPERTY.setValue(recentsView, getRecentsScale(dismissFraction))
         playDismissThresholdHaptic(displacement)
         return true
     }
@@ -216,6 +226,10 @@
                         if (isDismissing) (dismissLength * verticalFactor).toFloat() else 0f
                     )
                 }
+        recentsScaleAnimation =
+            recentsView.animateRecentsScale(RECENTS_SCALE_DEFAULT).addEndListener { _, _, _, _ ->
+                recentsScaleAnimation = null
+            }
     }
 
     // Returns if the current task being dragged is towards "positive" (e.g. dismissal).
@@ -230,8 +244,54 @@
         springAnimation = null
     }
 
+    private fun getRecentsScale(dismissFraction: Float): Float {
+        return when {
+            // Do not scale recents when dragging below origin.
+            dismissFraction <= 0 -> {
+                RECENTS_SCALE_DEFAULT
+            }
+            // Initially scale recents as the drag begins, up to the first threshold.
+            dismissFraction < RECENTS_SCALE_FIRST_THRESHOLD_FRACTION -> {
+                mapToRange(
+                    dismissFraction,
+                    0f,
+                    RECENTS_SCALE_FIRST_THRESHOLD_FRACTION,
+                    RECENTS_SCALE_DEFAULT,
+                    RECENTS_SCALE_ON_DISMISS_CANCEL,
+                    LINEAR,
+                )
+            }
+            // Keep scale consistent until dragging to the dismiss threshold.
+            dismissFraction < RECENTS_SCALE_DISMISS_THRESHOLD_FRACTION -> {
+                RECENTS_SCALE_ON_DISMISS_CANCEL
+            }
+            // Scale beyond the dismiss threshold again, to indicate dismiss will occur on release.
+            dismissFraction < RECENTS_SCALE_SECOND_THRESHOLD_FRACTION -> {
+                mapToRange(
+                    dismissFraction,
+                    RECENTS_SCALE_DISMISS_THRESHOLD_FRACTION,
+                    RECENTS_SCALE_SECOND_THRESHOLD_FRACTION,
+                    RECENTS_SCALE_ON_DISMISS_CANCEL,
+                    RECENTS_SCALE_ON_DISMISS_SUCCESS,
+                    LINEAR,
+                )
+            }
+            // Keep scale beyond the dismiss threshold scaling consistent.
+            else -> {
+                RECENTS_SCALE_ON_DISMISS_SUCCESS
+            }
+        }
+    }
+
     companion object {
         private const val DISMISS_THRESHOLD_FRACTION = 0.5f
         private const val DISMISS_THRESHOLD_HAPTIC_RANGE = 10f
+
+        private const val RECENTS_SCALE_ON_DISMISS_CANCEL = 0.9875f
+        private const val RECENTS_SCALE_ON_DISMISS_SUCCESS = 0.975f
+        private const val RECENTS_SCALE_DEFAULT = 1f
+        private const val RECENTS_SCALE_FIRST_THRESHOLD_FRACTION = 0.2f
+        private const val RECENTS_SCALE_DISMISS_THRESHOLD_FRACTION = 0.5f
+        private const val RECENTS_SCALE_SECOND_THRESHOLD_FRACTION = 0.575f
     }
 }
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index b43c3ac..3640e1f 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -1112,6 +1112,9 @@
     public void onGestureEnded(float endVelocityPxPerMs, PointF velocityPxPerMs) {
         float flingThreshold = mContext.getResources()
                 .getDimension(R.dimen.quickstep_fling_threshold_speed);
+        Log.d(TAG, "onGestureEnded: mGestureStarted=" + mGestureStarted
+                + ", mIsMotionPaused=" + mIsMotionPaused
+                + ", flingThresholdPassed=" + (Math.abs(endVelocityPxPerMs) > flingThreshold));
         boolean isFling = mGestureStarted && !mIsMotionPaused
                 && Math.abs(endVelocityPxPerMs) > flingThreshold;
         mStateCallback.setStateOnUiThread(STATE_GESTURE_COMPLETED);
@@ -1260,12 +1263,12 @@
                 dpiFromPx(velocityPxPerMs.x),
                 dpiFromPx(velocityPxPerMs.y),
                 Math.toDegrees(Math.atan2(-velocityPxPerMs.y, velocityPxPerMs.x)));
-
         if (mGestureState.isHandlingAtomicEvent()) {
             // Button mode, this is only used to go to recents.
             return RECENTS;
         }
 
+        Log.d(TAG, "calculateEndTarget: isCancel=" + isCancel + ", isFlingY=" + isFlingY);
         GestureEndTarget endTarget;
         if (isCancel) {
             endTarget = LAST_TASK;
@@ -1275,6 +1278,7 @@
             endTarget = calculateEndTargetForNonFling(velocityPxPerMs);
         }
 
+        Log.d(TAG, "calculateEndTarget: endTarget(1)=" + endTarget);
         if (mDeviceState.isOverviewDisabled() && endTarget == RECENTS) {
             return LAST_TASK;
         }
@@ -1293,6 +1297,7 @@
                 return LAST_TASK;
             }
         }
+        Log.d(TAG, "calculateEndTarget: endTarget(2)=" + endTarget);
         return endTarget;
     }
 
@@ -1301,9 +1306,12 @@
         final boolean willGoToNewTask =
                 isScrollingToNewTask() && Math.abs(velocity.x) > Math.abs(endVelocity);
         final boolean isSwipeUp = endVelocity < 0;
+        Log.d(TAG, "calculateEndTargetForFlingY: willGoToNewTask=" + willGoToNewTask
+                + ", isSwipeUp=" + isSwipeUp);
         if (!isSwipeUp) {
             final boolean isCenteredOnNewTask = mRecentsView != null
                     && mRecentsView.getDestinationPage() != mRecentsView.getRunningTaskIndex();
+            Log.d(TAG, "calculateEndTargetForFlingY: isCenteredOnNewTask=" + isCenteredOnNewTask);
             return willGoToNewTask || isCenteredOnNewTask ? NEW_TASK : LAST_TASK;
         }
 
@@ -1316,6 +1324,9 @@
         // Fully gestural mode.
         final boolean isFlingX = Math.abs(velocity.x) > mContext.getResources()
                 .getDimension(R.dimen.quickstep_fling_threshold_speed);
+        Log.d(TAG, "calculateEndTargetForNonFling: isScrollingToNewTask=" + isScrollingToNewTask
+                + ", isFlingX=" + isFlingX
+                + ", mIsMotionPaused=" + mIsMotionPaused);
         if (isScrollingToNewTask && isFlingX) {
             // Flinging towards new task takes precedence over mIsMotionPaused (which only
             // checks y-velocity).
@@ -1325,6 +1336,7 @@
         } else if (isScrollingToNewTask) {
             return NEW_TASK;
         }
+        Log.d(TAG, "calculateEndTargetForNonFling: mCanSlowSwipeGoHome=" + mCanSlowSwipeGoHome);
         return velocity.y < 0 && mCanSlowSwipeGoHome ? HOME : LAST_TASK;
     }
 
diff --git a/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt b/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt
index 42aa86e..afdb403 100644
--- a/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt
+++ b/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt
@@ -293,10 +293,6 @@
         val recentsView: RecentsView<*, *>? = recentsViewContainer?.getOverviewPanel()
         val deviceProfile = recentsViewContainer?.getDeviceProfile()
         val uiController = containerInterface.getTaskbarController()
-        val allowQuickSwitch =
-            uiController != null &&
-                deviceProfile != null &&
-                (deviceProfile.isTablet || deviceProfile.isTwoPanels)
 
         val focusedDisplayId = focusState.focusedDisplayId
         val focusedDisplayUIController: TaskbarUIController? =
@@ -322,26 +318,26 @@
 
         when (command.type) {
             HIDE -> {
-                if (!allowQuickSwitch) return true
+                if (uiController == null || deviceProfile?.isTablet == false) return true
                 keyboardTaskFocusIndex =
                     if (
                         enableAltTabKqsOnConnectedDisplays() && focusedDisplayUIController != null
                     ) {
                         focusedDisplayUIController.launchFocusedTask()
                     } else {
-                        uiController!!.launchFocusedTask()
+                        uiController.launchFocusedTask()
                     }
 
                 if (keyboardTaskFocusIndex == -1) return true
             }
             KEYBOARD_INPUT ->
-                if (allowQuickSwitch) {
+                if (uiController != null && deviceProfile?.isTablet == true) {
                     if (
                         enableAltTabKqsOnConnectedDisplays() && focusedDisplayUIController != null
                     ) {
                         focusedDisplayUIController.openQuickSwitchView()
                     } else {
-                        uiController!!.openQuickSwitchView()
+                        uiController.openQuickSwitchView()
                     }
                     return true
                 } else {
@@ -365,7 +361,11 @@
             TOGGLE -> {}
         }
 
-        recentsView?.setKeyboardTaskFocusIndex(keyboardTaskFocusIndex)
+        recentsView?.setKeyboardTaskFocusIndex(
+            recentsView.indexOfChild(recentsView.taskViews.elementAtOrNull(keyboardTaskFocusIndex))
+                ?: -1
+        )
+
         // Handle recents view focus when launching from home
         val animatorListener: Animator.AnimatorListener =
             object : AnimatorListenerAdapter() {
@@ -526,7 +526,7 @@
         // Stops requesting focused after first view gets focused.
         recentsView.getTaskViewAt(keyboardTaskFocusIndex).requestFocus() ||
             recentsView.nextTaskView.requestFocus() ||
-            recentsView.getFirstTaskView().requestFocus() ||
+            recentsView.firstTaskView.requestFocus() ||
             recentsView.requestFocus()
     }
 
diff --git a/quickstep/src/com/android/quickstep/TaskViewUtils.java b/quickstep/src/com/android/quickstep/TaskViewUtils.java
index 35a6e72..37c2d1c 100644
--- a/quickstep/src/com/android/quickstep/TaskViewUtils.java
+++ b/quickstep/src/com/android/quickstep/TaskViewUtils.java
@@ -38,6 +38,7 @@
 import static com.android.launcher3.QuickstepTransitionManager.SPLIT_LAUNCH_DURATION;
 import static com.android.launcher3.Utilities.getDescendantCoordRelativeToAncestor;
 import static com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE;
+import static com.android.quickstep.BaseContainerInterface.getTaskDimension;
 import static com.android.quickstep.util.AnimUtils.clampToDuration;
 import static com.android.wm.shell.shared.TransitionUtil.TYPE_SPLIT_SCREEN_DIM_LAYER;
 
@@ -65,6 +66,7 @@
 import com.android.app.animation.Interpolators;
 import com.android.internal.jank.Cuj;
 import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimatedFloat;
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.AnimatorPlaybackController;
@@ -102,6 +104,11 @@
 
     private TaskViewUtils() {}
 
+    private static final Rect TEMP_THUMBNAIL_BOUNDS = new Rect();
+    private static final Rect TEMP_FULLSCREEN_BOUNDS = new Rect();
+    private static final PointF TEMP_TASK_DIMENSION = new PointF();
+    private static final PointF TEMP_PIVOT = new PointF();
+
     /**
      * Try to find a TaskView that corresponds with the component of the launched view.
      *
@@ -164,7 +171,7 @@
     public static <T extends Context & RecentsViewContainer & StatefulContainer<?>>
     void createRecentsWindowAnimator(
             @NonNull RecentsView<T, ?> recentsView,
-            @NonNull TaskView v,
+            @NonNull TaskView taskView,
             boolean skipViewChanges,
             @NonNull RemoteAnimationTarget[] appTargets,
             @NonNull RemoteAnimationTarget[] wallpaperTargets,
@@ -172,31 +179,31 @@
             @Nullable DepthController depthController,
             @Nullable TransitionInfo transitionInfo,
             PendingAnimation out) {
-        boolean isQuickSwitch = v.isEndQuickSwitchCuj();
-        v.setEndQuickSwitchCuj(false);
+        boolean isQuickSwitch = taskView.isEndQuickSwitchCuj();
+        taskView.setEndQuickSwitchCuj(false);
 
         final RemoteAnimationTargets targets =
                 new RemoteAnimationTargets(appTargets, wallpaperTargets, nonAppTargets,
                         MODE_OPENING);
         final RemoteAnimationTarget navBarTarget = targets.getNavBarRemoteAnimationTarget();
 
-        SurfaceTransactionApplier applier = new SurfaceTransactionApplier(v);
+        SurfaceTransactionApplier applier = new SurfaceTransactionApplier(taskView);
         targets.addReleaseCheck(applier);
 
         RemoteTargetHandle[] remoteTargetHandles;
         RemoteTargetHandle[] recentsViewHandles = recentsView.getRemoteTargetHandles();
-        if (v.isRunningTask() && recentsViewHandles != null) {
+        if (taskView.isRunningTask() && recentsViewHandles != null) {
             // Re-use existing handles
             remoteTargetHandles = recentsViewHandles;
         } else {
-            boolean forDesktop = v instanceof DesktopTaskView;
-            RemoteTargetGluer gluer = new RemoteTargetGluer(v.getContext(),
+            boolean forDesktop = taskView instanceof DesktopTaskView;
+            RemoteTargetGluer gluer = new RemoteTargetGluer(taskView.getContext(),
                     recentsView.getSizeStrategy(), targets, forDesktop);
             if (forDesktop) {
                 remoteTargetHandles = gluer.assignTargetsForDesktop(targets, transitionInfo);
-            } else if (v.containsMultipleTasks()) {
+            } else if (taskView.containsMultipleTasks()) {
                 remoteTargetHandles = gluer.assignTargetsForSplitScreen(targets,
-                        ((GroupedTaskView) v).getSplitBoundsConfig());
+                        ((GroupedTaskView) taskView).getSplitBoundsConfig());
             } else {
                 remoteTargetHandles = gluer.assignTargets(targets);
             }
@@ -210,8 +217,8 @@
             remoteTargetHandle.getTransformParams().setSyncTransactionApplier(applier);
         }
 
-        int taskIndex = recentsView.indexOfChild(v);
-        Context context = v.getContext();
+        int taskIndex = recentsView.indexOfChild(taskView);
+        Context context = taskView.getContext();
 
         T container = RecentsViewContainer.containerFromContext(context);
         DeviceProfile dp = container.getDeviceProfile();
@@ -219,11 +226,11 @@
         boolean parallaxCenterAndAdjacentTask =
                 !showAsGrid && taskIndex != recentsView.getCurrentPage();
         int taskRectTranslationPrimary = recentsView.getScrollOffset(taskIndex);
-        int taskRectTranslationSecondary = showAsGrid ? (int) v.getGridTranslationY() : 0;
+        int taskRectTranslationSecondary = showAsGrid ? (int) taskView.getGridTranslationY() : 0;
 
         RemoteTargetHandle[] topMostSimulators = null;
 
-        if (!v.isRunningTask()) {
+        if (!taskView.isRunningTask()) {
             // TVSs already initialized from the running task, no need to re-init
             for (RemoteTargetHandle targetHandle : remoteTargetHandles) {
                 TaskViewSimulator tvsLocal = targetHandle.getTaskViewSimulator();
@@ -237,13 +244,13 @@
                 tvsLocal.fullScreenProgress.value = 0;
                 tvsLocal.recentsViewScale.value = 1;
                 if (!enableGridOnlyOverview()) {
-                    tvsLocal.setIsGridTask(v.isGridTask());
+                    tvsLocal.setIsGridTask(taskView.isGridTask());
                 }
                 tvsLocal.getOrientationState().getOrientationHandler().set(tvsLocal,
                         TaskViewSimulator::setTaskRectTranslation, taskRectTranslationPrimary,
                         taskRectTranslationSecondary);
 
-                if (v instanceof DesktopTaskView) {
+                if (taskView instanceof DesktopTaskView) {
                     targetHandle.getTransformParams().setTargetAlpha(1f);
                 } else {
                     // Fade in the task during the initial 20% of the animation
@@ -260,8 +267,11 @@
             out.setFloat(tvsLocal.recentsViewScale,
                     AnimatedFloat.VALUE, tvsLocal.getFullScreenScale(),
                     TOUCH_RESPONSE);
-            out.setFloat(tvsLocal.recentsViewScroll, AnimatedFloat.VALUE, 0,
-                    TOUCH_RESPONSE);
+            if (!enableGridOnlyOverview()) {
+                out.setFloat(tvsLocal.recentsViewScroll, AnimatedFloat.VALUE, 0,
+                        TOUCH_RESPONSE);
+            }
+
             out.addListener(new AnimatorListenerAdapter() {
                 @Override
                 public void onAnimationStart(Animator animation) {
@@ -271,6 +281,18 @@
                         showTransaction.getTransaction().show(targets.apps[i].leash);
                     }
                     applier.scheduleApply(showTransaction);
+
+                    if (enableGridOnlyOverview()) {
+                        taskView.getThumbnailBounds(TEMP_THUMBNAIL_BOUNDS, /*relativeToDragLayer=*/
+                                true);
+                        getTaskDimension(context, container.getDeviceProfile(),
+                                TEMP_TASK_DIMENSION);
+                        TEMP_FULLSCREEN_BOUNDS.set(0, 0, (int) TEMP_TASK_DIMENSION.x,
+                                (int) TEMP_TASK_DIMENSION.y);
+                        Utilities.getPivotsForScalingRectToRect(TEMP_THUMBNAIL_BOUNDS,
+                                TEMP_FULLSCREEN_BOUNDS, TEMP_PIVOT);
+                        tvsLocal.setPivotOverride(TEMP_PIVOT);
+                    }
                 }
             });
             out.addOnFrameCallback(() -> {
@@ -321,7 +343,7 @@
 
         if (!skipViewChanges && parallaxCenterAndAdjacentTask && topMostSimulators != null
                 && topMostSimulators.length > 0) {
-            out.addFloat(v, VIEW_ALPHA, 1, 0, clampToProgress(LINEAR, 0.2f, 0.4f));
+            out.addFloat(taskView, VIEW_ALPHA, 1, 0, clampToProgress(LINEAR, 0.2f, 0.4f));
 
             RemoteTargetHandle[] simulatorCopies = topMostSimulators;
             for (RemoteTargetHandle handle : simulatorCopies) {
@@ -340,7 +362,7 @@
             // During animation we apply transformation on the thumbnailView (and not the rootView)
             // to follow the TaskViewSimulator. So the final matrix applied on the thumbnailView is:
             //    Mt K(0)` K(t) Mt`
-            View[] thumbnails = v.getSnapshotViews();
+            View[] thumbnails = taskView.getSnapshotViews();
 
             // In case simulator copies and thumbnail size do no match, ensure we get the lesser.
             // This ensures we do not create arrays with empty elements or attempt to references
diff --git a/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
index 4b3bef3..c6b3d6a 100644
--- a/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
+++ b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
@@ -31,7 +31,6 @@
 import com.android.quickstep.recents.domain.usecase.GetThumbnailPositionUseCase
 import com.android.quickstep.recents.domain.usecase.IsThumbnailValidUseCase
 import com.android.quickstep.recents.domain.usecase.OrganizeDesktopTasksUseCase
-import com.android.quickstep.recents.usecase.GetThumbnailUseCase
 import com.android.quickstep.recents.viewmodel.RecentsViewData
 import com.android.quickstep.task.viewmodel.TaskOverlayViewModel
 import com.android.systemui.shared.recents.model.Task
@@ -42,7 +41,7 @@
 
 internal typealias RecentsScopeId = String
 
-class RecentsDependencies private constructor(private val appContext: Context) {
+class RecentsDependencies private constructor(appContext: Context) {
     private val scopes = mutableMapOf<RecentsScopeId, RecentsDependenciesScope>()
 
     init {
@@ -185,7 +184,6 @@
                 IsThumbnailValidUseCase::class.java ->
                     IsThumbnailValidUseCase(rotationStateRepository = inject())
                 GetTaskUseCase::class.java -> GetTaskUseCase(repository = inject())
-                GetThumbnailUseCase::class.java -> GetThumbnailUseCase(taskRepository = inject())
                 GetSysUiStatusNavFlagsUseCase::class.java -> GetSysUiStatusNavFlagsUseCase()
                 GetThumbnailPositionUseCase::class.java ->
                     GetThumbnailPositionUseCase(
diff --git a/quickstep/src/com/android/quickstep/recents/usecase/GetThumbnailUseCase.kt b/quickstep/src/com/android/quickstep/recents/usecase/GetThumbnailUseCase.kt
deleted file mode 100644
index b9e9e02..0000000
--- a/quickstep/src/com/android/quickstep/recents/usecase/GetThumbnailUseCase.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * 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.recents.usecase
-
-import android.graphics.Bitmap
-import com.android.quickstep.recents.data.RecentTasksRepository
-
-/** Use case for retrieving thumbnail. */
-class GetThumbnailUseCase(private val taskRepository: RecentTasksRepository) {
-    /** Returns the latest thumbnail associated with [taskId] if loaded, or null otherwise */
-    fun run(taskId: Int): Bitmap? = taskRepository.getCurrentThumbnailById(taskId)?.thumbnail
-}
diff --git a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
index a1963c9..a9dbbf2 100644
--- a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
+++ b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
@@ -215,13 +215,13 @@
         if (enableOverviewIconMenu()) {
             builder.add(
                 ObjectAnimator.ofFloat(
-                    (iconView as IconAppChipView).splitTranslationX,
+                    (iconView as IconAppChipView).getSplitTranslationX(),
                     MULTI_PROPERTY_VALUE,
                     0f,
                 )
             )
             builder.add(
-                ObjectAnimator.ofFloat(iconView.splitTranslationY, MULTI_PROPERTY_VALUE, 0f)
+                ObjectAnimator.ofFloat(iconView.getSplitTranslationY(), MULTI_PROPERTY_VALUE, 0f)
             )
         }
 
@@ -985,12 +985,11 @@
         val splitTree: Pair<Change, List<Change>>? = extractTopParentAndChildren(transitionInfo)
         check(splitTree != null) { "Could not find a split root candidate" }
         val rootCandidate = splitTree.first
-        val stageRootTaskIds: Set<Int> = splitTree.second
-            .map { it.taskInfo!!.taskId }
-            .toSet()
-        val leafTasks: List<Change> = transitionInfo.changes
-            .filter { it.taskInfo != null && it.taskInfo!!.parentTaskId in stageRootTaskIds}
-            .toList()
+        val stageRootTaskIds: Set<Int> = splitTree.second.map { it.taskInfo!!.taskId }.toSet()
+        val leafTasks: List<Change> =
+            transitionInfo.changes
+                .filter { it.taskInfo != null && it.taskInfo!!.parentTaskId in stageRootTaskIds }
+                .toList()
 
         // Starting position is a 34% size tile centered in the middle of the screen.
         // Ending position is the full device screen.
@@ -1031,8 +1030,13 @@
                         val endAbsBounds = leaf.endAbsBounds
 
                         t.setAlpha(leaf.leash, 1f)
-                        t.setCrop(leaf.leash, 0f, 0f,
-                            endAbsBounds.width().toFloat(), endAbsBounds.height().toFloat())
+                        t.setCrop(
+                            leaf.leash,
+                            0f,
+                            0f,
+                            endAbsBounds.width().toFloat(),
+                            endAbsBounds.height().toFloat(),
+                        )
                         t.setPosition(leaf.leash, 0f, 0f)
                     }
 
@@ -1040,10 +1044,18 @@
                         val endAbsBounds = stageRoot.endAbsBounds
 
                         t.setAlpha(stageRoot.leash, 1f)
-                        t.setCrop(stageRoot.leash, 0f, 0f,
-                            endAbsBounds.width().toFloat(), endAbsBounds.height().toFloat())
-                        t.setPosition(stageRoot.leash, endAbsBounds.left.toFloat(),
-                            endAbsBounds.top.toFloat())
+                        t.setCrop(
+                            stageRoot.leash,
+                            0f,
+                            0f,
+                            endAbsBounds.width().toFloat(),
+                            endAbsBounds.height().toFloat(),
+                        )
+                        t.setPosition(
+                            stageRoot.leash,
+                            endAbsBounds.left.toFloat(),
+                            endAbsBounds.top.toFloat(),
+                        )
                     }
                     t.apply()
                 }
diff --git a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
index 09e9c8b..b844079 100644
--- a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
+++ b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
@@ -97,6 +97,8 @@
     private final FullscreenDrawParams mCurrentFullscreenParams;
     public final AnimatedFloat taskPrimaryTranslation = new AnimatedFloat();
     public final AnimatedFloat taskSecondaryTranslation = new AnimatedFloat();
+    public final AnimatedFloat taskGridTranslationX = new AnimatedFloat();
+    public final AnimatedFloat taskGridTranslationY = new AnimatedFloat();
 
     // Carousel properties
     public final AnimatedFloat carouselScale = new AnimatedFloat();
@@ -445,6 +447,7 @@
                 taskPrimaryTranslation.value);
         mOrientationState.getOrientationHandler().setSecondary(mMatrix, MATRIX_POST_TRANSLATE,
                 taskSecondaryTranslation.value);
+        mMatrix.postTranslate(taskGridTranslationX.value, taskGridTranslationY.value);
 
         mMatrix.postScale(carouselScale.value, carouselScale.value,
                 mIsRecentsRtl ? mCarouselTaskSize.right : mCarouselTaskSize.left,
@@ -484,6 +487,8 @@
                 + " taskRect: " + mTaskRect
                 + " taskPrimaryT: " + taskPrimaryTranslation.value
                 + " taskSecondaryT: " + taskSecondaryTranslation.value
+                + " taskGridTranslationX: " + taskGridTranslationX.value
+                + " taskGridTranslationY: " + taskGridTranslationY.value
                 + " recentsPrimaryT: " + recentsViewPrimaryTranslation.value
                 + " recentsSecondaryT: " + recentsViewSecondaryTranslation.value
                 + " recentsScroll: " + recentsViewScroll.value
diff --git a/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt b/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
index 229c8f5..2abfb13 100644
--- a/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
@@ -171,12 +171,10 @@
                 val iconMargins = (iconViewMarginStart + iconViewBackgroundMarginStart) * 2
                 // setMaxWidth() needs to be called before mIconView.setIconOrientation which is
                 // called in the super below.
-                (leftTopTaskContainer.iconView as IconAppChipView).setMaxWidth(
+                (leftTopTaskContainer.iconView as IconAppChipView).maxWidth =
                     groupedTaskViewSizes.first.x - iconMargins
-                )
-                (rightBottomTaskContainer.iconView as IconAppChipView).setMaxWidth(
+                (rightBottomTaskContainer.iconView as IconAppChipView).maxWidth =
                     groupedTaskViewSizes.second.x - iconMargins
-                )
             }
         }
         super.setOrientationState(orientationState)
diff --git a/quickstep/src/com/android/quickstep/views/IconAppChipView.java b/quickstep/src/com/android/quickstep/views/IconAppChipView.java
deleted file mode 100644
index 5270477..0000000
--- a/quickstep/src/com/android/quickstep/views/IconAppChipView.java
+++ /dev/null
@@ -1,471 +0,0 @@
-/*
- * Copyright (C) 2023 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 static com.android.app.animation.Interpolators.EMPHASIZED;
-import static com.android.app.animation.Interpolators.LINEAR;
-import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
-import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X;
-import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
-
-import android.animation.Animator;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.animation.RectEvaluator;
-import android.animation.ValueAnimator;
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Outline;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.util.AttributeSet;
-import android.view.View;
-import android.view.ViewAnimationUtils;
-import android.view.ViewOutlineProvider;
-import android.widget.FrameLayout;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.util.MultiPropertyFactory;
-import com.android.launcher3.util.MultiValueAlpha;
-import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
-import com.android.quickstep.util.RecentsOrientedState;
-
-/**
- * An icon app menu view which can be used in place of an IconView in overview TaskViews.
- */
-public class IconAppChipView extends FrameLayout implements TaskViewIcon {
-
-    private static final int MENU_BACKGROUND_REVEAL_DURATION = 417;
-    private static final int MENU_BACKGROUND_HIDE_DURATION = 333;
-
-    private static final int NUM_ALPHA_CHANNELS = 4;
-    private static final int INDEX_CONTENT_ALPHA = 0;
-    private static final int INDEX_COLOR_FILTER_ALPHA = 1;
-    private static final int INDEX_MODAL_ALPHA = 2;
-    /** Used to hide the app chip for 90:10 flex split. */
-    private static final int INDEX_MINIMUM_RATIO_ALPHA = 3;
-
-    private final MultiValueAlpha mMultiValueAlpha;
-
-    private View mMenuAnchorView;
-    private IconView mIconView;
-    // Two textview so we can ellipsize the collapsed view and crossfade on expand to the full name.
-    private TextView mIconTextCollapsedView;
-    private TextView mIconTextExpandedView;
-    private ImageView mIconArrowView;
-    private final Rect mBackgroundRelativeLtrLocation = new Rect();
-    final RectEvaluator mBackgroundAnimationRectEvaluator =
-            new RectEvaluator(mBackgroundRelativeLtrLocation);
-    private final int mCollapsedMenuDefaultWidth;
-    private final int mExpandedMenuDefaultWidth;
-    private final int mCollapsedMenuDefaultHeight;
-    private final int mExpandedMenuDefaultHeight;
-    private final int mIconMenuMarginTopStart;
-    private final int mMenuToChipGap;
-    private final int mBackgroundMarginTopStart;
-    private final int mAppNameHorizontalMargin;
-    private final int mIconViewMarginStart;
-    private final int mAppIconSize;
-    private final int mArrowSize;
-    private final int mIconViewDrawableExpandedSize;
-    private final int mArrowMarginEnd;
-    private AnimatorSet mAnimator;
-
-    private int mMaxWidth = Integer.MAX_VALUE;
-
-    private static final int INDEX_SPLIT_TRANSLATION = 0;
-    private static final int INDEX_MENU_TRANSLATION = 1;
-    private static final int INDEX_COUNT_TRANSLATION = 2;
-
-    private final MultiPropertyFactory<View> mViewTranslationX;
-    private final MultiPropertyFactory<View> mViewTranslationY;
-
-    /**
-     * Gets the view split x-axis translation
-     */
-    public MultiPropertyFactory<View>.MultiProperty getSplitTranslationX() {
-        return mViewTranslationX.get(INDEX_SPLIT_TRANSLATION);
-    }
-
-    /**
-     * Sets the view split x-axis translation
-     * @param translationX x-axis translation
-     */
-    public void setSplitTranslationX(float translationX) {
-        getSplitTranslationX().setValue(translationX);
-    }
-
-    /**
-     * Gets the view split y-axis translation
-     */
-    public MultiPropertyFactory<View>.MultiProperty getSplitTranslationY() {
-        return mViewTranslationY.get(INDEX_SPLIT_TRANSLATION);
-    }
-
-    /**
-     * Sets the view split y-axis translation
-     * @param translationY y-axis translation
-     */
-    public void setSplitTranslationY(float translationY) {
-        getSplitTranslationY().setValue(translationY);
-    }
-
-    /**
-     * Gets the menu x-axis translation for split task
-     */
-    public MultiPropertyFactory<View>.MultiProperty getMenuTranslationX() {
-        return mViewTranslationX.get(INDEX_MENU_TRANSLATION);
-    }
-
-    /**
-     * Gets the menu y-axis translation for split task
-     */
-    public MultiPropertyFactory<View>.MultiProperty getMenuTranslationY() {
-        return mViewTranslationY.get(INDEX_MENU_TRANSLATION);
-    }
-
-    public IconAppChipView(Context context) {
-        this(context, null);
-    }
-
-    public IconAppChipView(Context context, AttributeSet attrs) {
-        this(context, attrs, 0);
-    }
-
-    public IconAppChipView(Context context, AttributeSet attrs, int defStyleAttr) {
-        this(context, attrs, defStyleAttr, 0);
-    }
-
-    public IconAppChipView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
-            int defStyleRes) {
-        super(context, attrs, defStyleAttr, defStyleRes);
-        Resources res = getResources();
-        mMultiValueAlpha = new MultiValueAlpha(this, NUM_ALPHA_CHANNELS);
-        mMultiValueAlpha.setUpdateVisibility(/* updateVisibility= */ true);
-
-        // Menu dimensions
-        mCollapsedMenuDefaultWidth =
-                res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_collapsed_width);
-        mExpandedMenuDefaultWidth =
-                res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_expanded_width);
-        mCollapsedMenuDefaultHeight =
-                res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_collapsed_height);
-        mExpandedMenuDefaultHeight =
-                res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_expanded_height);
-        mIconMenuMarginTopStart = res.getDimensionPixelSize(
-                R.dimen.task_thumbnail_icon_menu_expanded_top_start_margin);
-        mMenuToChipGap = res.getDimensionPixelSize(
-                R.dimen.task_thumbnail_icon_menu_expanded_gap);
-
-        // Background dimensions
-        mBackgroundMarginTopStart = res.getDimensionPixelSize(
-                R.dimen.task_thumbnail_icon_menu_background_margin_top_start);
-
-        // Contents dimensions
-        mAppNameHorizontalMargin = res.getDimensionPixelSize(
-                R.dimen.task_thumbnail_icon_menu_app_name_margin_horizontal_collapsed);
-        mArrowMarginEnd = res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_arrow_margin);
-        mIconViewMarginStart = res.getDimensionPixelSize(
-                R.dimen.task_thumbnail_icon_view_start_margin);
-        mAppIconSize = res.getDimensionPixelSize(
-                R.dimen.task_thumbnail_icon_menu_app_icon_collapsed_size);
-        mArrowSize = res.getDimensionPixelSize(
-                R.dimen.task_thumbnail_icon_menu_arrow_size);
-        mIconViewDrawableExpandedSize = res.getDimensionPixelSize(
-                R.dimen.task_thumbnail_icon_menu_app_icon_expanded_size);
-
-        mViewTranslationX = new MultiPropertyFactory<>(this, VIEW_TRANSLATE_X,
-                INDEX_COUNT_TRANSLATION,
-                Float::sum);
-        mViewTranslationY = new MultiPropertyFactory<>(this, VIEW_TRANSLATE_Y,
-                INDEX_COUNT_TRANSLATION,
-                Float::sum);
-    }
-
-    @Override
-    protected void onFinishInflate() {
-        super.onFinishInflate();
-        mIconView = findViewById(R.id.icon_view);
-        mIconTextCollapsedView = findViewById(R.id.icon_text_collapsed);
-        mIconTextExpandedView = findViewById(R.id.icon_text_expanded);
-        mIconArrowView = findViewById(R.id.icon_arrow);
-        mMenuAnchorView = findViewById(R.id.icon_view_menu_anchor);
-    }
-
-    protected IconView getIconView() {
-        return mIconView;
-    }
-
-    @Override
-    public void setText(CharSequence text) {
-        if (mIconTextCollapsedView != null) {
-            mIconTextCollapsedView.setText(text);
-        }
-        if (mIconTextExpandedView != null) {
-            mIconTextExpandedView.setText(text);
-        }
-    }
-
-    @Override
-    public Drawable getDrawable() {
-        return mIconView == null ? null : mIconView.getDrawable();
-    }
-
-    @Override
-    public void setDrawable(Drawable icon) {
-        if (mIconView != null) {
-            mIconView.setDrawable(icon);
-        }
-    }
-
-    @Override
-    public void setDrawableSize(int iconWidth, int iconHeight) {
-        if (mIconView != null) {
-            mIconView.setDrawableSize(iconWidth, iconHeight);
-        }
-    }
-
-    /**
-     * Sets the maximum width of this Icon Menu. This is usually used when space is limited for
-     * split screen.
-     */
-    public void setMaxWidth(int maxWidth) {
-        // Width showing only the app icon and arrow. Max width should not be set to less than this.
-        int minimumMaxWidth = mIconViewMarginStart + mAppIconSize + mArrowSize + mArrowMarginEnd;
-        mMaxWidth = Math.max(maxWidth, minimumMaxWidth);
-    }
-
-    @Override
-    public void setIconOrientation(RecentsOrientedState orientationState, boolean isGridTask) {
-        RecentsPagedOrientationHandler orientationHandler =
-                orientationState.getOrientationHandler();
-        // Layout params for anchor view
-        LayoutParams anchorLayoutParams = (LayoutParams) mMenuAnchorView.getLayoutParams();
-        anchorLayoutParams.topMargin = mExpandedMenuDefaultHeight + mMenuToChipGap;
-        mMenuAnchorView.setLayoutParams(anchorLayoutParams);
-
-        // Layout Params for the Menu View (this)
-        LayoutParams iconMenuParams = (LayoutParams) getLayoutParams();
-        iconMenuParams.width = mExpandedMenuDefaultWidth;
-        iconMenuParams.height = mExpandedMenuDefaultHeight;
-        orientationHandler.setIconAppChipMenuParams(this, iconMenuParams, mIconMenuMarginTopStart,
-                mIconMenuMarginTopStart);
-        setLayoutParams(iconMenuParams);
-
-        // Layout params for the background
-        Rect collapsedBackgroundBounds = getCollapsedBackgroundLtrBounds();
-        mBackgroundRelativeLtrLocation.set(collapsedBackgroundBounds);
-        setOutlineProvider(new ViewOutlineProvider() {
-            final Rect mRtlAppliedOutlineBounds = new Rect();
-            @Override
-            public void getOutline(View view, Outline outline) {
-                mRtlAppliedOutlineBounds.set(mBackgroundRelativeLtrLocation);
-                if (isLayoutRtl()) {
-                    int width = getWidth();
-                    mRtlAppliedOutlineBounds.left = width - mBackgroundRelativeLtrLocation.right;
-                    mRtlAppliedOutlineBounds.right = width - mBackgroundRelativeLtrLocation.left;
-                }
-                outline.setRoundRect(
-                        mRtlAppliedOutlineBounds, mRtlAppliedOutlineBounds.height() / 2f);
-            }
-        });
-
-        // Layout Params for the Icon View
-        LayoutParams iconParams = (LayoutParams) mIconView.getLayoutParams();
-        int iconMarginStartRelativeToParent = mIconViewMarginStart + mBackgroundMarginTopStart;
-        orientationHandler.setIconAppChipChildrenParams(
-                iconParams, iconMarginStartRelativeToParent);
-
-        mIconView.setLayoutParams(iconParams);
-        mIconView.setDrawableSize(mAppIconSize, mAppIconSize);
-
-        // Layout Params for the collapsed Icon Text View
-        int textMarginStart =
-                iconMarginStartRelativeToParent + mAppIconSize + mAppNameHorizontalMargin;
-        LayoutParams iconTextCollapsedParams =
-                (LayoutParams) mIconTextCollapsedView.getLayoutParams();
-        orientationHandler.setIconAppChipChildrenParams(iconTextCollapsedParams, textMarginStart);
-        int collapsedTextWidth = collapsedBackgroundBounds.width() - mIconViewMarginStart
-                - mAppIconSize - mArrowSize - mAppNameHorizontalMargin - mArrowMarginEnd;
-        iconTextCollapsedParams.width = collapsedTextWidth;
-        mIconTextCollapsedView.setLayoutParams(iconTextCollapsedParams);
-        mIconTextCollapsedView.setAlpha(1f);
-
-        // Layout Params for the expanded Icon Text View
-        LayoutParams iconTextExpandedParams =
-                (LayoutParams) mIconTextExpandedView.getLayoutParams();
-        orientationHandler.setIconAppChipChildrenParams(iconTextExpandedParams, textMarginStart);
-        mIconTextExpandedView.setLayoutParams(iconTextExpandedParams);
-        mIconTextExpandedView.setAlpha(0f);
-        mIconTextExpandedView.setRevealClip(true, 0, mAppIconSize / 2f, collapsedTextWidth);
-
-        // Layout Params for the Icon Arrow View
-        LayoutParams iconArrowParams = (LayoutParams) mIconArrowView.getLayoutParams();
-        int arrowMarginStart = collapsedBackgroundBounds.right - mArrowMarginEnd - mArrowSize;
-        orientationHandler.setIconAppChipChildrenParams(iconArrowParams, arrowMarginStart);
-        mIconArrowView.setPivotY(iconArrowParams.height / 2f);
-        mIconArrowView.setLayoutParams(iconArrowParams);
-
-        // This method is called twice sometimes (like when rotating split tasks). It is called
-        // once before onMeasure and onLayout, and again after onMeasure but before onLayout with
-        // a new width. This happens because we update widths on rotation and on measure of
-        // grouped task views. Calling requestLayout() does not guarantee a call to onMeasure if
-        // it has just measured, so we explicitly call it here.
-        measure(MeasureSpec.makeMeasureSpec(getLayoutParams().width, MeasureSpec.EXACTLY),
-                MeasureSpec.makeMeasureSpec(getLayoutParams().height, MeasureSpec.EXACTLY));
-    }
-
-    @Override
-    public void setIconColorTint(int color, float amount) {
-        // RecentsView's COLOR_TINT animates between 0 and 0.5f, we want to hide the app chip menu.
-        float colorTintAlpha = Utilities.mapToRange(amount, 0f, 0.5f, 1f, 0f, LINEAR);
-        mMultiValueAlpha.get(INDEX_COLOR_FILTER_ALPHA).setValue(colorTintAlpha);
-    }
-
-    @Override
-    public void setContentAlpha(float alpha) {
-        mMultiValueAlpha.get(INDEX_CONTENT_ALPHA).setValue(alpha);
-    }
-
-    @Override
-    public void setModalAlpha(float alpha) {
-        mMultiValueAlpha.get(INDEX_MODAL_ALPHA).setValue(alpha);
-    }
-
-    @Override
-    public void setFlexSplitAlpha(float alpha) {
-        mMultiValueAlpha.get(INDEX_MINIMUM_RATIO_ALPHA).setValue(alpha);
-    }
-
-    @Override
-    public int getDrawableWidth() {
-        return mIconView == null ? 0 : mIconView.getDrawableWidth();
-    }
-
-    @Override
-    public int getDrawableHeight() {
-        return mIconView == null ? 0 : mIconView.getDrawableHeight();
-    }
-
-    protected void revealAnim(boolean isRevealing) {
-        cancelInProgressAnimations();
-        final Rect collapsedBackgroundBounds = getCollapsedBackgroundLtrBounds();
-        final Rect expandedBackgroundBounds = getExpandedBackgroundLtrBounds();
-        final Rect initialBackground = new Rect(mBackgroundRelativeLtrLocation);
-        mAnimator = new AnimatorSet();
-
-        if (isRevealing) {
-            boolean isRtl = isLayoutRtl();
-            bringToFront();
-            // Clip expanded text with reveal animation so it doesn't go beyond the edge of the menu
-            Animator expandedTextRevealAnim = ViewAnimationUtils.createCircularReveal(
-                    mIconTextExpandedView, 0, mIconTextExpandedView.getHeight() / 2,
-                    mIconTextCollapsedView.getWidth(), mIconTextExpandedView.getWidth());
-            // Animate background clipping
-            ValueAnimator backgroundAnimator = ValueAnimator.ofObject(
-                    mBackgroundAnimationRectEvaluator,
-                    initialBackground,
-                    expandedBackgroundBounds);
-            backgroundAnimator.addUpdateListener(valueAnimator -> invalidateOutline());
-
-            float iconViewScaling = mIconViewDrawableExpandedSize / (float) mAppIconSize;
-            float arrowTranslationX =
-                    expandedBackgroundBounds.right - collapsedBackgroundBounds.right;
-            float iconCenterToTextCollapsed = mAppIconSize / 2f + mAppNameHorizontalMargin;
-            float iconCenterToTextExpanded =
-                    mIconViewDrawableExpandedSize / 2f + mAppNameHorizontalMargin;
-            float textTranslationX = iconCenterToTextExpanded - iconCenterToTextCollapsed;
-
-            float textTranslationXWithRtl = isRtl ? -textTranslationX : textTranslationX;
-            float arrowTranslationWithRtl = isRtl ? -arrowTranslationX : arrowTranslationX;
-
-            mAnimator.playTogether(
-                    expandedTextRevealAnim,
-                    backgroundAnimator,
-                    ObjectAnimator.ofFloat(mIconView, SCALE_X, iconViewScaling),
-                    ObjectAnimator.ofFloat(mIconView, SCALE_Y, iconViewScaling),
-                    ObjectAnimator.ofFloat(mIconTextCollapsedView, TRANSLATION_X,
-                            textTranslationXWithRtl),
-                    ObjectAnimator.ofFloat(mIconTextExpandedView, TRANSLATION_X,
-                            textTranslationXWithRtl),
-                    ObjectAnimator.ofFloat(mIconTextCollapsedView, ALPHA, 0),
-                    ObjectAnimator.ofFloat(mIconTextExpandedView, ALPHA, 1),
-                    ObjectAnimator.ofFloat(mIconArrowView, TRANSLATION_X, arrowTranslationWithRtl),
-                    ObjectAnimator.ofFloat(mIconArrowView, SCALE_Y, -1));
-            mAnimator.setDuration(MENU_BACKGROUND_REVEAL_DURATION);
-        } else {
-            // Clip expanded text with reveal animation so it doesn't go beyond the edge of the menu
-            Animator expandedTextClipAnim = ViewAnimationUtils.createCircularReveal(
-                    mIconTextExpandedView, 0, mIconTextExpandedView.getHeight() / 2,
-                    mIconTextExpandedView.getWidth(), mIconTextCollapsedView.getWidth());
-
-            // Animate background clipping
-            ValueAnimator backgroundAnimator = ValueAnimator.ofObject(
-                    mBackgroundAnimationRectEvaluator,
-                    initialBackground,
-                    collapsedBackgroundBounds);
-            backgroundAnimator.addUpdateListener(valueAnimator -> invalidateOutline());
-
-            mAnimator.playTogether(
-                    expandedTextClipAnim,
-                    backgroundAnimator,
-                    ObjectAnimator.ofFloat(mIconView, SCALE_PROPERTY, 1),
-                    ObjectAnimator.ofFloat(mIconTextCollapsedView, TRANSLATION_X, 0),
-                    ObjectAnimator.ofFloat(mIconTextExpandedView, TRANSLATION_X, 0),
-                    ObjectAnimator.ofFloat(mIconTextCollapsedView, ALPHA, 1),
-                    ObjectAnimator.ofFloat(mIconTextExpandedView, ALPHA, 0),
-                    ObjectAnimator.ofFloat(mIconArrowView, TRANSLATION_X, 0),
-                    ObjectAnimator.ofFloat(mIconArrowView, SCALE_Y, 1));
-            mAnimator.setDuration(MENU_BACKGROUND_HIDE_DURATION);
-        }
-
-        mAnimator.setInterpolator(EMPHASIZED);
-        mAnimator.start();
-    }
-
-    private Rect getCollapsedBackgroundLtrBounds() {
-        Rect bounds = new Rect(
-                0,
-                0,
-                Math.min(mMaxWidth, mCollapsedMenuDefaultWidth),
-                mCollapsedMenuDefaultHeight);
-        bounds.offset(mBackgroundMarginTopStart, mBackgroundMarginTopStart);
-        return bounds;
-    }
-
-    private Rect getExpandedBackgroundLtrBounds() {
-        return new Rect(0, 0, mExpandedMenuDefaultWidth, mExpandedMenuDefaultHeight);
-    }
-
-    private void cancelInProgressAnimations() {
-        // We null the `AnimatorSet` because it holds references to the `Animators` which aren't
-        // expecting to be mutable and will cause a crash if they are re-used.
-        if (mAnimator != null && mAnimator.isStarted()) {
-            mAnimator.cancel();
-            mAnimator = null;
-        }
-    }
-
-    @Override
-    public View asView() {
-        return this;
-    }
-}
diff --git a/quickstep/src/com/android/quickstep/views/IconAppChipView.kt b/quickstep/src/com/android/quickstep/views/IconAppChipView.kt
new file mode 100644
index 0000000..85d14cc
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/IconAppChipView.kt
@@ -0,0 +1,438 @@
+/*
+ * Copyright (C) 2023 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.animation.AnimatorSet
+import android.animation.ObjectAnimator
+import android.animation.RectEvaluator
+import android.animation.ValueAnimator
+import android.content.Context
+import android.graphics.Outline
+import android.graphics.Rect
+import android.graphics.drawable.Drawable
+import android.util.AttributeSet
+import android.view.View
+import android.view.ViewAnimationUtils
+import android.view.ViewOutlineProvider
+import android.widget.FrameLayout
+import android.widget.ImageView
+import android.widget.TextView
+import com.android.app.animation.Interpolators
+import com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY
+import com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X
+import com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y
+import com.android.launcher3.R
+import com.android.launcher3.Utilities
+import com.android.launcher3.util.MultiPropertyFactory
+import com.android.launcher3.util.MultiPropertyFactory.FloatBiFunction
+import com.android.launcher3.util.MultiValueAlpha
+import com.android.quickstep.util.RecentsOrientedState
+import kotlin.math.max
+import kotlin.math.min
+
+/** An icon app menu view which can be used in place of an IconView in overview TaskViews. */
+class IconAppChipView
+@JvmOverloads
+constructor(
+    context: Context,
+    attrs: AttributeSet? = null,
+    defStyleAttr: Int = 0,
+    defStyleRes: Int = 0,
+) : FrameLayout(context, attrs, defStyleAttr, defStyleRes), TaskViewIcon {
+
+    private var iconView: IconView? = null
+    private var iconArrowView: ImageView? = null
+    private var menuAnchorView: View? = null
+    // Two textview so we can ellipsize the collapsed view and crossfade on expand to the full name.
+    private var iconTextCollapsedView: TextView? = null
+    private var iconTextExpandedView: TextView? = null
+
+    private val backgroundRelativeLtrLocation = Rect()
+    private val backgroundAnimationRectEvaluator = RectEvaluator(backgroundRelativeLtrLocation)
+
+    // Menu dimensions
+    private val collapsedMenuDefaultWidth: Int =
+        resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_collapsed_width)
+    private val expandedMenuDefaultWidth: Int =
+        resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_expanded_width)
+    private val collapsedMenuDefaultHeight =
+        resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_collapsed_height)
+    private val expandedMenuDefaultHeight =
+        resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_expanded_height)
+    private val iconMenuMarginTopStart =
+        resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_expanded_top_start_margin)
+    private val menuToChipGap: Int =
+        resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_expanded_gap)
+
+    // Background dimensions
+    private val backgroundMarginTopStart: Int =
+        resources.getDimensionPixelSize(
+            R.dimen.task_thumbnail_icon_menu_background_margin_top_start
+        )
+
+    // Contents dimensions
+    private val appNameHorizontalMargin =
+        resources.getDimensionPixelSize(
+            R.dimen.task_thumbnail_icon_menu_app_name_margin_horizontal_collapsed
+        )
+    private val arrowMarginEnd =
+        resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_arrow_margin)
+    private val iconViewMarginStart =
+        resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_view_start_margin)
+    private val appIconSize =
+        resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_app_icon_collapsed_size)
+    private val arrowSize =
+        resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_arrow_size)
+    private val iconViewDrawableExpandedSize =
+        resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_app_icon_expanded_size)
+
+    private var animator: AnimatorSet? = null
+
+    private val multiValueAlpha: MultiValueAlpha =
+        MultiValueAlpha(this, NUM_ALPHA_CHANNELS).apply { setUpdateVisibility(true) }
+
+    private val viewTranslationX: MultiPropertyFactory<View> =
+        MultiPropertyFactory(this, VIEW_TRANSLATE_X, INDEX_COUNT_TRANSLATION, SUM_AGGREGATOR)
+
+    private val viewTranslationY: MultiPropertyFactory<View> =
+        MultiPropertyFactory(this, VIEW_TRANSLATE_Y, INDEX_COUNT_TRANSLATION, SUM_AGGREGATOR)
+
+    var maxWidth = Int.MAX_VALUE
+        /**
+         * Sets the maximum width of this Icon Menu. This is usually used when space is limited for
+         * split screen.
+         */
+        set(value) {
+            // Width showing only the app icon and arrow. Max width should not be set to less than
+            // this.
+            val minMaxWidth = iconViewMarginStart + appIconSize + arrowSize + arrowMarginEnd
+            field = max(value, minMaxWidth)
+        }
+
+    override fun onFinishInflate() {
+        super.onFinishInflate()
+        iconView = findViewById(R.id.icon_view)
+        iconTextCollapsedView = findViewById(R.id.icon_text_collapsed)
+        iconTextExpandedView = findViewById(R.id.icon_text_expanded)
+        iconArrowView = findViewById(R.id.icon_arrow)
+        menuAnchorView = findViewById(R.id.icon_view_menu_anchor)
+    }
+
+    override fun setText(text: CharSequence?) {
+        iconTextCollapsedView?.text = text
+        iconTextExpandedView?.text = text
+    }
+
+    override fun getDrawable(): Drawable? = iconView?.drawable
+
+    override fun setDrawable(icon: Drawable?) {
+        iconView?.drawable = icon
+    }
+
+    override fun setDrawableSize(iconWidth: Int, iconHeight: Int) {
+        iconView?.setDrawableSize(iconWidth, iconHeight)
+    }
+
+    override fun setIconOrientation(orientationState: RecentsOrientedState, isGridTask: Boolean) {
+        val orientationHandler = orientationState.orientationHandler
+        // Layout params for anchor view
+        val anchorLayoutParams = menuAnchorView!!.layoutParams as LayoutParams
+        anchorLayoutParams.topMargin = expandedMenuDefaultHeight + menuToChipGap
+        menuAnchorView!!.layoutParams = anchorLayoutParams
+
+        // Layout Params for the Menu View (this)
+        val iconMenuParams = layoutParams as LayoutParams
+        iconMenuParams.width = expandedMenuDefaultWidth
+        iconMenuParams.height = expandedMenuDefaultHeight
+        orientationHandler.setIconAppChipMenuParams(
+            this,
+            iconMenuParams,
+            iconMenuMarginTopStart,
+            iconMenuMarginTopStart,
+        )
+        layoutParams = iconMenuParams
+
+        // Layout params for the background
+        val collapsedBackgroundBounds = getCollapsedBackgroundLtrBounds()
+        backgroundRelativeLtrLocation.set(collapsedBackgroundBounds)
+        outlineProvider =
+            object : ViewOutlineProvider() {
+                val mRtlAppliedOutlineBounds: Rect = Rect()
+
+                override fun getOutline(view: View, outline: Outline) {
+                    mRtlAppliedOutlineBounds.set(backgroundRelativeLtrLocation)
+                    if (isLayoutRtl) {
+                        val width = width
+                        mRtlAppliedOutlineBounds.left = width - backgroundRelativeLtrLocation.right
+                        mRtlAppliedOutlineBounds.right = width - backgroundRelativeLtrLocation.left
+                    }
+                    outline.setRoundRect(
+                        mRtlAppliedOutlineBounds,
+                        mRtlAppliedOutlineBounds.height() / 2f,
+                    )
+                }
+            }
+
+        // Layout Params for the Icon View
+        val iconParams = iconView!!.layoutParams as LayoutParams
+        val iconMarginStartRelativeToParent = iconViewMarginStart + backgroundMarginTopStart
+        orientationHandler.setIconAppChipChildrenParams(iconParams, iconMarginStartRelativeToParent)
+
+        iconView!!.layoutParams = iconParams
+        iconView!!.setDrawableSize(appIconSize, appIconSize)
+
+        // Layout Params for the collapsed Icon Text View
+        val textMarginStart =
+            iconMarginStartRelativeToParent + appIconSize + appNameHorizontalMargin
+        val iconTextCollapsedParams = iconTextCollapsedView!!.layoutParams as LayoutParams
+        orientationHandler.setIconAppChipChildrenParams(iconTextCollapsedParams, textMarginStart)
+        val collapsedTextWidth =
+            (collapsedBackgroundBounds.width() -
+                iconViewMarginStart -
+                appIconSize -
+                arrowSize -
+                appNameHorizontalMargin -
+                arrowMarginEnd)
+        iconTextCollapsedParams.width = collapsedTextWidth
+        iconTextCollapsedView!!.layoutParams = iconTextCollapsedParams
+        iconTextCollapsedView!!.alpha = 1f
+
+        // Layout Params for the expanded Icon Text View
+        val iconTextExpandedParams = iconTextExpandedView!!.layoutParams as LayoutParams
+        orientationHandler.setIconAppChipChildrenParams(iconTextExpandedParams, textMarginStart)
+        iconTextExpandedView!!.layoutParams = iconTextExpandedParams
+        iconTextExpandedView!!.alpha = 0f
+        iconTextExpandedView!!.setRevealClip(
+            true,
+            0f,
+            appIconSize / 2f,
+            collapsedTextWidth.toFloat(),
+        )
+
+        // Layout Params for the Icon Arrow View
+        val iconArrowParams = iconArrowView!!.layoutParams as LayoutParams
+        val arrowMarginStart = collapsedBackgroundBounds.right - arrowMarginEnd - arrowSize
+        orientationHandler.setIconAppChipChildrenParams(iconArrowParams, arrowMarginStart)
+        iconArrowView!!.pivotY = iconArrowParams.height / 2f
+        iconArrowView!!.layoutParams = iconArrowParams
+
+        // This method is called twice sometimes (like when rotating split tasks). It is called
+        // once before onMeasure and onLayout, and again after onMeasure but before onLayout with
+        // a new width. This happens because we update widths on rotation and on measure of
+        // grouped task views. Calling requestLayout() does not guarantee a call to onMeasure if
+        // it has just measured, so we explicitly call it here.
+        measure(
+            MeasureSpec.makeMeasureSpec(layoutParams.width, MeasureSpec.EXACTLY),
+            MeasureSpec.makeMeasureSpec(layoutParams.height, MeasureSpec.EXACTLY),
+        )
+    }
+
+    override fun setIconColorTint(color: Int, amount: Float) {
+        // RecentsView's COLOR_TINT animates between 0 and 0.5f, we want to hide the app chip menu.
+        val colorTintAlpha = Utilities.mapToRange(amount, 0f, 0.5f, 1f, 0f, Interpolators.LINEAR)
+        multiValueAlpha[INDEX_COLOR_FILTER_ALPHA].value = colorTintAlpha
+    }
+
+    override fun setContentAlpha(alpha: Float) {
+        multiValueAlpha[INDEX_CONTENT_ALPHA].value = alpha
+    }
+
+    override fun setModalAlpha(alpha: Float) {
+        multiValueAlpha[INDEX_MODAL_ALPHA].value = alpha
+    }
+
+    override fun setFlexSplitAlpha(alpha: Float) {
+        multiValueAlpha[INDEX_MINIMUM_RATIO_ALPHA].value = alpha
+    }
+
+    override fun getDrawableWidth(): Int = iconView?.drawableWidth ?: 0
+
+    override fun getDrawableHeight(): Int = iconView?.drawableHeight ?: 0
+
+    /** Gets the view split x-axis translation */
+    fun getSplitTranslationX(): MultiPropertyFactory<View>.MultiProperty =
+        viewTranslationX.get(INDEX_SPLIT_TRANSLATION)
+
+    /**
+     * Sets the view split x-axis translation
+     *
+     * @param value x-axis translation
+     */
+    fun setSplitTranslationX(value: Float) {
+        getSplitTranslationX().value = value
+    }
+
+    /** Gets the view split y-axis translation */
+    fun getSplitTranslationY(): MultiPropertyFactory<View>.MultiProperty =
+        viewTranslationY[INDEX_SPLIT_TRANSLATION]
+
+    /**
+     * Sets the view split y-axis translation
+     *
+     * @param value y-axis translation
+     */
+    fun setSplitTranslationY(value: Float) {
+        getSplitTranslationY().value = value
+    }
+
+    /** Gets the menu x-axis translation for split task */
+    fun getMenuTranslationX(): MultiPropertyFactory<View>.MultiProperty =
+        viewTranslationX[INDEX_MENU_TRANSLATION]
+
+    /** Gets the menu y-axis translation for split task */
+    fun getMenuTranslationY(): MultiPropertyFactory<View>.MultiProperty =
+        viewTranslationY[INDEX_MENU_TRANSLATION]
+
+    internal fun revealAnim(isRevealing: Boolean) {
+        cancelInProgressAnimations()
+        val collapsedBackgroundBounds = getCollapsedBackgroundLtrBounds()
+        val expandedBackgroundBounds = getExpandedBackgroundLtrBounds()
+        val initialBackground = Rect(backgroundRelativeLtrLocation)
+        animator = AnimatorSet()
+
+        if (isRevealing) {
+            val isRtl = isLayoutRtl
+            bringToFront()
+            // Clip expanded text with reveal animation so it doesn't go beyond the edge of the menu
+            val expandedTextRevealAnim =
+                ViewAnimationUtils.createCircularReveal(
+                    iconTextExpandedView,
+                    0,
+                    iconTextExpandedView!!.height / 2,
+                    iconTextCollapsedView!!.width.toFloat(),
+                    iconTextExpandedView!!.width.toFloat(),
+                )
+            // Animate background clipping
+            val backgroundAnimator =
+                ValueAnimator.ofObject(
+                    backgroundAnimationRectEvaluator,
+                    initialBackground,
+                    expandedBackgroundBounds,
+                )
+            backgroundAnimator.addUpdateListener { invalidateOutline() }
+
+            val iconViewScaling = iconViewDrawableExpandedSize / appIconSize.toFloat()
+            val arrowTranslationX =
+                (expandedBackgroundBounds.right - collapsedBackgroundBounds.right).toFloat()
+            val iconCenterToTextCollapsed = appIconSize / 2f + appNameHorizontalMargin
+            val iconCenterToTextExpanded =
+                iconViewDrawableExpandedSize / 2f + appNameHorizontalMargin
+            val textTranslationX = iconCenterToTextExpanded - iconCenterToTextCollapsed
+
+            val textTranslationXWithRtl = if (isRtl) -textTranslationX else textTranslationX
+            val arrowTranslationWithRtl = if (isRtl) -arrowTranslationX else arrowTranslationX
+
+            animator!!.playTogether(
+                expandedTextRevealAnim,
+                backgroundAnimator,
+                ObjectAnimator.ofFloat(iconView, SCALE_X, iconViewScaling),
+                ObjectAnimator.ofFloat(iconView, SCALE_Y, iconViewScaling),
+                ObjectAnimator.ofFloat(
+                    iconTextCollapsedView,
+                    TRANSLATION_X,
+                    textTranslationXWithRtl,
+                ),
+                ObjectAnimator.ofFloat(
+                    iconTextExpandedView,
+                    TRANSLATION_X,
+                    textTranslationXWithRtl,
+                ),
+                ObjectAnimator.ofFloat(iconTextCollapsedView, ALPHA, 0f),
+                ObjectAnimator.ofFloat(iconTextExpandedView, ALPHA, 1f),
+                ObjectAnimator.ofFloat(iconArrowView, TRANSLATION_X, arrowTranslationWithRtl),
+                ObjectAnimator.ofFloat(iconArrowView, SCALE_Y, -1f),
+            )
+            animator!!.setDuration(MENU_BACKGROUND_REVEAL_DURATION.toLong())
+        } else {
+            // Clip expanded text with reveal animation so it doesn't go beyond the edge of the menu
+            val expandedTextClipAnim =
+                ViewAnimationUtils.createCircularReveal(
+                    iconTextExpandedView,
+                    0,
+                    iconTextExpandedView!!.height / 2,
+                    iconTextExpandedView!!.width.toFloat(),
+                    iconTextCollapsedView!!.width.toFloat(),
+                )
+
+            // Animate background clipping
+            val backgroundAnimator =
+                ValueAnimator.ofObject(
+                    backgroundAnimationRectEvaluator,
+                    initialBackground,
+                    collapsedBackgroundBounds,
+                )
+            backgroundAnimator.addUpdateListener { valueAnimator: ValueAnimator? ->
+                invalidateOutline()
+            }
+
+            animator!!.playTogether(
+                expandedTextClipAnim,
+                backgroundAnimator,
+                ObjectAnimator.ofFloat(iconView, SCALE_PROPERTY, 1f),
+                ObjectAnimator.ofFloat(iconTextCollapsedView, TRANSLATION_X, 0f),
+                ObjectAnimator.ofFloat(iconTextExpandedView, TRANSLATION_X, 0f),
+                ObjectAnimator.ofFloat(iconTextCollapsedView, ALPHA, 1f),
+                ObjectAnimator.ofFloat(iconTextExpandedView, ALPHA, 0f),
+                ObjectAnimator.ofFloat(iconArrowView, TRANSLATION_X, 0f),
+                ObjectAnimator.ofFloat(iconArrowView, SCALE_Y, 1f),
+            )
+            animator!!.setDuration(MENU_BACKGROUND_HIDE_DURATION.toLong())
+        }
+
+        animator!!.interpolator = Interpolators.EMPHASIZED
+        animator!!.start()
+    }
+
+    private fun getCollapsedBackgroundLtrBounds(): Rect {
+        val bounds =
+            Rect(0, 0, min(maxWidth, collapsedMenuDefaultWidth), collapsedMenuDefaultHeight)
+        bounds.offset(backgroundMarginTopStart, backgroundMarginTopStart)
+        return bounds
+    }
+
+    private fun getExpandedBackgroundLtrBounds() =
+        Rect(0, 0, expandedMenuDefaultWidth, expandedMenuDefaultHeight)
+
+    private fun cancelInProgressAnimations() {
+        // We null the `AnimatorSet` because it holds references to the `Animators` which aren't
+        // expecting to be mutable and will cause a crash if they are re-used.
+        if (animator != null && animator!!.isStarted) {
+            animator!!.cancel()
+            animator = null
+        }
+    }
+
+    override fun asView(): View = this
+
+    private companion object {
+        private val SUM_AGGREGATOR = FloatBiFunction { a: Float, b: Float -> a + b }
+
+        private const val MENU_BACKGROUND_REVEAL_DURATION = 417
+        private const val MENU_BACKGROUND_HIDE_DURATION = 333
+
+        private const val NUM_ALPHA_CHANNELS = 4
+        private const val INDEX_CONTENT_ALPHA = 0
+        private const val INDEX_COLOR_FILTER_ALPHA = 1
+        private const val INDEX_MODAL_ALPHA = 2
+
+        /** Used to hide the app chip for 90:10 flex split. */
+        private const val INDEX_MINIMUM_RATIO_ALPHA = 3
+
+        private const val INDEX_SPLIT_TRANSLATION = 0
+        private const val INDEX_MENU_TRANSLATION = 1
+        private const val INDEX_COUNT_TRANSLATION = 2
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/views/RecentsDismissUtils.kt b/quickstep/src/com/android/quickstep/views/RecentsDismissUtils.kt
new file mode 100644
index 0000000..96eed87
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/RecentsDismissUtils.kt
@@ -0,0 +1,294 @@
+/*
+ * 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.views
+
+import android.os.VibrationAttributes
+import androidx.dynamicanimation.animation.FloatPropertyCompat
+import androidx.dynamicanimation.animation.FloatValueHolder
+import androidx.dynamicanimation.animation.SpringAnimation
+import androidx.dynamicanimation.animation.SpringForce
+import com.android.launcher3.R
+import com.android.launcher3.Utilities.boundToRange
+import com.android.launcher3.touch.SingleAxisSwipeDetector
+import com.android.launcher3.util.DynamicResource
+import com.android.launcher3.util.MSDLPlayerWrapper
+import com.android.quickstep.util.TaskGridNavHelper
+import com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY
+import com.google.android.msdl.data.model.MSDLToken
+import com.google.android.msdl.domain.InteractionProperties
+import kotlin.math.abs
+
+/**
+ * Helper class for [RecentsView]. This util class contains refactored and extracted functions from
+ * RecentsView related to TaskView dismissal.
+ */
+class RecentsDismissUtils(private val recentsView: RecentsView<*, *>) {
+
+    /**
+     * Creates the spring animations which run when a dragged task view in overview is released.
+     *
+     * <p>When a task dismiss is cancelled, the task will return to its original position via a
+     * spring animation. As it passes the threshold of its settling state, its neighbors will spring
+     * in response to the perceived impact of the settling task.
+     */
+    fun createTaskDismissSettlingSpringAnimation(
+        draggedTaskView: TaskView?,
+        velocity: Float,
+        isDismissing: Boolean,
+        detector: SingleAxisSwipeDetector,
+        dismissLength: Int,
+        onEndRunnable: () -> Unit,
+    ): SpringAnimation? {
+        draggedTaskView ?: return null
+        val taskDismissFloatProperty =
+            FloatPropertyCompat.createFloatPropertyCompat(
+                draggedTaskView.secondaryDismissTranslationProperty
+            )
+        // Animate dragged task towards dismissal or rest state.
+        val draggedTaskViewSpringAnimation =
+            SpringAnimation(draggedTaskView, taskDismissFloatProperty)
+                .setSpring(createExpressiveDismissSpringForce())
+                .setStartVelocity(if (detector.isFling(velocity)) velocity else 0f)
+                .addUpdateListener { animation, value, _ ->
+                    if (isDismissing && abs(value) >= abs(dismissLength)) {
+                        animation.cancel()
+                    } else if (draggedTaskView.isRunningTask && recentsView.enableDrawingLiveTile) {
+                        recentsView.runActionOnRemoteHandles { remoteTargetHandle ->
+                            remoteTargetHandle.taskViewSimulator.taskSecondaryTranslation.value =
+                                taskDismissFloatProperty.getValue(draggedTaskView)
+                        }
+                        recentsView.redrawLiveTile()
+                    }
+                }
+                .addEndListener { _, _, _, _ ->
+                    if (isDismissing) {
+                        recentsView.dismissTask(
+                            draggedTaskView,
+                            /* animateTaskView = */ false,
+                            /* removeTask = */ true,
+                        )
+                    } else {
+                        recentsView.onDismissAnimationEnds()
+                    }
+                    onEndRunnable()
+                }
+        if (!isDismissing) {
+            addNeighboringSpringAnimationsForDismissCancel(
+                draggedTaskView,
+                draggedTaskViewSpringAnimation,
+            )
+        }
+        return draggedTaskViewSpringAnimation
+    }
+
+    private fun addNeighboringSpringAnimationsForDismissCancel(
+        draggedTaskView: TaskView,
+        draggedTaskViewSpringAnimation: SpringAnimation,
+    ) {
+        // Empty spring animation exists for conditional start, and to drive neighboring springs.
+        val neighborsToSettle =
+            SpringAnimation(FloatValueHolder()).setSpring(createExpressiveDismissSpringForce())
+        var lastPosition = 0f
+        var startSettling = false
+        draggedTaskViewSpringAnimation.addUpdateListener { _, value, velocity ->
+            // Start the settling animation the first time the dragged task passes the origin (from
+            // negative displacement to positive displacement). We do not check for an exact value
+            // to compare to, as the update listener does not necessarily hit every value (e.g. a
+            // value of zero). Do not check again once it has started settling, as a spring can
+            // bounce past the origin multiple times depending on the stiffness and damping ratio.
+            if (startSettling) return@addUpdateListener
+            if (lastPosition < 0 && value >= 0) {
+                startSettling = true
+            }
+            lastPosition = value
+            if (startSettling) {
+                neighborsToSettle.setStartVelocity(velocity).animateToFinalPosition(0f)
+                playDismissSettlingHaptic(velocity)
+            }
+        }
+
+        // Add tasks before dragged index, fanning out from the dragged task.
+        // The order they are added matters, as each spring drives the next.
+        var previousNeighbor = neighborsToSettle
+        getTasksOffsetPairAdjacentToDraggedTask(draggedTaskView, towardsStart = true).forEach {
+            (taskView, offset) ->
+            previousNeighbor =
+                createNeighboringTaskViewSpringAnimation(
+                    taskView,
+                    offset * ADDITIONAL_DISMISS_DAMPING_RATIO,
+                    previousNeighbor,
+                )
+        }
+        // Add tasks after dragged index, fanning out from the dragged task.
+        // The order they are added matters, as each spring drives the next.
+        previousNeighbor = neighborsToSettle
+        getTasksOffsetPairAdjacentToDraggedTask(draggedTaskView, towardsStart = false).forEach {
+            (taskView, offset) ->
+            previousNeighbor =
+                createNeighboringTaskViewSpringAnimation(
+                    taskView,
+                    offset * ADDITIONAL_DISMISS_DAMPING_RATIO,
+                    previousNeighbor,
+                )
+        }
+    }
+
+    /**
+     * Gets pairs of (TaskView, offset) adjacent the dragged task in visual order.
+     *
+     * <p>Gets tasks either before or after the dragged task along with their offset from it. The
+     * offset is the distance between indices for carousels, or distance between columns for grids.
+     */
+    private fun getTasksOffsetPairAdjacentToDraggedTask(
+        draggedTaskView: TaskView,
+        towardsStart: Boolean,
+    ): Sequence<Pair<TaskView, Int>> {
+        if (recentsView.showAsGrid()) {
+            val taskGridNavHelper =
+                TaskGridNavHelper(
+                    recentsView.topRowIdArray,
+                    recentsView.bottomRowIdArray,
+                    recentsView.mUtils.getLargeTaskViewIds(),
+                    hasAddDesktopButton = false,
+                )
+            return taskGridNavHelper
+                .gridTaskViewIdOffsetPairInTabOrderSequence(
+                    draggedTaskView.taskViewId,
+                    towardsStart,
+                )
+                .mapNotNull { (taskViewId, columnOffset) ->
+                    recentsView.getTaskViewFromTaskViewId(taskViewId)?.let { taskView ->
+                        Pair(taskView, columnOffset)
+                    }
+                }
+        } else {
+            val taskViewList = recentsView.mUtils.taskViews.toList()
+            val draggedTaskViewIndex = taskViewList.indexOf(draggedTaskView)
+
+            return if (towardsStart) {
+                taskViewList
+                    .take(draggedTaskViewIndex)
+                    .reversed()
+                    .mapIndexed { index, taskView -> Pair(taskView, index + 1) }
+                    .asSequence()
+            } else {
+                taskViewList
+                    .takeLast(taskViewList.size - draggedTaskViewIndex - 1)
+                    .mapIndexed { index, taskView -> Pair(taskView, index + 1) }
+                    .asSequence()
+            }
+        }
+    }
+
+    /** Creates a neighboring task view spring, driven by the spring of its neighbor. */
+    private fun createNeighboringTaskViewSpringAnimation(
+        taskView: TaskView,
+        dampingOffsetRatio: Float,
+        previousNeighborSpringAnimation: SpringAnimation,
+    ): SpringAnimation {
+        val neighboringTaskViewSpringAnimation =
+            SpringAnimation(
+                    taskView,
+                    FloatPropertyCompat.createFloatPropertyCompat(
+                        taskView.secondaryDismissTranslationProperty
+                    ),
+                )
+                .setSpring(createExpressiveDismissSpringForce(dampingOffsetRatio))
+        // Update live tile on spring animation.
+        if (taskView.isRunningTask && recentsView.enableDrawingLiveTile) {
+            neighboringTaskViewSpringAnimation.addUpdateListener { _, _, _ ->
+                recentsView.runActionOnRemoteHandles { remoteTargetHandle ->
+                    remoteTargetHandle.taskViewSimulator.taskSecondaryTranslation.value =
+                        taskView.secondaryDismissTranslationProperty.get(taskView)
+                }
+                recentsView.redrawLiveTile()
+            }
+        }
+        // Drive current neighbor's spring with the previous neighbor's.
+        previousNeighborSpringAnimation.addUpdateListener { _, value, _ ->
+            neighboringTaskViewSpringAnimation.animateToFinalPosition(value)
+        }
+        return neighboringTaskViewSpringAnimation
+    }
+
+    private fun createExpressiveDismissSpringForce(dampingRatioOffset: Float = 0f): SpringForce {
+        val resourceProvider = DynamicResource.provider(recentsView.mContainer)
+        return SpringForce()
+            .setDampingRatio(
+                resourceProvider.getFloat(R.dimen.expressive_dismiss_task_trans_y_damping_ratio) +
+                    dampingRatioOffset
+            )
+            .setStiffness(
+                resourceProvider.getFloat(R.dimen.expressive_dismiss_task_trans_y_stiffness)
+            )
+    }
+
+    /**
+     * Plays a haptic as the dragged task view settles back into its rest state.
+     *
+     * <p>Haptic intensity is proportional to velocity.
+     */
+    private fun playDismissSettlingHaptic(velocity: Float) {
+        val maxDismissSettlingVelocity =
+            recentsView.pagedOrientationHandler.getSecondaryDimension(recentsView)
+        MSDLPlayerWrapper.INSTANCE.get(recentsView.context)
+            .playToken(
+                MSDLToken.CANCEL,
+                InteractionProperties.DynamicVibrationScale(
+                    boundToRange(velocity / maxDismissSettlingVelocity, 0f, 1f),
+                    VibrationAttributes.Builder()
+                        .setUsage(VibrationAttributes.USAGE_TOUCH)
+                        .setFlags(VibrationAttributes.FLAG_PIPELINED_EFFECT)
+                        .build(),
+                ),
+            )
+    }
+
+    /** Animates RecentsView's scale to the provided value, using spring animations. */
+    fun animateRecentsScale(scale: Float): SpringAnimation {
+        val resourceProvider = DynamicResource.provider(recentsView.mContainer)
+        val dampingRatio = resourceProvider.getFloat(R.dimen.swipe_up_rect_scale_damping_ratio)
+        val stiffness = resourceProvider.getFloat(R.dimen.swipe_up_rect_scale_stiffness)
+
+        // Spring which sets the Recents scale on update. This is needed, as the SpringAnimation
+        // struggles to animate small values like changing recents scale from 0.9 to 1. So
+        // we animate over a larger range (e.g. 900 to 1000) and convert back to the required value.
+        // (This is instead of converting RECENTS_SCALE_PROPERTY to a FloatPropertyCompat and
+        // animating it directly via springs.)
+        val initialRecentsScaleSpringValue =
+            RECENTS_SCALE_SPRING_MULTIPLIER * RECENTS_SCALE_PROPERTY.get(recentsView)
+        return SpringAnimation(FloatValueHolder(initialRecentsScaleSpringValue))
+            .setSpring(
+                SpringForce(initialRecentsScaleSpringValue)
+                    .setDampingRatio(dampingRatio)
+                    .setStiffness(stiffness)
+            )
+            .addUpdateListener { _, value, _ ->
+                RECENTS_SCALE_PROPERTY.setValue(
+                    recentsView,
+                    value / RECENTS_SCALE_SPRING_MULTIPLIER,
+                )
+            }
+            .apply { animateToFinalPosition(RECENTS_SCALE_SPRING_MULTIPLIER * scale) }
+    }
+
+    private companion object {
+        // The additional damping to apply to tasks further from the dismissed task.
+        private const val ADDITIONAL_DISMISS_DAMPING_RATIO = 0.15f
+        private const val RECENTS_SCALE_SPRING_MULTIPLIER = 1000f
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 5bfcf43..a29d302 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -851,6 +851,7 @@
     private final RecentsViewModel mRecentsViewModel;
     private final RecentsViewModelHelper mHelper;
     protected final RecentsViewUtils mUtils = new RecentsViewUtils(this);
+    protected final RecentsDismissUtils mDismissUtils = new RecentsDismissUtils(this);
 
     private final Matrix mTmpMatrix = new Matrix();
 
@@ -916,10 +917,12 @@
 
         mTaskViewPool = new ViewPool<>(context, this, R.layout.task, 20 /* max size */,
                 10 /* initial size */);
+        int groupedViewPoolInitialSize = enableRefactorTaskThumbnail() ? 2 : 10;
         mGroupedTaskViewPool = new ViewPool<>(context, this,
-                R.layout.task_grouped, 20 /* max size */, 10 /* initial size */);
+                R.layout.task_grouped, 20 /* max size */, groupedViewPoolInitialSize);
+        int desktopViewPoolInitialSize = DesktopModeStatus.canEnterDesktopMode(mContext) ? 1 : 0;
         mDesktopTaskViewPool = new ViewPool<>(context, this, R.layout.task_desktop,
-                5 /* max size */, 1 /* initial size */);
+                5 /* max size */, desktopViewPoolInitialSize);
 
         setOrientationHandler(mOrientationState.getOrientationHandler());
         mIsRtl = getPagedOrientationHandler().getRecentsRtlSetting(getResources());
@@ -2010,7 +2013,7 @@
             }
             // If the list changed, maybe the focused task doesn't exist anymore.
             if (newFocusedTaskView == null) {
-                newFocusedTaskView = mUtils.getExpectedFocusedTask();
+                newFocusedTaskView = mUtils.getFirstNonDesktopTaskView();
             }
         }
         setFocusedTaskViewId(
@@ -2116,15 +2119,6 @@
         return mTaskViewCount;
     }
 
-    /**
-     * Transverse RecentsView children to calculate the amount of DesktopTaskViews.
-     *
-     * @return Number of children that are instances of DesktopTaskView
-     */
-    private int getDesktopTaskViewCount() {
-        return mUtils.getDesktopTaskViewCount();
-    }
-
     /** Counts {@link TaskView}s that are not {@link DesktopTaskView} instances. */
     public int getNonDesktopTaskViewCount() {
         return mUtils.getNonDesktopTaskViewCount();
@@ -2917,28 +2911,40 @@
         BaseState<?> endState = mSizeStrategy.stateFromGestureEndTarget(endTarget);
         if (endState.displayOverviewTasksAsGrid(mContainer.getDeviceProfile())) {
             TaskView runningTaskView = getRunningTaskView();
-            float runningTaskPrimaryGridTranslation = 0;
-            float runningTaskSecondaryGridTranslation = 0;
+            float runningTaskGridTranslationX = 0;
+            float runningTaskGridTranslationY = 0;
             if (runningTaskView != null) {
                 // Apply the grid translation to running task unless it's being snapped to
                 // and removes the current translation applied to the running task.
-                runningTaskPrimaryGridTranslation = runningTaskView.getGridTranslationX()
+                runningTaskGridTranslationX = runningTaskView.getGridTranslationX()
                         - runningTaskView.getNonGridTranslationX();
-                runningTaskSecondaryGridTranslation = runningTaskView.getGridTranslationY();
+                runningTaskGridTranslationY = runningTaskView.getGridTranslationY();
             }
             for (RemoteTargetHandle remoteTargetHandle : remoteTargetHandles) {
                 TaskViewSimulator tvs = remoteTargetHandle.getTaskViewSimulator();
                 if (animatorSet == null) {
                     setGridProgress(1);
-                    tvs.taskPrimaryTranslation.value = runningTaskPrimaryGridTranslation;
-                    tvs.taskSecondaryTranslation.value = runningTaskSecondaryGridTranslation;
+                    if (enableGridOnlyOverview()) {
+                        tvs.taskGridTranslationX.value = runningTaskGridTranslationX;
+                        tvs.taskGridTranslationY.value = runningTaskGridTranslationY;
+                    } else {
+                        tvs.taskPrimaryTranslation.value = runningTaskGridTranslationX;
+                        tvs.taskSecondaryTranslation.value = runningTaskGridTranslationY;
+                    }
                 } else {
                     animatorSet.play(ObjectAnimator.ofFloat(this, RECENTS_GRID_PROGRESS, 1));
-                    animatorSet.play(tvs.carouselScale.animateToValue(1));
-                    animatorSet.play(tvs.taskPrimaryTranslation.animateToValue(
-                            runningTaskPrimaryGridTranslation));
-                    animatorSet.play(tvs.taskSecondaryTranslation.animateToValue(
-                            runningTaskSecondaryGridTranslation));
+                    if (enableGridOnlyOverview()) {
+                        animatorSet.play(tvs.carouselScale.animateToValue(1));
+                        animatorSet.play(tvs.taskGridTranslationX.animateToValue(
+                                runningTaskGridTranslationX));
+                        animatorSet.play(tvs.taskGridTranslationY.animateToValue(
+                                runningTaskGridTranslationY));
+                    } else {
+                        animatorSet.play(tvs.taskPrimaryTranslation.animateToValue(
+                                runningTaskGridTranslationX));
+                        animatorSet.play(tvs.taskSecondaryTranslation.animateToValue(
+                                runningTaskGridTranslationY));
+                    }
                 }
             }
         }
@@ -3064,7 +3070,7 @@
             focusedTaskViewId = INVALID_TASK_ID;
         } else if (enableLargeDesktopWindowingTile()
                 && getRunningTaskView() instanceof DesktopTaskView) {
-            TaskView focusedTaskView = getTaskViewAt(getDesktopTaskViewCount());
+            TaskView focusedTaskView = mUtils.getFirstNonDesktopTaskView();
             focusedTaskViewId =
                     focusedTaskView != null ? focusedTaskView.getTaskViewId() : INVALID_TASK_ID;
         } else {
@@ -3491,8 +3497,13 @@
         final TaskView runningTask = getRunningTaskView();
         if (showAsGrid() && enableGridOnlyOverview() && runningTask != null) {
             runActionOnRemoteHandles(
-                    remoteTargetHandle -> remoteTargetHandle.getTaskViewSimulator()
-                            .taskSecondaryTranslation.value = runningTask.getGridTranslationY()
+                    remoteTargetHandle -> {
+                        remoteTargetHandle.getTaskViewSimulator().taskGridTranslationX.value =
+                                runningTask.getGridTranslationX()
+                                        - runningTask.getNonGridTranslationX();
+                        remoteTargetHandle.getTaskViewSimulator().taskGridTranslationY.value =
+                                runningTask.getGridTranslationY();
+                    }
             );
         }
 
@@ -3606,12 +3617,9 @@
         if (taskView.isRunningTask()) {
             anim.addOnFrameCallback(() -> {
                 if (!mEnableDrawingLiveTile) return;
-                runActionOnRemoteHandles(
-                        remoteTargetHandle -> remoteTargetHandle.getTaskViewSimulator()
-                                .taskSecondaryTranslation.value = getPagedOrientationHandler()
-                                .getSecondaryValue(taskView.getTranslationX(),
-                                        taskView.getTranslationY()
-                                ));
+                runActionOnRemoteHandles(remoteTargetHandle ->
+                        remoteTargetHandle.getTaskViewSimulator().taskSecondaryTranslation.value =
+                                taskView.getSecondaryDismissTranslationProperty().get(taskView));
                 redrawLiveTile();
             });
         }
@@ -3933,9 +3941,9 @@
         int distanceFromDismissedTask = 1;
         int slidingTranslation = 0;
         if (isSlidingTasks) {
-            int nextSnappedPage = isStagingFocusedTask
-                    ? indexOfChild(mUtils.getFirstSmallTaskView())
-                    : mUtils.getDesktopTaskViewCount();
+            int nextSnappedPage = indexOfChild(isStagingFocusedTask
+                    ? mUtils.getFirstSmallTaskView()
+                    : mUtils.getFirstNonDesktopTaskView());
             slidingTranslation = getPagedOrientationHandler().getPrimaryScroll(this)
                     - getScrollForPage(nextSnappedPage);
             slidingTranslation += mIsRtl ? newClearAllShortTotalWidthTranslation
@@ -3961,7 +3969,7 @@
                             Math.abs(i - dismissedIndex),
                             scrollDiff,
                             anim,
-                            splitTimings, i);
+                            splitTimings);
                     needsCurveUpdates = true;
                 }
             } else if (child instanceof TaskView taskView) {
@@ -4337,8 +4345,12 @@
             int indexDiff,
             int scrollDiffPerPage,
             PendingAnimation pendingAnimation,
-            SplitAnimationTimings splitTimings,
-            int index) {
+            SplitAnimationTimings splitTimings) {
+        // No need to translate the AddDesktopButton on dismissing a TaskView, which should be
+        // always at the right most position, even when dismissing the last TaskView.
+        if (view instanceof AddDesktopButton) {
+            return;
+        }
         FloatProperty translationProperty = view instanceof TaskView
                 ? ((TaskView) view).getPrimaryDismissTranslationProperty()
                 : getPagedOrientationHandler().getPrimaryViewTranslate();
@@ -5655,7 +5667,7 @@
             anim.play(ObjectAnimator.ofFloat(this, FULLSCREEN_PROGRESS, 1));
             anim.addListener(new AnimatorListenerAdapter() {
                 @Override
-                public void onAnimationStart(@NonNull Animator animation, boolean isReverse) {
+                public void onAnimationStart(@NonNull Animator animation) {
                     taskView.getThumbnailBounds(mTempRect, /*relativeToDragLayer=*/true);
                     getTaskDimension(mContext, mContainer.getDeviceProfile(), mTempPointF);
                     Rect fullscreenBounds = new Rect(0, 0, (int) mTempPointF.x,
@@ -5677,6 +5689,18 @@
                                 });
                     }
                 }
+
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    // If live tile is not launching, reset the pivot applied above.
+                    if (!taskView.isRunningTask()) {
+                        runActionOnRemoteHandles(
+                                remoteTargetHandle -> {
+                                    remoteTargetHandle.getTaskViewSimulator().setPivotOverride(
+                                            null);
+                                });
+                    }
+                }
             });
         } else if (!showAsGrid) {
             // We are launching an adjacent task, so parallax the center and other adjacent task.
@@ -5785,10 +5809,12 @@
 
         mPendingAnimation = new PendingAnimation(duration);
         mPendingAnimation.add(anim);
-        runActionOnRemoteHandles(
-                remoteTargetHandle -> remoteTargetHandle.getTaskViewSimulator()
-                        .addOverviewToAppAnim(mPendingAnimation, interpolator));
-        mPendingAnimation.addOnFrameCallback(this::redrawLiveTile);
+        if (taskView.isRunningTask()) {
+            runActionOnRemoteHandles(
+                    remoteTargetHandle -> remoteTargetHandle.getTaskViewSimulator()
+                            .addOverviewToAppAnim(mPendingAnimation, interpolator));
+            mPendingAnimation.addOnFrameCallback(this::redrawLiveTile);
+        }
         mPendingAnimation.addListener(new AnimatorListenerAdapter() {
             @Override
             public void onAnimationStart(Animator animation) {
@@ -6241,12 +6267,12 @@
     }
 
     @Override
-    protected int getChildVisibleSize(int index) {
-        final TaskView taskView = getTaskViewAt(index);
+    protected int getChildVisibleSize(int childIndex) {
+        final TaskView taskView = getTaskViewAt(childIndex);
         if (taskView == null) {
-            return super.getChildVisibleSize(index);
+            return super.getChildVisibleSize(childIndex);
         }
-        return (int) (super.getChildVisibleSize(index) * taskView.getSizeAdjustment(
+        return (int) (super.getChildVisibleSize(childIndex) * taskView.getSizeAdjustment(
                 showAsFullscreen()));
     }
 
@@ -6305,7 +6331,7 @@
      * Returns how many pixels the page is offset on the currently laid out dominant axis.
      */
     private int getUnclampedScrollOffset(int pageIndex) {
-        if (pageIndex == -1) {
+        if (pageIndex == INVALID_PAGE) {
             return 0;
         }
         // Don't dampen the scroll (due to overscroll) if the adjacent tasks are offscreen, so that
@@ -6943,10 +6969,17 @@
     public SpringAnimation createTaskDismissSettlingSpringAnimation(TaskView draggedTaskView,
             float velocity, boolean isDismissing, SingleAxisSwipeDetector detector,
             int dismissLength, Function0<Unit> onEndRunnable) {
-        return mUtils.createTaskDismissSettlingSpringAnimation(draggedTaskView, velocity,
+        return mDismissUtils.createTaskDismissSettlingSpringAnimation(draggedTaskView, velocity,
                 isDismissing, detector, dismissLength, onEndRunnable);
     }
 
+    /**
+     * Animates RecentsView's scale to the provided value, using spring animations.
+     */
+    public SpringAnimation animateRecentsScale(float scale) {
+        return mDismissUtils.animateRecentsScale(scale);
+    }
+
     public interface TaskLaunchListener {
         void onTaskLaunched();
     }
diff --git a/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt b/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt
index d3549fb..f742ec3 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt
+++ b/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt
@@ -17,30 +17,16 @@
 package com.android.quickstep.views
 
 import android.graphics.Rect
-import android.os.VibrationAttributes
 import android.view.View
 import androidx.core.view.children
-import androidx.dynamicanimation.animation.FloatPropertyCompat
-import androidx.dynamicanimation.animation.FloatValueHolder
-import androidx.dynamicanimation.animation.SpringAnimation
-import androidx.dynamicanimation.animation.SpringForce
 import com.android.launcher3.Flags.enableLargeDesktopWindowingTile
 import com.android.launcher3.Flags.enableSeparateExternalDisplayTasks
-import com.android.launcher3.R
-import com.android.launcher3.Utilities.boundToRange
-import com.android.launcher3.touch.SingleAxisSwipeDetector
-import com.android.launcher3.util.DynamicResource
 import com.android.launcher3.util.IntArray
-import com.android.launcher3.util.MSDLPlayerWrapper
 import com.android.quickstep.util.GroupTask
-import com.android.quickstep.util.TaskGridNavHelper
 import com.android.quickstep.util.isExternalDisplay
 import com.android.quickstep.views.RecentsView.RUNNING_TASK_ATTACH_ALPHA
 import com.android.systemui.shared.recents.model.ThumbnailData
-import com.google.android.msdl.data.model.MSDLToken
-import com.google.android.msdl.domain.InteractionProperties
 import java.util.function.BiConsumer
-import kotlin.math.abs
 
 /**
  * Helper class for [RecentsView]. This util class contains refactored and extracted functions from
@@ -87,7 +73,7 @@
     }
 
     /** Counts [TaskView]s that are [DesktopTaskView] instances. */
-    fun getDesktopTaskViewCount(): Int = taskViews.count { it is DesktopTaskView }
+    private fun getDesktopTaskViewCount(): Int = taskViews.count { it is DesktopTaskView }
 
     /** Counts [TaskView]s that are not [DesktopTaskView] instances. */
     fun getNonDesktopTaskViewCount(): Int = taskViews.count { it !is DesktopTaskView }
@@ -105,7 +91,7 @@
         }
 
     /** Returns the expected focus task. */
-    fun getExpectedFocusedTask(): TaskView? =
+    fun getFirstNonDesktopTaskView(): TaskView? =
         if (enableLargeDesktopWindowingTile()) taskViews.firstOrNull { it !is DesktopTaskView }
         else taskViews.firstOrNull()
 
@@ -308,234 +294,7 @@
         }
     }
 
-    /**
-     * Creates the spring animations which run when a dragged task view in overview is released.
-     *
-     * <p>When a task dismiss is cancelled, the task will return to its original position via a
-     * spring animation. As it passes the threshold of its settling state, its neighbors will spring
-     * in response to the perceived impact of the settling task.
-     */
-    fun createTaskDismissSettlingSpringAnimation(
-        draggedTaskView: TaskView?,
-        velocity: Float,
-        isDismissing: Boolean,
-        detector: SingleAxisSwipeDetector,
-        dismissLength: Int,
-        onEndRunnable: () -> Unit,
-    ): SpringAnimation? {
-        draggedTaskView ?: return null
-        val taskDismissFloatProperty =
-            FloatPropertyCompat.createFloatPropertyCompat(
-                draggedTaskView.secondaryDismissTranslationProperty
-            )
-        // Animate dragged task towards dismissal or rest state.
-        val draggedTaskViewSpringAnimation =
-            SpringAnimation(draggedTaskView, taskDismissFloatProperty)
-                .setSpring(createExpressiveDismissSpringForce())
-                .setStartVelocity(if (detector.isFling(velocity)) velocity else 0f)
-                .addUpdateListener { animation, value, _ ->
-                    if (isDismissing && abs(value) >= abs(dismissLength)) {
-                        // TODO(b/393553524): Remove 0 alpha, instead animate task fully off screen.
-                        draggedTaskView.alpha = 0f
-                        animation.cancel()
-                    } else if (draggedTaskView.isRunningTask && recentsView.enableDrawingLiveTile) {
-                        recentsView.runActionOnRemoteHandles { remoteTargetHandle ->
-                            remoteTargetHandle.taskViewSimulator.taskSecondaryTranslation.value =
-                                taskDismissFloatProperty.getValue(draggedTaskView)
-                        }
-                        recentsView.redrawLiveTile()
-                    }
-                }
-                .addEndListener { _, _, _, _ ->
-                    if (isDismissing) {
-                        recentsView.dismissTask(
-                            draggedTaskView,
-                            /* animateTaskView = */ false,
-                            /* removeTask = */ true,
-                        )
-                    } else {
-                        recentsView.onDismissAnimationEnds()
-                    }
-                    onEndRunnable()
-                }
-        if (!isDismissing) {
-            addNeighboringSpringAnimationsForDismissCancel(
-                draggedTaskView,
-                draggedTaskViewSpringAnimation,
-                recentsView.pageCount,
-            )
-        }
-        return draggedTaskViewSpringAnimation
-    }
-
-    private fun addNeighboringSpringAnimationsForDismissCancel(
-        draggedTaskView: TaskView,
-        draggedTaskViewSpringAnimation: SpringAnimation,
-        taskCount: Int,
-    ) {
-        // Empty spring animation exists for conditional start, and to drive neighboring springs.
-        val neighborsToSettle =
-            SpringAnimation(FloatValueHolder()).setSpring(createExpressiveDismissSpringForce())
-        var lastPosition = 0f
-        var startSettling = false
-        draggedTaskViewSpringAnimation.addUpdateListener { _, value, velocity ->
-            // Start the settling animation the first time the dragged task passes the origin (from
-            // negative displacement to positive displacement). We do not check for an exact value
-            // to compare to, as the update listener does not necessarily hit every value (e.g. a
-            // value of zero). Do not check again once it has started settling, as a spring can
-            // bounce past the origin multiple times depending on the stiffness and damping ratio.
-            if (startSettling) return@addUpdateListener
-            if (lastPosition < 0 && value >= 0) {
-                startSettling = true
-            }
-            lastPosition = value
-            if (startSettling) {
-                neighborsToSettle.setStartVelocity(velocity).animateToFinalPosition(0f)
-                playDismissSettlingHaptic(velocity)
-            }
-        }
-
-        // Add tasks before dragged index, fanning out from the dragged task.
-        // The order they are added matters, as each spring drives the next.
-        var previousNeighbor = neighborsToSettle
-        getTasksOffsetPairAdjacentToDraggedTask(draggedTaskView, towardsStart = true).forEach {
-            (taskView, offset) ->
-            previousNeighbor =
-                createNeighboringTaskViewSpringAnimation(
-                    taskView,
-                    offset * ADDITIONAL_DISMISS_DAMPING_RATIO,
-                    previousNeighbor,
-                )
-        }
-        // Add tasks after dragged index, fanning out from the dragged task.
-        // The order they are added matters, as each spring drives the next.
-        previousNeighbor = neighborsToSettle
-        getTasksOffsetPairAdjacentToDraggedTask(draggedTaskView, towardsStart = false).forEach {
-            (taskView, offset) ->
-            previousNeighbor =
-                createNeighboringTaskViewSpringAnimation(
-                    taskView,
-                    offset * ADDITIONAL_DISMISS_DAMPING_RATIO,
-                    previousNeighbor,
-                )
-        }
-    }
-
-    /**
-     * Gets pairs of (TaskView, offset) adjacent the dragged task in visual order.
-     *
-     * <p>Gets tasks either before or after the dragged task along with their offset from it. The
-     * offset is the distance between indices for carousels, or distance between columns for grids.
-     */
-    private fun getTasksOffsetPairAdjacentToDraggedTask(
-        draggedTaskView: TaskView,
-        towardsStart: Boolean,
-    ): Sequence<Pair<TaskView, Int>> {
-        if (recentsView.showAsGrid()) {
-            val taskGridNavHelper =
-                TaskGridNavHelper(
-                    recentsView.topRowIdArray,
-                    recentsView.bottomRowIdArray,
-                    getLargeTaskViewIds(),
-                    hasAddDesktopButton = false,
-                )
-            return taskGridNavHelper
-                .gridTaskViewIdOffsetPairInTabOrderSequence(
-                    draggedTaskView.taskViewId,
-                    towardsStart,
-                )
-                .mapNotNull { (taskViewId, columnOffset) ->
-                    recentsView.getTaskViewFromTaskViewId(taskViewId)?.let { taskView ->
-                        Pair(taskView, columnOffset)
-                    }
-                }
-        } else {
-            val taskViewList = taskViews.toList()
-            val draggedTaskViewIndex = taskViewList.indexOf(draggedTaskView)
-
-            return if (towardsStart) {
-                taskViewList
-                    .take(draggedTaskViewIndex)
-                    .reversed()
-                    .mapIndexed { index, taskView -> Pair(taskView, index + 1) }
-                    .asSequence()
-            } else {
-                taskViewList
-                    .takeLast(taskViewList.size - draggedTaskViewIndex - 1)
-                    .mapIndexed { index, taskView -> Pair(taskView, index + 1) }
-                    .asSequence()
-            }
-        }
-    }
-
-    /** Creates a neighboring task view spring, driven by the spring of its neighbor. */
-    private fun createNeighboringTaskViewSpringAnimation(
-        taskView: TaskView,
-        dampingOffsetRatio: Float,
-        previousNeighborSpringAnimation: SpringAnimation,
-    ): SpringAnimation {
-        val neighboringTaskViewSpringAnimation =
-            SpringAnimation(
-                    taskView,
-                    FloatPropertyCompat.createFloatPropertyCompat(
-                        taskView.secondaryDismissTranslationProperty
-                    ),
-                )
-                .setSpring(createExpressiveDismissSpringForce(dampingOffsetRatio))
-        // Update live tile on spring animation.
-        if (taskView.isRunningTask && recentsView.enableDrawingLiveTile) {
-            neighboringTaskViewSpringAnimation.addUpdateListener { _, _, _ ->
-                recentsView.runActionOnRemoteHandles { remoteTargetHandle ->
-                    remoteTargetHandle.taskViewSimulator.taskSecondaryTranslation.value =
-                        taskView.secondaryDismissTranslationProperty.get(taskView)
-                }
-                recentsView.redrawLiveTile()
-            }
-        }
-        // Drive current neighbor's spring with the previous neighbor's.
-        previousNeighborSpringAnimation.addUpdateListener { _, value, _ ->
-            neighboringTaskViewSpringAnimation.animateToFinalPosition(value)
-        }
-        return neighboringTaskViewSpringAnimation
-    }
-
-    private fun createExpressiveDismissSpringForce(dampingRatioOffset: Float = 0f): SpringForce {
-        val resourceProvider = DynamicResource.provider(recentsView.mContainer)
-        return SpringForce()
-            .setDampingRatio(
-                resourceProvider.getFloat(R.dimen.expressive_dismiss_task_trans_y_damping_ratio) +
-                    dampingRatioOffset
-            )
-            .setStiffness(
-                resourceProvider.getFloat(R.dimen.expressive_dismiss_task_trans_y_stiffness)
-            )
-    }
-
-    /**
-     * Plays a haptic as the dragged task view settles back into its rest state.
-     *
-     * <p>Haptic intensity is proportional to velocity.
-     */
-    private fun playDismissSettlingHaptic(velocity: Float) {
-        val maxDismissSettlingVelocity =
-            recentsView.pagedOrientationHandler.getSecondaryDimension(recentsView)
-        MSDLPlayerWrapper.INSTANCE.get(recentsView.context)
-            .playToken(
-                MSDLToken.CANCEL,
-                InteractionProperties.DynamicVibrationScale(
-                    boundToRange(velocity / maxDismissSettlingVelocity, 0f, 1f),
-                    VibrationAttributes.Builder()
-                        .setUsage(VibrationAttributes.USAGE_TOUCH)
-                        .setFlags(VibrationAttributes.FLAG_PIPELINED_EFFECT)
-                        .build(),
-                ),
-            )
-    }
-
     companion object {
         val TEMP_RECT = Rect()
-
-        // The additional damping to apply to tasks further from the dismissed task.
-        const val ADDITIONAL_DISMISS_DAMPING_RATIO = 0.15f
     }
 }
diff --git a/quickstep/src/com/android/quickstep/views/TaskContainer.kt b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
index ce20bf9..7301cfc 100644
--- a/quickstep/src/com/android/quickstep/views/TaskContainer.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
@@ -25,8 +25,6 @@
 import com.android.launcher3.util.TransformingTouchDelegate
 import com.android.quickstep.TaskOverlayFactory
 import com.android.quickstep.ViewUtils.addAccessibleChildToList
-import com.android.quickstep.recents.di.RecentsDependencies
-import com.android.quickstep.recents.di.getScope
 import com.android.quickstep.recents.ui.mapper.TaskUiStateMapper
 import com.android.quickstep.recents.ui.viewmodel.TaskData
 import com.android.quickstep.task.thumbnail.TaskThumbnailView
@@ -57,13 +55,6 @@
     init {
         if (enableRefactorTaskThumbnail()) {
             require(snapshotView is TaskThumbnailView)
-            RecentsDependencies.getScope(snapshotView).apply {
-                val taskViewScope = RecentsDependencies.getScope(taskView)
-                linkTo(taskViewScope)
-
-                val taskContainerScope = RecentsDependencies.getScope(this@TaskContainer)
-                linkTo(taskContainerScope)
-            }
         } else {
             require(snapshotView is TaskThumbnailViewDeprecated)
         }
@@ -116,8 +107,6 @@
         snapshotView.scaleY = 1f
         overlay.destroy()
         if (enableRefactorTaskThumbnail()) {
-            RecentsDependencies.getInstance().removeScope(snapshotView)
-            RecentsDependencies.getInstance().removeScope(this)
             isThumbnailValid = false
         } else {
             thumbnailViewDeprecated.setShowSplashForSplitSelection(false)
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt
index 46b5659..a456fb9 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt
@@ -60,6 +60,7 @@
 import org.mockito.kotlin.atLeastOnce
 import org.mockito.kotlin.eq
 import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
 import org.mockito.kotlin.verify
 import org.mockito.kotlin.whenever
 
@@ -573,6 +574,71 @@
     }
 
     @Test
+    fun animateToInitialState_whileDragging_inApp() {
+        setUpBubbleBar()
+        setUpBubbleStashController()
+        whenever(bubbleStashController.bubbleBarTranslationY)
+            .thenReturn(BAR_TRANSLATION_Y_FOR_TASKBAR)
+
+        val handle = View(context)
+        val handleAnimator = PhysicsAnimator.getInstance(handle)
+        whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
+
+        val barAnimator = PhysicsAnimator.getInstance(bubbleBarView)
+
+        var notifiedBubbleBarVisible = false
+        val onBubbleBarVisible = Runnable { notifiedBubbleBarVisible = true }
+        val animator =
+            BubbleBarViewAnimator(
+                bubbleBarView,
+                bubbleStashController,
+                flyoutController,
+                bubbleBarParentViewController,
+                onExpanded = emptyRunnable,
+                onBubbleBarVisible = onBubbleBarVisible,
+                animatorScheduler,
+            )
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            bubbleBarView.visibility = INVISIBLE
+            animator.animateToInitialState(
+                bubble,
+                isInApp = true,
+                isExpanding = false,
+                isDragging = true,
+            )
+        }
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        barAnimator.assertIsNotRunning()
+        assertThat(animator.isAnimating).isTrue()
+        assertThat(bubbleBarView.alpha).isEqualTo(1)
+        assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
+        assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1)
+        waitForFlyoutToShow()
+
+        assertThat(animatorScheduler.delayedBlock).isNotNull()
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
+
+        waitForFlyoutToHide()
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+        assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(2)
+        assertThat(animator.isAnimating).isFalse()
+        assertThat(bubbleBarView.alpha).isEqualTo(1)
+        assertThat(handle.translationY).isEqualTo(0)
+        assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE)
+        assertThat(notifiedBubbleBarVisible).isTrue()
+
+        verify(bubbleStashController, never()).stashBubbleBarImmediate()
+    }
+
+    @Test
     fun animateToInitialState_inApp_autoExpanding() {
         setUpBubbleBar()
         setUpBubbleStashController()
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailUseCaseTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailUseCaseTest.kt
deleted file mode 100644
index 90ef61d..0000000
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailUseCaseTest.kt
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * 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.recents.usecase
-
-import android.content.ComponentName
-import android.content.Intent
-import android.graphics.Bitmap
-import android.graphics.Color
-import com.android.quickstep.recents.data.FakeTasksRepository
-import com.android.systemui.shared.recents.model.Task
-import com.android.systemui.shared.recents.model.ThumbnailData
-import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-import org.mockito.kotlin.mock
-import org.mockito.kotlin.whenever
-
-/** Test for [GetThumbnailUseCase] */
-class GetThumbnailUseCaseTest {
-    private val task =
-        Task(Task.TaskKey(TASK_ID, 0, Intent(), ComponentName("", ""), 0, 2000)).apply {
-            colorBackground = Color.BLACK
-        }
-    private val thumbnailData =
-        ThumbnailData(
-            thumbnail =
-                mock<Bitmap>().apply {
-                    whenever(width).thenReturn(THUMBNAIL_WIDTH)
-                    whenever(height).thenReturn(THUMBNAIL_HEIGHT)
-                }
-        )
-
-    private val tasksRepository = FakeTasksRepository()
-    private val systemUnderTest = GetThumbnailUseCase(tasksRepository)
-
-    @Test
-    fun taskNotSeeded_returnsNull() {
-        assertThat(systemUnderTest.run(TASK_ID)).isNull()
-    }
-
-    @Test
-    fun taskNotLoaded_returnsNull() {
-        tasksRepository.seedTasks(listOf(task))
-
-        assertThat(systemUnderTest.run(TASK_ID)).isNull()
-    }
-
-    @Test
-    fun taskNotVisible_returnsNull() {
-        tasksRepository.seedTasks(listOf(task))
-        tasksRepository.seedThumbnailData(mapOf(TASK_ID to thumbnailData))
-
-        assertThat(systemUnderTest.run(TASK_ID)).isNull()
-    }
-
-    @Test
-    fun taskVisible_returnsThumbnail() {
-        tasksRepository.seedTasks(listOf(task))
-        tasksRepository.seedThumbnailData(mapOf(TASK_ID to thumbnailData))
-        tasksRepository.setVisibleTasks(setOf(TASK_ID))
-
-        assertThat(systemUnderTest.run(TASK_ID)).isEqualTo(thumbnailData.thumbnail)
-    }
-
-    private companion object {
-        const val TASK_ID = 0
-        const val THUMBNAIL_WIDTH = 100
-        const val THUMBNAIL_HEIGHT = 200
-    }
-}
diff --git a/quickstep/tests/src/com/android/launcher3/statehandlers/DesktopVisibilityControllerTest.kt b/quickstep/tests/src/com/android/launcher3/statehandlers/DesktopVisibilityControllerTest.kt
new file mode 100644
index 0000000..4b8f2a2
--- /dev/null
+++ b/quickstep/tests/src/com/android/launcher3/statehandlers/DesktopVisibilityControllerTest.kt
@@ -0,0 +1,80 @@
+/*
+ * 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.statehandlers
+
+import android.content.Context
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
+import com.android.launcher3.util.DaggerSingletonTracker
+import com.android.quickstep.SystemUiProxy
+import com.android.window.flags.Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND
+import com.android.window.flags.Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_FRONTEND
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
+import org.junit.After
+import org.junit.Assert.assertFalse
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+import org.mockito.quality.Strictness
+
+/**
+ * Tests the behavior of [DesktopVisibilityController] in regards to multiple desktops and multiple
+ * displays.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class DesktopVisibilityControllerTest {
+
+    @get:Rule val setFlagsRule = SetFlagsRule()
+
+    private val mockitoSession =
+        mockitoSession()
+            .strictness(Strictness.LENIENT)
+            .mockStatic(DesktopModeStatus::class.java)
+            .startMocking()
+
+    private val context = mock<Context>()
+    private val systemUiProxy = mock<SystemUiProxy>()
+    private val lifeCycleTracker = mock<DaggerSingletonTracker>()
+    private lateinit var desktopVisibilityController: DesktopVisibilityController
+
+    @Before
+    fun setUp() {
+        whenever(context.resources).thenReturn(mock())
+        whenever(DesktopModeStatus.enableMultipleDesktops(context)).thenReturn(true)
+        desktopVisibilityController =
+            DesktopVisibilityController(context, systemUiProxy, lifeCycleTracker)
+    }
+
+    @After
+    fun tearDown() {
+        mockitoSession.finishMocking()
+    }
+
+    @Test
+    @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, FLAG_ENABLE_MULTIPLE_DESKTOPS_FRONTEND)
+    fun noCrashWhenCheckingNonExistentDisplay() {
+        assertFalse(desktopVisibilityController.isInDesktopMode(displayId = 500))
+        assertFalse(desktopVisibilityController.isInDesktopModeAndNotInOverview(displayId = 300))
+    }
+}
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
index 79d3c19..aaaa0e4 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
@@ -62,6 +62,7 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.Comparator;
 import java.util.function.Consumer;
 import java.util.function.Function;
 
@@ -557,6 +558,27 @@
                 numTasks == null ? 0 : numTasks, recentsView.getTaskViewCount()));
     }
 
+    @Test
+    @PortraitLandscape
+    public void testDismissBottomRow() throws Exception {
+        assumeTrue(mLauncher.isTablet());
+        mLauncher.goHome().switchToOverview().dismissAllTasks();
+        startTestAppsWithCheck();
+        Overview overview = mLauncher.goHome().switchToOverview();
+        assertIsInState("Launcher internal state didn't switch to Overview",
+                ExpectedState.OVERVIEW);
+        final Integer numTasks = getFromRecentsView(RecentsView::getTaskViewCount);
+        OverviewTask bottomTask = overview.getCurrentTasksForTablet().stream().max(
+                Comparator.comparingInt(OverviewTask::getTaskCenterY)).get();
+        assertNotNull("bottomTask null", bottomTask);
+
+        bottomTask.dismiss();
+
+        runOnRecentsView(recentsView -> assertEquals(
+                "Dismissing a bottomTask didn't remove 1 bottomTask from Overview",
+                numTasks - 1, recentsView.getTaskViewCount()));
+    }
+
     private void startTestAppsWithCheck() throws Exception {
         startTestApps();
         expectLaunchedAppState();
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
+            }
+    }
+}
diff --git a/res/values-lt/strings.xml b/res/values-lt/strings.xml
index bd792ac..1a4a3f2 100644
--- a/res/values-lt/strings.xml
+++ b/res/values-lt/strings.xml
@@ -28,7 +28,7 @@
     <string name="shortcut_not_available" msgid="2536503539825726397">"Sparčiojo klavišo negalima naudoti"</string>
     <string name="home_screen" msgid="5629429142036709174">"Pagrindinis"</string>
     <string name="set_default_home_app" msgid="5808906607627586381">"Nustatykite „<xliff:g id="LAUNCHER_NAME">%1$s</xliff:g>“ kaip numatytąją pagrindinę programą skiltyje „Nustatymai“"</string>
-    <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Išskaidyto ekrano režimas"</string>
+    <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Išskaidytas ekranas"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Programos „%1$s“ informacija"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"„%1$s“ naudojimo nustatymai"</string>
     <string name="new_window_option_taskbar" msgid="6448780542727767211">"Naujas langas"</string>
diff --git a/res/values-or/strings.xml b/res/values-or/strings.xml
index 3031259..46aadc0 100644
--- a/res/values-or/strings.xml
+++ b/res/values-or/strings.xml
@@ -63,7 +63,7 @@
     <string name="widgets_full_sheet_search_bar_hint" msgid="8484659090860596457">"ସନ୍ଧାନ କରନ୍ତୁ"</string>
     <string name="widgets_full_sheet_cancel_button_description" msgid="5766167035728653605">"ସନ୍ଧାନ ବାକ୍ସରୁ ଟେକ୍ସଟ୍ ଖାଲି କରନ୍ତୁ"</string>
     <string name="no_widgets_available" msgid="4337693382501046170">"ୱିଜେଟ୍ ଏବଂ ସର୍ଟକଟଗୁଡ଼ିକ ଉପଲବ୍ଧ ନାହିଁ"</string>
-    <string name="no_search_results" msgid="3787956167293097509">"କୌଣସି ୱିଜେଟ୍ କିମ୍ବା ସର୍ଟକଟ୍ ମିଳିଲା ନାହିଁ"</string>
+    <string name="no_search_results" msgid="3787956167293097509">"କୌଣସି ୱିଜେଟ କିମ୍ବା ସର୍ଟକଟ ମିଳିଲା ନାହିଁ"</string>
     <string name="widgets_full_sheet_personal_tab" msgid="2743540105607120182">"ବ୍ୟକ୍ତିଗତ"</string>
     <string name="widgets_full_sheet_work_tab" msgid="3767150027110633765">"ୱାର୍କ"</string>
     <string name="widget_category_conversations" msgid="8894438636213590446">"ବାର୍ତ୍ତାଳାପଗୁଡ଼ିକ"</string>
diff --git a/res/values-pa/strings.xml b/res/values-pa/strings.xml
index c15e2e9..17ecb78 100644
--- a/res/values-pa/strings.xml
+++ b/res/values-pa/strings.xml
@@ -94,7 +94,7 @@
     <string name="all_apps_button_work_label" msgid="7270707118948892488">"ਕਾਰਜ-ਸਥਾਨ ਸੰਬੰਧੀ ਐਪਾਂ ਦੀ ਸੂਚੀ"</string>
     <string name="remove_drop_target_label" msgid="7812859488053230776">"ਹਟਾਓ"</string>
     <string name="uninstall_drop_target_label" msgid="4722034217958379417">"ਅਣਸਥਾਪਤ ਕਰੋ"</string>
-    <string name="app_info_drop_target_label" msgid="692894985365717661">"ਐਪ ਜਾਣਕਾਰੀ"</string>
+    <string name="app_info_drop_target_label" msgid="692894985365717661">"ਐਪ ਸੰਬੰਧੀ ਜਾਣਕਾਰੀ"</string>
     <string name="install_private_system_shortcut_label" msgid="1616889277073184841">"ਪ੍ਰਾਈਵੇਟ ਵਜੋਂ ਸਥਾਪਤ ਕਰੋ"</string>
     <string name="uninstall_private_system_shortcut_label" msgid="8423460530441627982">"ਐਪ ਅਣਸਥਾਪਤ ਕਰੋ"</string>
     <string name="install_drop_target_label" msgid="2539096853673231757">"ਸਥਾਪਤ ਕਰੋ"</string>
diff --git a/res/values-te/strings.xml b/res/values-te/strings.xml
index b6563b0..4145fb6 100644
--- a/res/values-te/strings.xml
+++ b/res/values-te/strings.xml
@@ -46,14 +46,14 @@
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d వెడల్పు X %2$d ఎత్తు"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> విడ్జెట్"</string>
     <string name="widget_preview_name_and_dims_content_description" msgid="8489038126122831595">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> విడ్జెట్, %2$d వెడల్పు %3$d ఎత్తు ఉండాలి"</string>
-    <string name="add_item_request_drag_hint" msgid="8730547755622776606">"విడ్జెట్‌ను మొదటి స్క్రీన్‌లో తిప్పడానికి దాన్ని తాకి, &amp; నొక్కి పట్టుకోండి"</string>
+    <string name="add_item_request_drag_hint" msgid="8730547755622776606">"విడ్జెట్‌ను మొదటి స్క్రీన్‌లో అటు, ఇటు కదపడానికి దాన్ని తాకి, నొక్కి పట్టుకోండి"</string>
     <string name="add_to_home_screen" msgid="9168649446635919791">"మొదటి స్క్రీన్‌కు జోడించండి"</string>
     <string name="added_to_home_screen_accessibility_text" msgid="4451545765448884415">"మొదటి స్క్రీన్‌కు <xliff:g id="WIDGET_NAME">%1$s</xliff:g> విడ్జెట్ జోడించబడింది"</string>
     <string name="suggested_widgets_header_title" msgid="1844314680798145222">"సూచనలు"</string>
     <string name="productivity_widget_recommendation_category_label" msgid="3811812719618323750">"నిత్యావసరాలు"</string>
     <string name="news_widget_recommendation_category_label" msgid="6756167867113741310">"వార్తలు &amp; మ్యాగజైన్లు"</string>
     <string name="entertainment_widget_recommendation_category_label" msgid="3973107268630717874">"వినోదం"</string>
-    <string name="social_widget_recommendation_category_label" msgid="689147679536384717">"సామాజికం"</string>
+    <string name="social_widget_recommendation_category_label" msgid="689147679536384717">"సోషల్ మీడియా"</string>
     <string name="others_widget_recommendation_category_label" msgid="5555987036267226245">"మీ కోసం సూచించినవి"</string>
     <string name="widget_picker_right_pane_accessibility_title" msgid="1673313931455067502">"కుడి వైపున <xliff:g id="SELECTED_HEADER">%1$s</xliff:g> విడ్జెట్‌లు, ఎడమ వైపున సెర్చ్, ఇతర ఆప్షన్‌లు"</string>
     <string name="widgets_count" msgid="6467746476364652096">"{count,plural, =1{# విడ్జెట్}other{# విడ్జెట్‌లు}}"</string>
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index e6b3457..03dd943 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -75,8 +75,8 @@
 import com.android.launcher3.dragndrop.DragOptions.PreDragCondition;
 import com.android.launcher3.dragndrop.DraggableView;
 import com.android.launcher3.folder.FolderIcon;
-import com.android.launcher3.graphics.IconShape;
 import com.android.launcher3.graphics.PreloadIconDrawable;
+import com.android.launcher3.graphics.ShapeDelegate;
 import com.android.launcher3.icons.DotRenderer;
 import com.android.launcher3.icons.FastBitmapDrawable;
 import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver;
@@ -724,7 +724,7 @@
         if (!mForceHideDot && (hasDot() || mDotParams.scale > 0)) {
             getIconBounds(mDotParams.iconBounds);
             Utilities.scaleRectAboutCenter(mDotParams.iconBounds,
-                    IconShape.INSTANCE.get(getContext()).getNormalizationScale());
+                    ShapeDelegate.getNormalizationScale());
             final int scrollX = getScrollX();
             final int scrollY = getScrollY();
             canvas.translate(scrollX, scrollY);
@@ -775,7 +775,7 @@
         getIconBounds(mRunningAppIconBounds);
         Utilities.scaleRectAboutCenter(
                 mRunningAppIconBounds,
-                IconShape.INSTANCE.get(getContext()).getNormalizationScale());
+                ShapeDelegate.getNormalizationScale());
 
         final boolean isMinimized = mRunningAppState == RunningAppState.MINIMIZED;
         final int indicatorTop = mRunningAppIconBounds.bottom + mRunningAppIndicatorTopMargin;
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index 3b283c3..4561506 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -31,6 +31,7 @@
 import static com.android.launcher3.testing.shared.ResourceUtils.roundPxValueFromFloat;
 import static com.android.wm.shell.Flags.enableBubbleBar;
 import static com.android.wm.shell.Flags.enableTinyTaskbar;
+import static com.android.wm.shell.Flags.enableBubbleBarOnPhones;
 
 import android.annotation.SuppressLint;
 import android.content.Context;
@@ -52,7 +53,7 @@
 import com.android.launcher3.CellLayout.ContainerType;
 import com.android.launcher3.DevicePaddings.DevicePadding;
 import com.android.launcher3.folder.ClippedFolderIconLayoutRule;
-import com.android.launcher3.graphics.IconShape;
+import com.android.launcher3.graphics.ThemeManager;
 import com.android.launcher3.icons.DotRenderer;
 import com.android.launcher3.icons.IconNormalizer;
 import com.android.launcher3.model.data.ItemInfo;
@@ -388,7 +389,7 @@
 
     /** TODO: Once we fully migrate to staged split, remove "isMultiWindowMode" */
     DeviceProfile(Context context, InvariantDeviceProfile inv, Info info,
-            WindowManagerProxy wmProxy, IconShape iconShape, WindowBounds windowBounds,
+            WindowManagerProxy wmProxy, ThemeManager themeManager, WindowBounds windowBounds,
             SparseArray<DotRenderer> dotRendererCache, boolean isMultiWindowMode,
             boolean transposeLayoutWithOrientation, boolean isMultiDisplay, boolean isGestureMode,
             @NonNull final ViewScaleProvider viewScaleProvider,
@@ -420,7 +421,9 @@
         isTablet = info.isTablet(windowBounds);
         isPhone = !isTablet;
         isTwoPanels = isTablet && isMultiDisplay;
-        isTaskbarPresent = (isTablet || (enableTinyTaskbar() && isGestureMode))
+        boolean taskbarOrBubbleBarOnPhones = enableTinyTaskbar()
+                || (enableBubbleBar() && enableBubbleBarOnPhones());
+        isTaskbarPresent = (isTablet || (taskbarOrBubbleBarOnPhones && isGestureMode))
                 && wmProxy.isTaskbarDrawnInProcess();
 
         // Some more constants.
@@ -846,8 +849,8 @@
         dimensionOverrideProvider.accept(this);
 
         // This is done last, after iconSizePx is calculated above.
-        mDotRendererWorkSpace = createDotRenderer(iconShape, iconSizePx, dotRendererCache);
-        mDotRendererAllApps = createDotRenderer(iconShape, allAppsIconSizePx, dotRendererCache);
+        mDotRendererWorkSpace = createDotRenderer(themeManager, iconSizePx, dotRendererCache);
+        mDotRendererAllApps = createDotRenderer(themeManager, allAppsIconSizePx, dotRendererCache);
     }
 
     /**
@@ -869,12 +872,12 @@
     }
 
     private static DotRenderer createDotRenderer(
-            @NonNull IconShape iconShape, int size, @NonNull SparseArray<DotRenderer> cache) {
+            @NonNull ThemeManager themeManager, int size, @NonNull SparseArray<DotRenderer> cache) {
         DotRenderer renderer = cache.get(size);
         if (renderer == null) {
             renderer = new DotRenderer(
                     size,
-                    iconShape.getShape().getPath(DEFAULT_DOT_SIZE),
+                    themeManager.getIconShape().getPath(DEFAULT_DOT_SIZE),
                     DEFAULT_DOT_SIZE);
             cache.put(size, renderer);
         }
@@ -2478,7 +2481,7 @@
         private final InvariantDeviceProfile mInv;
         private final Info mInfo;
         private final WindowManagerProxy mWMProxy;
-        private final IconShape mIconShape;
+        private final ThemeManager mThemeManager;
 
         private WindowBounds mWindowBounds;
         private boolean mIsMultiDisplay;
@@ -2495,12 +2498,12 @@
         private boolean mIsTransientTaskbar;
 
         public Builder(Context context, InvariantDeviceProfile inv, Info info,
-                WindowManagerProxy wmProxy, IconShape iconShape) {
+                WindowManagerProxy wmProxy, ThemeManager themeManager) {
             mContext = context;
             mInv = inv;
             mInfo = info;
             mWMProxy = wmProxy;
-            mIconShape = iconShape;
+            mThemeManager = themeManager;
             mIsTransientTaskbar = info.isTransientTaskbar();
         }
 
@@ -2581,7 +2584,7 @@
             if (mOverrideProvider == null) {
                 mOverrideProvider = DEFAULT_DIMENSION_PROVIDER;
             }
-            return new DeviceProfile(mContext, mInv, mInfo, mWMProxy, mIconShape,
+            return new DeviceProfile(mContext, mInv, mInfo, mWMProxy, mThemeManager,
                     mWindowBounds, mDotRendererCache,
                     mIsMultiWindowMode, mTransposeLayoutWithOrientation, mIsMultiDisplay,
                     mIsGestureMode, mViewScaleProvider, mOverrideProvider, mIsTransientTaskbar);
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index 4107c8f..870e6d6 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -57,7 +57,7 @@
 import com.android.launcher3.dagger.ApplicationContext;
 import com.android.launcher3.dagger.LauncherAppComponent;
 import com.android.launcher3.dagger.LauncherAppSingleton;
-import com.android.launcher3.graphics.IconShape;
+import com.android.launcher3.graphics.ThemeManager;
 import com.android.launcher3.icons.DotRenderer;
 import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.model.DeviceGridState;
@@ -85,6 +85,7 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
+import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.stream.Collectors;
 
 import javax.inject.Inject;
@@ -136,7 +137,7 @@
     private final DisplayController mDisplayController;
     private final WindowManagerProxy mWMProxy;
     private final LauncherPrefs mPrefs;
-    private final IconShape mIconShape;
+    private final ThemeManager mThemeManager;
 
     /**
      * Number of icons per row and column in the workspace.
@@ -252,7 +253,7 @@
 
     public Point defaultWallpaperSize;
 
-    private final ArrayList<OnIDPChangeListener> mChangeListeners = new ArrayList<>();
+    private final List<OnIDPChangeListener> mChangeListeners = new CopyOnWriteArrayList<>();
 
     @Inject
     InvariantDeviceProfile(
@@ -260,12 +261,12 @@
             LauncherPrefs prefs,
             DisplayController dc,
             WindowManagerProxy wmProxy,
-            IconShape iconShape,
+            ThemeManager themeManager,
             DaggerSingletonTracker lifeCycle) {
         mDisplayController = dc;
         mWMProxy = wmProxy;
         mPrefs = prefs;
-        mIconShape = iconShape;
+        mThemeManager = themeManager;
 
         String gridName = prefs.get(GRID_NAME);
         initGrid(context, gridName);
@@ -490,7 +491,7 @@
     }
 
     DeviceProfile.Builder newDPBuilder(Context context, Info info) {
-        return new DeviceProfile.Builder(context, this, info, mWMProxy, mIconShape);
+        return new DeviceProfile.Builder(context, this, info, mWMProxy, mThemeManager);
     }
 
     public void addOnChangeListener(OnIDPChangeListener listener) {
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 38e2806..3edba99 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -183,8 +183,8 @@
 import com.android.launcher3.celllayout.CellPosMapper.TwoPanelCellPosMapper;
 import com.android.launcher3.compat.AccessibilityManagerCompat;
 import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.debug.TestEvent;
 import com.android.launcher3.debug.TestEventEmitter;
+import com.android.launcher3.debug.TestEventEmitter.TestEvent;
 import com.android.launcher3.dot.DotInfo;
 import com.android.launcher3.dragndrop.DragController;
 import com.android.launcher3.dragndrop.DragLayer;
@@ -611,7 +611,7 @@
             RuleController.getInstance(this).setRules(
                     RuleController.parseRules(this, R.xml.split_configuration));
         }
-        TestEventEmitter.INSTANCE.get(this).sendEvent(TestEvent.LAUNCHER_ON_CREATE);
+        TestEventEmitter.sendEvent(TestEvent.LAUNCHER_ON_CREATE);
     }
 
     protected ModelCallbacks createModelCallbacks() {
diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java
index 71013c3..bf2ad92 100644
--- a/src/com/android/launcher3/LauncherAppState.java
+++ b/src/com/android/launcher3/LauncherAppState.java
@@ -16,54 +16,32 @@
 
 package com.android.launcher3;
 
-import static android.app.admin.DevicePolicyManager.ACTION_DEVICE_POLICY_RESOURCE_UPDATED;
-import static android.content.Context.RECEIVER_EXPORTED;
-
-import static com.android.launcher3.Flags.enableSmartspaceRemovalToggle;
-import static com.android.launcher3.model.LoaderTask.SMARTSPACE_ON_HOME_SCREEN;
-import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
-import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
-import static com.android.launcher3.util.SettingsCache.NOTIFICATION_BADGING_URI;
-import static com.android.launcher3.util.SettingsCache.PRIVATE_SPACE_HIDE_WHEN_LOCKED_URI;
-
-import android.content.ComponentName;
 import android.content.Context;
-import android.content.SharedPreferences;
-import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
-import android.content.pm.LauncherApps;
-import android.content.pm.LauncherApps.ArchiveCompatibilityParams;
 import android.util.Log;
 
 import androidx.annotation.Nullable;
-import androidx.core.os.BuildCompat;
 
+import com.android.launcher3.dagger.LauncherComponentProvider;
 import com.android.launcher3.graphics.ThemeManager;
-import com.android.launcher3.graphics.ThemeManager.ThemeChangeListener;
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.icons.IconProvider;
 import com.android.launcher3.icons.LauncherIconProvider;
-import com.android.launcher3.icons.LauncherIcons;
-import com.android.launcher3.model.ModelLauncherCallbacks;
+import com.android.launcher3.model.ModelInitializer;
 import com.android.launcher3.model.WidgetsFilterDataProvider;
-import com.android.launcher3.notification.NotificationListener;
 import com.android.launcher3.pm.InstallSessionHelper;
-import com.android.launcher3.pm.InstallSessionTracker;
 import com.android.launcher3.pm.UserCache;
-import com.android.launcher3.util.LockedUserState;
 import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.util.Preconditions;
 import com.android.launcher3.util.RunnableList;
 import com.android.launcher3.util.SafeCloseable;
 import com.android.launcher3.util.SettingsCache;
-import com.android.launcher3.util.SimpleBroadcastReceiver;
 import com.android.launcher3.util.TraceHelper;
 import com.android.launcher3.widget.custom.CustomWidgetManager;
 
 public class LauncherAppState implements SafeCloseable {
 
     public static final String TAG = "LauncherAppState";
-    public static final String ACTION_FORCE_ROLOAD = "force-reload-launcher";
 
     // We do not need any synchronization for this variable as its only written on UI thread.
     public static final MainThreadInitializedObject<LauncherAppState> INSTANCE =
@@ -94,82 +72,21 @@
 
         mIsSafeModeEnabled = TraceHelper.allowIpcs("isSafeMode",
                 () -> context.getPackageManager().isSafeMode());
-        mInvariantDeviceProfile.addOnChangeListener(modelPropertiesChanged -> {
-            if (modelPropertiesChanged) {
-                refreshAndReloadLauncher();
-            }
-        });
 
-        ThemeChangeListener themeChangeListener = this::refreshAndReloadLauncher;
-        ThemeManager.INSTANCE.get(context).addChangeListener(themeChangeListener);
-        mOnTerminateCallback.add(() ->
-                ThemeManager.INSTANCE.get(context).removeChangeListener(themeChangeListener));
-
-        ModelLauncherCallbacks callbacks = mModel.newModelCallbacks();
-        LauncherApps launcherApps = mContext.getSystemService(LauncherApps.class);
-        launcherApps.registerCallback(callbacks);
-        mOnTerminateCallback.add(() ->
-                mContext.getSystemService(LauncherApps.class).unregisterCallback(callbacks));
-
-        if (BuildCompat.isAtLeastV() && Flags.enableSupportForArchiving()) {
-            ArchiveCompatibilityParams params = new ArchiveCompatibilityParams();
-            params.setEnableUnarchivalConfirmation(false);
-            params.setEnableIconOverlay(!Flags.useNewIconForArchivedApps());
-            launcherApps.setArchiveCompatibility(params);
-        }
-
-        SimpleBroadcastReceiver modelChangeReceiver =
-                new SimpleBroadcastReceiver(context, UI_HELPER_EXECUTOR, mModel::onBroadcastIntent);
-        modelChangeReceiver.register(
-                ACTION_DEVICE_POLICY_RESOURCE_UPDATED);
-        if (BuildConfig.IS_STUDIO_BUILD) {
-            modelChangeReceiver.register(RECEIVER_EXPORTED, ACTION_FORCE_ROLOAD);
-        }
-        mOnTerminateCallback.add(() -> modelChangeReceiver.unregisterReceiverSafely());
-
-        SafeCloseable userChangeListener = UserCache.INSTANCE.get(mContext)
-                .addUserEventListener(mModel::onUserEvent);
-        mOnTerminateCallback.add(userChangeListener::close);
-
-        if (enableSmartspaceRemovalToggle()) {
-            OnSharedPreferenceChangeListener firstPagePinnedItemListener =
-                    new OnSharedPreferenceChangeListener() {
-                        @Override
-                        public void onSharedPreferenceChanged(
-                                SharedPreferences sharedPreferences, String key) {
-                            if (SMARTSPACE_ON_HOME_SCREEN.equals(key)) {
-                                mModel.forceReload();
-                            }
-                        }
-                    };
-            LauncherPrefs.getPrefs(mContext).registerOnSharedPreferenceChangeListener(
-                    firstPagePinnedItemListener);
-            mOnTerminateCallback.add(() -> LauncherPrefs.getPrefs(mContext)
-                    .unregisterOnSharedPreferenceChangeListener(firstPagePinnedItemListener));
-        }
-
-        LockedUserState.get(context).runOnUserUnlocked(() -> {
-            CustomWidgetManager cwm = CustomWidgetManager.INSTANCE.get(mContext);
-            mOnTerminateCallback.add(cwm.addWidgetRefreshCallback(mModel::rebindCallbacks)::close);
-
-            SafeCloseable iconChangeTracker = mIconProvider.registerIconChangeListener(
-                    mModel::onAppIconChanged, MODEL_EXECUTOR.getHandler());
-            mOnTerminateCallback.add(iconChangeTracker::close);
-
-            InstallSessionTracker installSessionTracker =
-                    InstallSessionHelper.INSTANCE.get(context).registerInstallTracker(callbacks);
-            mOnTerminateCallback.add(installSessionTracker::unregister);
-        });
-
-        // Register an observer to rebind the notification listener when dots are re-enabled.
-        SettingsCache settingsCache = SettingsCache.INSTANCE.get(mContext);
-        SettingsCache.OnChangeListener notificationLister = this::onNotificationSettingsChanged;
-        settingsCache.register(NOTIFICATION_BADGING_URI, notificationLister);
-        onNotificationSettingsChanged(settingsCache.getValue(NOTIFICATION_BADGING_URI));
-        mOnTerminateCallback.add(() ->
-                settingsCache.unregister(NOTIFICATION_BADGING_URI, notificationLister));
-        // Register an observer to notify Launcher about Private Space settings toggle.
-        registerPrivateSpaceHideWhenLockListener(settingsCache);
+        ModelInitializer initializer = new ModelInitializer(
+                context,
+                LauncherComponentProvider.get(context).getIconPool(),
+                mIconCache,
+                mInvariantDeviceProfile,
+                ThemeManager.INSTANCE.get(context),
+                UserCache.INSTANCE.get(context),
+                SettingsCache.INSTANCE.get(context),
+                mIconProvider,
+                CustomWidgetManager.INSTANCE.get(context),
+                InstallSessionHelper.INSTANCE.get(context),
+                closeable -> mOnTerminateCallback.add(closeable::close)
+        );
+        initializer.initialize(mModel);
     }
 
     public LauncherAppState(Context context, @Nullable String iconCacheFileName) {
@@ -186,32 +103,6 @@
         mOnTerminateCallback.add(mModel::destroy);
     }
 
-    private void onNotificationSettingsChanged(boolean areNotificationDotsEnabled) {
-        if (areNotificationDotsEnabled) {
-            NotificationListener.requestRebind(new ComponentName(
-                    mContext, NotificationListener.class));
-        }
-    }
-
-    private void registerPrivateSpaceHideWhenLockListener(SettingsCache settingsCache) {
-        SettingsCache.OnChangeListener psHideWhenLockChangedListener =
-                this::onPrivateSpaceHideWhenLockChanged;
-        settingsCache.register(PRIVATE_SPACE_HIDE_WHEN_LOCKED_URI, psHideWhenLockChangedListener);
-        mOnTerminateCallback.add(() -> settingsCache.unregister(PRIVATE_SPACE_HIDE_WHEN_LOCKED_URI,
-                psHideWhenLockChangedListener));
-    }
-
-    private void onPrivateSpaceHideWhenLockChanged(boolean isPrivateSpaceHideOnLockEnabled) {
-        mModel.forceReload();
-    }
-
-    private void refreshAndReloadLauncher() {
-        LauncherIcons.clearPool(mContext);
-        mIconCache.updateIconParams(
-                mInvariantDeviceProfile.fillResIconDpi, mInvariantDeviceProfile.iconBitmapSize);
-        mModel.forceReload();
-    }
-
     /**
      * Call from Application.onTerminate(), which is not guaranteed to ever be called.
      */
diff --git a/src/com/android/launcher3/LauncherModel.kt b/src/com/android/launcher3/LauncherModel.kt
index 185629b..6e4276d 100644
--- a/src/com/android/launcher3/LauncherModel.kt
+++ b/src/com/android/launcher3/LauncherModel.kt
@@ -15,13 +15,11 @@
  */
 package com.android.launcher3
 
-import android.app.admin.DevicePolicyManager
 import android.content.Context
 import android.content.Intent
 import android.content.pm.ShortcutInfo
 import android.os.UserHandle
 import android.text.TextUtils
-import android.util.Log
 import android.util.Pair
 import androidx.annotation.WorkerThread
 import com.android.launcher3.celllayout.CellPosMapper
@@ -47,7 +45,6 @@
 import com.android.launcher3.model.data.WorkspaceItemInfo
 import com.android.launcher3.pm.UserCache
 import com.android.launcher3.shortcuts.ShortcutRequest
-import com.android.launcher3.testing.shared.TestProtocol.sDebugTracing
 import com.android.launcher3.util.Executors.MAIN_EXECUTOR
 import com.android.launcher3.util.Executors.MODEL_EXECUTOR
 import com.android.launcher3.util.PackageManagerHelper
@@ -173,15 +170,8 @@
         }
     }
 
-    fun onBroadcastIntent(intent: Intent) {
-        if (DEBUG_RECEIVER || sDebugTracing) Log.d(TAG, "onReceive intent=$intent")
-        when (intent.action) {
-            LauncherAppState.ACTION_FORCE_ROLOAD ->
-                // If we have changed locale we need to clear out the labels in all apps/workspace.
-                forceReload()
-            DevicePolicyManager.ACTION_DEVICE_POLICY_RESOURCE_UPDATED ->
-                enqueueModelUpdateTask(ReloadStringCacheTask(this.modelDelegate))
-        }
+    fun reloadStringCache() {
+        enqueueModelUpdateTask(ReloadStringCacheTask(this.modelDelegate))
     }
 
     /**
diff --git a/src/com/android/launcher3/ModelCallbacks.kt b/src/com/android/launcher3/ModelCallbacks.kt
index 5338fb4..d01f35d 100644
--- a/src/com/android/launcher3/ModelCallbacks.kt
+++ b/src/com/android/launcher3/ModelCallbacks.kt
@@ -11,8 +11,8 @@
 import com.android.launcher3.WorkspaceLayoutManager.FIRST_SCREEN_ID
 import com.android.launcher3.allapps.AllAppsStore
 import com.android.launcher3.config.FeatureFlags
-import com.android.launcher3.debug.TestEvent
 import com.android.launcher3.debug.TestEventEmitter
+import com.android.launcher3.debug.TestEventEmitter.TestEvent
 import com.android.launcher3.model.BgDataModel
 import com.android.launcher3.model.StringCache
 import com.android.launcher3.model.data.AppInfo
@@ -154,7 +154,7 @@
             /*pause=*/ false,
             deviceProfile.isTwoPanels,
         )
-        TestEventEmitter.INSTANCE.get(launcher).sendEvent(TestEvent.WORKSPACE_FINISH_LOADING)
+        TestEventEmitter.sendEvent(TestEvent.WORKSPACE_FINISH_LOADING)
     }
 
     /**
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index b41a425..cfb1161 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -80,8 +80,8 @@
 import com.android.launcher3.celllayout.CellPosMapper;
 import com.android.launcher3.celllayout.CellPosMapper.CellPos;
 import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.debug.TestEvent;
 import com.android.launcher3.debug.TestEventEmitter;
+import com.android.launcher3.debug.TestEventEmitter.TestEvent;
 import com.android.launcher3.dot.FolderDotInfo;
 import com.android.launcher3.dragndrop.DragController;
 import com.android.launcher3.dragndrop.DragLayer;
@@ -2255,7 +2255,7 @@
         if (d.stateAnnouncer != null && !droppedOnOriginalCell) {
             d.stateAnnouncer.completeAction(R.string.item_moved);
         }
-        TestEventEmitter.INSTANCE.get(getContext()).sendEvent(TestEvent.WORKSPACE_ON_DROP);
+        TestEventEmitter.sendEvent(TestEvent.WORKSPACE_ON_DROP);
     }
 
     @Nullable
diff --git a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
index 260ff9f..fafa60b 100644
--- a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
@@ -116,6 +116,7 @@
         ScrimView.ScrimDrawingController {
 
 
+    private static final String TAG = "ActivityAllAppsContainerView";
     public static final float PULL_MULTIPLIER = .02f;
     public static final float FLING_VELOCITY_MULTIPLIER = 1200f;
     protected static final String BUNDLE_KEY_CURRENT_PAGE = "launcher.allapps.current_page";
@@ -600,6 +601,7 @@
             mViewPager.getPageIndicator().setActiveMarker(AdapterHolder.MAIN);
             findViewById(R.id.tab_personal)
                     .setOnClickListener((View view) -> {
+                        Log.d(TAG, "rebindAdapters: " + "Clicked personal tab.");
                         if (mViewPager.snapToPage(AdapterHolder.MAIN)) {
                             mActivityContext.getStatsLogManager().logger()
                                     .log(LAUNCHER_ALLAPPS_TAP_ON_PERSONAL_TAB);
@@ -607,6 +609,7 @@
                     });
             findViewById(R.id.tab_work)
                     .setOnClickListener((View view) -> {
+                        Log.d(TAG, "rebindAdapters: " + "Clicked work tab.");
                         if (mViewPager.snapToPage(AdapterHolder.WORK)) {
                             mActivityContext.getStatsLogManager().logger()
                                     .log(LAUNCHER_ALLAPPS_TAP_ON_WORK_TAB);
diff --git a/src/com/android/launcher3/allapps/WorkPausedCard.java b/src/com/android/launcher3/allapps/WorkPausedCard.java
index a14ac98..864ede8 100644
--- a/src/com/android/launcher3/allapps/WorkPausedCard.java
+++ b/src/com/android/launcher3/allapps/WorkPausedCard.java
@@ -20,6 +20,7 @@
 import android.content.Context;
 import android.content.res.Configuration;
 import android.util.AttributeSet;
+import android.util.Log;
 import android.view.View;
 import android.widget.Button;
 import android.widget.LinearLayout;
@@ -34,6 +35,7 @@
  */
 public class WorkPausedCard extends LinearLayout implements View.OnClickListener {
 
+    private static final String TAG = "WorkPausedCard";
     private final ActivityContext mActivityContext;
     private Button mBtn;
 
@@ -79,6 +81,7 @@
 
     @Override
     public void onClick(View view) {
+        Log.d(TAG, "WorkPausedCard clicked.");
         mActivityContext.getAppsView().getWorkManager().setWorkProfileEnabled(true);
         mActivityContext.getStatsLogManager().logger().log(LAUNCHER_TURN_ON_WORK_APPS_TAP);
     }
diff --git a/src/com/android/launcher3/allapps/WorkProfileManager.java b/src/com/android/launcher3/allapps/WorkProfileManager.java
index 6d7d193..920efa4 100644
--- a/src/com/android/launcher3/allapps/WorkProfileManager.java
+++ b/src/com/android/launcher3/allapps/WorkProfileManager.java
@@ -200,6 +200,7 @@
 
     private void onWorkFabClicked(View view) {
         if (getCurrentState() == STATE_ENABLED && mWorkUtilityView.isEnabled()) {
+            Log.d(TAG, "Work FAB clicked.");
             logEvents(LAUNCHER_TURN_OFF_WORK_APPS_TAP);
             setWorkProfileEnabled(false);
         }
diff --git a/src/com/android/launcher3/allapps/WorkUtilityView.java b/src/com/android/launcher3/allapps/WorkUtilityView.java
index e42a6b9..20ceb15 100644
--- a/src/com/android/launcher3/allapps/WorkUtilityView.java
+++ b/src/com/android/launcher3/allapps/WorkUtilityView.java
@@ -30,6 +30,7 @@
 import android.graphics.Rect;
 import android.text.TextUtils;
 import android.util.AttributeSet;
+import android.util.Log;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.WindowInsets;
@@ -63,6 +64,7 @@
 public class WorkUtilityView extends LinearLayout implements Insettable,
         KeyboardInsetAnimationCallback.KeyboardInsetListener {
 
+    private static final String TAG = "WorkUtilityView";
     private static final int TEXT_EXPAND_OPACITY_DURATION = 300;
     private static final int TEXT_COLLAPSE_OPACITY_DURATION = 50;
     private static final int EXPAND_COLLAPSE_DURATION = 300;
@@ -143,9 +145,11 @@
         mSchedulerButton.setOnClickListener(null);
         if (shouldUseScheduler()) {
             mSchedulerButton.setVisibility(VISIBLE);
-            mSchedulerButton.setOnClickListener(view ->
-                    mActivityContext.startActivitySafely(view,
-                            new Intent(mWorkSchedulerIntentAction), null /* itemInfo */));
+            mSchedulerButton.setOnClickListener(view -> {
+                Log.d(TAG, "WorkScheduler button clicked.");
+                mActivityContext.startActivitySafely(view,
+                        new Intent(mWorkSchedulerIntentAction), null /* itemInfo */);
+            });
         }
     }
 
diff --git a/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java b/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
index 35ae68f..31d0da0 100644
--- a/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
+++ b/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
@@ -20,7 +20,6 @@
 
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherPrefs;
-import com.android.launcher3.graphics.IconShape;
 import com.android.launcher3.graphics.ThemeManager;
 import com.android.launcher3.icons.LauncherIcons.IconPool;
 import com.android.launcher3.model.ItemInstallQueue;
@@ -57,7 +56,6 @@
     ApiWrapper getApiWrapper();
     CustomWidgetManager getCustomWidgetManager();
     DynamicResource getDynamicResource();
-    IconShape getIconShape();
     InstallSessionHelper getInstallSessionHelper();
     ItemInstallQueue getItemInstallQueue();
     RefreshRateTracker getRefreshRateTracker();
diff --git a/src/com/android/launcher3/debug/TestEventEmitter.java b/src/com/android/launcher3/debug/TestEventEmitter.java
new file mode 100644
index 0000000..ed3b4bb
--- /dev/null
+++ b/src/com/android/launcher3/debug/TestEventEmitter.java
@@ -0,0 +1,45 @@
+/*
+ * 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.debug;
+
+/**
+ * TestEventsEmitter shouldn't do anything since it runs on the launcher code and not on
+ * tests. This is just a placeholder and test should mock the static sendEvent method.
+ * See "EventsRule.kt" in tests folder where sendEvent is statically mocked to change the
+ * behavior in tests.
+ */
+public class TestEventEmitter {
+    public static void sendEvent(TestEvent event) {
+    }
+
+    /** Events fired by the launcher. */
+    public enum TestEvent {
+
+        LAUNCHER_ON_CREATE("LAUNCHER_ON_CREATE"),
+        WORKSPACE_ON_DROP("WORKSPACE_ON_DROP"),
+        RESIZE_FRAME_SHOWING("RESIZE_FRAME_SHOWING"),
+        WORKSPACE_FINISH_LOADING("WORKSPACE_FINISH_LOADING"),
+        SPRING_LOADED_STATE_STARTED("SPRING_LOADED_STATE_STARTED"),
+        SPRING_LOADED_STATE_COMPLETED("SPRING_LOADED_STATE_COMPLETED");
+
+        TestEvent(String event) {
+        }
+
+    }
+}
+
+
diff --git a/src/com/android/launcher3/debug/TestEventsEmitterProduction.kt b/src/com/android/launcher3/debug/TestEventsEmitterProduction.kt
deleted file mode 100644
index 52b454f..0000000
--- a/src/com/android/launcher3/debug/TestEventsEmitterProduction.kt
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * 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.launcher3.debug
-
-import android.content.Context
-import android.util.Log
-import com.android.launcher3.util.MainThreadInitializedObject
-import com.android.launcher3.util.SafeCloseable
-
-/** Events fired by the launcher. */
-enum class TestEvent(val event: String) {
-    LAUNCHER_ON_CREATE("LAUNCHER_ON_CREATE"),
-    WORKSPACE_ON_DROP("WORKSPACE_ON_DROP"),
-    RESIZE_FRAME_SHOWING("RESIZE_FRAME_SHOWING"),
-    WORKSPACE_FINISH_LOADING("WORKSPACE_FINISH_LOADING"),
-    SPRING_LOADED_STATE_STARTED("SPRING_LOADED_STATE_STARTED"),
-    SPRING_LOADED_STATE_COMPLETED("SPRING_LOADED_STATE_COMPLETED"),
-}
-
-/** Interface to create TestEventEmitters. */
-interface TestEventEmitter : SafeCloseable {
-
-    companion object {
-        @JvmField
-        val INSTANCE =
-            MainThreadInitializedObject<TestEventEmitter> { _: Context? ->
-                TestEventsEmitterProduction()
-            }
-    }
-
-    fun sendEvent(event: TestEvent)
-}
-
-/**
- * TestEventsEmitterProduction shouldn't do anything since it runs on the launcher code and not on
- * tests. This is just a placeholder and test should override this class.
- */
-class TestEventsEmitterProduction : TestEventEmitter {
-
-    override fun close() {}
-
-    override fun sendEvent(event: TestEvent) {
-        Log.d("TestEventsEmitterProduction", "Event sent ${event.event}")
-    }
-}
diff --git a/src/com/android/launcher3/dragndrop/DragView.java b/src/com/android/launcher3/dragndrop/DragView.java
index 072673d..bff323c 100644
--- a/src/com/android/launcher3/dragndrop/DragView.java
+++ b/src/com/android/launcher3/dragndrop/DragView.java
@@ -60,7 +60,7 @@
 import com.android.app.animation.Interpolators;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.graphics.IconShape;
+import com.android.launcher3.graphics.ThemeManager;
 import com.android.launcher3.icons.FastBitmapDrawable;
 import com.android.launcher3.icons.LauncherIcons;
 import com.android.launcher3.model.data.ItemInfo;
@@ -274,9 +274,9 @@
                 Utilities.scaleRectAboutCenter(shrunkBounds, 0.98f);
                 adaptiveIcon.setBounds(shrunkBounds);
 
-                IconShape iconShape = IconShape.INSTANCE.get(getContext());
+                ThemeManager themeManager = ThemeManager.INSTANCE.get(getContext());
                 final Path mask = (adaptiveIcon instanceof FolderAdaptiveIcon
-                        ? iconShape.getFolderShape() : iconShape.getShape())
+                        ? themeManager.getFolderShape() : themeManager.getIconShape())
                         .getPath(shrunkBounds);
 
                 mTranslateX = new SpringFloatValue(DragView.this,
diff --git a/src/com/android/launcher3/folder/FolderAnimationManager.java b/src/com/android/launcher3/folder/FolderAnimationManager.java
index 2157610..d2354c1 100644
--- a/src/com/android/launcher3/folder/FolderAnimationManager.java
+++ b/src/com/android/launcher3/folder/FolderAnimationManager.java
@@ -46,8 +46,8 @@
 import com.android.launcher3.anim.PropertyResetListener;
 import com.android.launcher3.apppairs.AppPairIcon;
 import com.android.launcher3.celllayout.CellLayoutLayoutParams;
-import com.android.launcher3.graphics.IconShape;
-import com.android.launcher3.graphics.IconShape.ShapeDelegate;
+import com.android.launcher3.graphics.ShapeDelegate;
+import com.android.launcher3.graphics.ThemeManager;
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.views.BaseDragLayer;
 
@@ -237,7 +237,7 @@
         }
         play(a, getAnimator(mFolder.mFooter, ALPHA, 0, 1f), footerStartDelay, footerAlphaDuration);
 
-        ShapeDelegate shapeDelegate = IconShape.INSTANCE.get(mContext).getFolderShape();
+        ShapeDelegate shapeDelegate = ThemeManager.INSTANCE.get(mContext).getFolderShape();
         // Create reveal animator for the folder background
         play(a, shapeDelegate.createRevealAnimator(
                 mFolder, startRect, endRect, finalRadius, !mIsOpening));
diff --git a/src/com/android/launcher3/folder/PreviewBackground.java b/src/com/android/launcher3/folder/PreviewBackground.java
index 77fa355..ba8a290 100644
--- a/src/com/android/launcher3/folder/PreviewBackground.java
+++ b/src/com/android/launcher3/folder/PreviewBackground.java
@@ -47,8 +47,8 @@
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.R;
 import com.android.launcher3.celllayout.DelegatedCellDrawing;
-import com.android.launcher3.graphics.IconShape;
-import com.android.launcher3.graphics.IconShape.ShapeDelegate;
+import com.android.launcher3.graphics.ShapeDelegate;
+import com.android.launcher3.graphics.ThemeManager;
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.views.ActivityContext;
 
@@ -260,7 +260,7 @@
     }
 
     private ShapeDelegate getShape() {
-        return IconShape.INSTANCE.get(mContext).getFolderShape();
+        return ThemeManager.INSTANCE.get(mContext).getFolderShape();
     }
 
     public void drawShadow(Canvas canvas) {
diff --git a/src/com/android/launcher3/graphics/PreloadIconDrawable.java b/src/com/android/launcher3/graphics/PreloadIconDrawable.java
index 50d6d1c..3bd9fb5 100644
--- a/src/com/android/launcher3/graphics/PreloadIconDrawable.java
+++ b/src/com/android/launcher3/graphics/PreloadIconDrawable.java
@@ -119,7 +119,7 @@
                 IconPalette.getPreloadProgressColor(context, info.bitmap.color),
                 getPreloadColors(context),
                 Utilities.isDarkTheme(context),
-                IconShape.INSTANCE.get(context).getShape().getPath(DEFAULT_PATH_SIZE)
+                ThemeManager.INSTANCE.get(context).getIconShape().getPath(DEFAULT_PATH_SIZE)
         );
     }
 
diff --git a/src/com/android/launcher3/graphics/IconShape.kt b/src/com/android/launcher3/graphics/ShapeDelegate.kt
similarity index 81%
rename from src/com/android/launcher3/graphics/IconShape.kt
rename to src/com/android/launcher3/graphics/ShapeDelegate.kt
index eac3440..df0c8f9 100644
--- a/src/com/android/launcher3/graphics/IconShape.kt
+++ b/src/com/android/launcher3/graphics/ShapeDelegate.kt
@@ -42,78 +42,38 @@
 import androidx.graphics.shapes.toPath
 import androidx.graphics.shapes.transformed
 import com.android.launcher3.anim.RoundedRectRevealOutlineProvider
-import com.android.launcher3.dagger.LauncherAppComponent
-import com.android.launcher3.dagger.LauncherAppSingleton
-import com.android.launcher3.graphics.ThemeManager.ThemeChangeListener
 import com.android.launcher3.icons.GraphicsUtils
 import com.android.launcher3.icons.IconNormalizer.normalizeAdaptiveIcon
-import com.android.launcher3.util.DaggerSingletonObject
-import com.android.launcher3.util.DaggerSingletonTracker
 import com.android.launcher3.views.ClipPathView
-import javax.inject.Inject
 
 /** Abstract representation of the shape of an icon shape */
-@LauncherAppSingleton
-class IconShape
-@Inject
-constructor(private val themeManager: ThemeManager, lifeCycle: DaggerSingletonTracker) {
+interface ShapeDelegate {
 
-    val normalizationScale =
-        normalizeAdaptiveIcon(
-            AdaptiveIconDrawable(null, ColorDrawable(Color.BLACK)),
-            AREA_CALC_SIZE,
-        )
+    fun getPath(pathSize: Float = DEFAULT_PATH_SIZE) =
+        Path().apply { addToPath(this, 0f, 0f, pathSize / 2) }
 
-    var shape: ShapeDelegate = pickBestShape(themeManager.iconState.iconMask)
-        private set
-
-    var folderShape: ShapeDelegate =
-        themeManager.iconState.run {
-            if (folderShapeMask == iconMask || folderShapeMask.isEmpty()) shape
-            else pickBestShape(folderShapeMask)
+    fun getPath(bounds: Rect) =
+        Path().apply {
+            addToPath(
+                this,
+                bounds.left.toFloat(),
+                bounds.top.toFloat(),
+                // Radius is half of the average size of the icon
+                (bounds.width() + bounds.height()) / 4f,
+            )
         }
-        private set
 
-    init {
-        val changeListener = ThemeChangeListener {
-            shape = pickBestShape(themeManager.iconState.iconMask)
-            folderShape =
-                themeManager.iconState.run {
-                    if (folderShapeMask == iconMask || folderShapeMask.isEmpty()) shape
-                    else pickBestShape(folderShapeMask)
-                }
-        }
-        themeManager.addChangeListener(changeListener)
-        lifeCycle.addCloseable { themeManager.removeChangeListener(changeListener) }
-    }
+    fun drawShape(canvas: Canvas, offsetX: Float, offsetY: Float, radius: Float, paint: Paint)
 
-    interface ShapeDelegate {
-        fun getPath(pathSize: Float = DEFAULT_PATH_SIZE) =
-            Path().apply { addToPath(this, 0f, 0f, pathSize / 2) }
+    fun addToPath(path: Path, offsetX: Float, offsetY: Float, radius: Float)
 
-        fun getPath(bounds: Rect) =
-            Path().apply {
-                addToPath(
-                    this,
-                    bounds.left.toFloat(),
-                    bounds.top.toFloat(),
-                    // Radius is half of the average size of the icon
-                    (bounds.width() + bounds.height()) / 4f,
-                )
-            }
-
-        fun drawShape(canvas: Canvas, offsetX: Float, offsetY: Float, radius: Float, paint: Paint)
-
-        fun addToPath(path: Path, offsetX: Float, offsetY: Float, radius: Float)
-
-        fun <T> createRevealAnimator(
-            target: T,
-            startRect: Rect,
-            endRect: Rect,
-            endRadius: Float,
-            isReversed: Boolean,
-        ): ValueAnimator where T : View, T : ClipPathView
-    }
+    fun <T> createRevealAnimator(
+        target: T,
+        startRect: Rect,
+        endRect: Rect,
+        endRadius: Float,
+        isReversed: Boolean,
+    ): ValueAnimator where T : View, T : ClipPathView
 
     class Circle : RoundedSquare(1f) {
 
@@ -179,10 +139,15 @@
                 }
                 .createRevealAnimator(target, isReversed)
         }
+
+        override fun equals(other: Any?) =
+            other is RoundedSquare && other.radiusRatio == radiusRatio
+
+        override fun hashCode() = radiusRatio.hashCode()
     }
 
     /** Generic shape delegate with pathString in bounds [0, 0, 100, 100] */
-    class GenericPathShape(pathString: String) : ShapeDelegate {
+    data class GenericPathShape(private val pathString: String) : ShapeDelegate {
         private val poly =
             RoundedPolygon(
                 features = SvgPathParser.parseFeatures(pathString),
@@ -287,7 +252,6 @@
     }
 
     companion object {
-        @JvmField var INSTANCE = DaggerSingletonObject(LauncherAppComponent::getIconShape)
 
         const val TAG = "IconShape"
         const val DEFAULT_PATH_SIZE = 100f
@@ -312,7 +276,6 @@
             }
         }
 
-        @VisibleForTesting
         fun pickBestShape(shapeStr: String): ShapeDelegate {
             val baseShape =
                 if (shapeStr.isNotEmpty()) {
@@ -332,7 +295,6 @@
             return pickBestShape(baseShape, shapeStr)
         }
 
-        @VisibleForTesting
         fun pickBestShape(baseShape: Path, shapeStr: String): ShapeDelegate {
             val calcAreaDiff = areaDiffCalculator(baseShape)
 
@@ -380,5 +342,12 @@
                 centerY = (bottom - top) / 2,
                 rounding = CornerRounding(cornerR),
             )
+
+        @JvmStatic
+        val normalizationScale =
+            normalizeAdaptiveIcon(
+                AdaptiveIconDrawable(null, ColorDrawable(Color.BLACK)),
+                AREA_CALC_SIZE,
+            )
     }
 }
diff --git a/src/com/android/launcher3/graphics/ThemeManager.kt b/src/com/android/launcher3/graphics/ThemeManager.kt
index e767290..de85460 100644
--- a/src/com/android/launcher3/graphics/ThemeManager.kt
+++ b/src/com/android/launcher3/graphics/ThemeManager.kt
@@ -19,12 +19,14 @@
 import android.content.Context
 import android.content.res.Resources
 import com.android.launcher3.EncryptionType
+import com.android.launcher3.Item
 import com.android.launcher3.LauncherPrefChangeListener
 import com.android.launcher3.LauncherPrefs
 import com.android.launcher3.LauncherPrefs.Companion.backedUpItem
 import com.android.launcher3.dagger.ApplicationContext
 import com.android.launcher3.dagger.LauncherAppComponent
 import com.android.launcher3.dagger.LauncherAppSingleton
+import com.android.launcher3.graphics.ShapeDelegate.Companion.pickBestShape
 import com.android.launcher3.icons.IconThemeController
 import com.android.launcher3.icons.mono.MonoIconThemeController
 import com.android.launcher3.shapes.ShapesProvider
@@ -37,50 +39,57 @@
 
 /** Centralized class for managing Launcher icon theming */
 @LauncherAppSingleton
-open class ThemeManager
+class ThemeManager
 @Inject
 constructor(
-    @ApplicationContext protected val context: Context,
-    protected val prefs: LauncherPrefs,
+    @ApplicationContext private val context: Context,
+    private val prefs: LauncherPrefs,
+    private val iconControllerFactory: IconControllerFactory,
     lifecycle: DaggerSingletonTracker,
 ) {
 
     /** Representation of the current icon state */
-    var iconState = parseIconState()
+    var iconState = parseIconState(null)
         private set
 
     var isMonoThemeEnabled
         set(value) = prefs.put(THEMED_ICONS, value)
         get() = prefs.get(THEMED_ICONS)
 
-    val themeController: IconThemeController?
+    val themeController
         get() = iconState.themeController
 
-    val isIconThemeEnabled: Boolean
+    val isIconThemeEnabled
         get() = themeController != null
 
+    val iconShape
+        get() = iconState.iconShape
+
+    val folderShape
+        get() = iconState.folderShape
+
     private val listeners = CopyOnWriteArrayList<ThemeChangeListener>()
 
     init {
         val receiver = SimpleBroadcastReceiver(context, MAIN_EXECUTOR) { verifyIconState() }
         receiver.registerPkgActions("android", ACTION_OVERLAY_CHANGED)
 
-        val prefListener = LauncherPrefChangeListener { key ->
-            when (key) {
-                KEY_THEMED_ICONS,
-                KEY_ICON_SHAPE -> verifyIconState()
-            }
-        }
-        prefs.addListener(prefListener, THEMED_ICONS, PREF_ICON_SHAPE)
+        val keys = (iconControllerFactory.prefKeys + PREF_ICON_SHAPE)
 
+        val keysArray = keys.toTypedArray()
+        val prefKeySet = keys.map { it.sharedPrefKey }
+        val prefListener = LauncherPrefChangeListener { key ->
+            if (prefKeySet.contains(key)) verifyIconState()
+        }
+        prefs.addListener(prefListener, *keysArray)
         lifecycle.addCloseable {
             receiver.unregisterReceiverSafely()
-            prefs.removeListener(prefListener)
+            prefs.removeListener(prefListener, *keysArray)
         }
     }
 
-    protected fun verifyIconState() {
-        val newState = parseIconState()
+    private fun verifyIconState() {
+        val newState = parseIconState(iconState)
         if (newState == iconState) return
         iconState = newState
 
@@ -91,7 +100,7 @@
 
     fun removeChangeListener(listener: ThemeChangeListener) = listeners.remove(listener)
 
-    private fun parseIconState(): IconState {
+    private fun parseIconState(oldState: IconState?): IconState {
         val shapeModel =
             prefs.get(PREF_ICON_SHAPE).let { shapeOverride ->
                 ShapesProvider.iconShapes.values.firstOrNull { it.key == shapeOverride }
@@ -102,24 +111,38 @@
                 CONFIG_ICON_MASK_RES_ID == Resources.ID_NULL -> ""
                 else -> context.resources.getString(CONFIG_ICON_MASK_RES_ID)
             }
+
+        val iconShape =
+            if (oldState != null && oldState.iconMask == iconMask) oldState.iconShape
+            else pickBestShape(iconMask)
+
+        val folderShapeMask = shapeModel?.folderPathString ?: iconMask
+        val folderShape =
+            when {
+                oldState != null && oldState.folderShapeMask == folderShapeMask ->
+                    oldState.folderShape
+                folderShapeMask == iconMask || folderShapeMask.isEmpty() -> iconShape
+                else -> pickBestShape(folderShapeMask)
+            }
+
         return IconState(
             iconMask = iconMask,
-            folderShapeMask = shapeModel?.folderPathString ?: iconMask,
-            themeController = createThemeController(),
+            folderShapeMask = folderShapeMask,
+            themeController = iconControllerFactory.createThemeController(),
             iconScale = shapeModel?.iconScale ?: 1f,
+            iconShape = iconShape,
+            folderShape = folderShape,
         )
     }
 
-    protected open fun createThemeController(): IconThemeController? {
-        return if (isMonoThemeEnabled) MONO_THEME_CONTROLLER else null
-    }
-
     data class IconState(
         val iconMask: String,
         val folderShapeMask: String,
         val themeController: IconThemeController?,
         val themeCode: String = themeController?.themeID ?: "no-theme",
         val iconScale: Float = 1f,
+        val iconShape: ShapeDelegate,
+        val folderShape: ShapeDelegate,
     ) {
         fun toUniqueId() = "${iconMask.hashCode()},$themeCode"
     }
@@ -129,6 +152,15 @@
         fun onThemeChanged()
     }
 
+    open class IconControllerFactory @Inject constructor(protected val prefs: LauncherPrefs) {
+
+        open val prefKeys: List<Item> = listOf(THEMED_ICONS)
+
+        open fun createThemeController(): IconThemeController? {
+            return if (prefs.get(THEMED_ICONS)) MONO_THEME_CONTROLLER else null
+        }
+    }
+
     companion object {
 
         @JvmField val INSTANCE = DaggerSingletonObject(LauncherAppComponent::getThemeManager)
diff --git a/src/com/android/launcher3/icons/LauncherIconProvider.java b/src/com/android/launcher3/icons/LauncherIconProvider.java
index 482360c..836b7d1 100644
--- a/src/com/android/launcher3/icons/LauncherIconProvider.java
+++ b/src/com/android/launcher3/icons/LauncherIconProvider.java
@@ -30,7 +30,7 @@
 
 import com.android.launcher3.R;
 import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.graphics.IconShape;
+import com.android.launcher3.graphics.ShapeDelegate;
 import com.android.launcher3.graphics.ThemeManager;
 import com.android.launcher3.util.ApiWrapper;
 
@@ -54,13 +54,13 @@
     private Map<String, ThemeData> mThemedIconMap;
 
     private final ApiWrapper mApiWrapper;
-    private final IconShape mIconShape;
+    private final ThemeManager mThemeManager;
 
     public LauncherIconProvider(Context context) {
         super(context);
-        setIconThemeSupported(ThemeManager.INSTANCE.get(context).isMonoThemeEnabled());
+        mThemeManager = ThemeManager.INSTANCE.get(context);
         mApiWrapper = ApiWrapper.INSTANCE.get(context);
-        mIconShape = IconShape.INSTANCE.get(context);
+        setIconThemeSupported(mThemeManager.isMonoThemeEnabled());
     }
 
     /**
@@ -91,7 +91,7 @@
     @Override
     protected Drawable loadAppInfoIcon(ApplicationInfo info, Resources resources, int density) {
         // Tries to load the round icon res, if the app defines it as an adaptive icon
-        if (mIconShape.getShape() instanceof IconShape.Circle) {
+        if (mThemeManager.getIconShape() instanceof ShapeDelegate.Circle) {
             int roundIconRes = mApiWrapper.getRoundIconRes(info);
             if (roundIconRes != 0 && roundIconRes != info.icon) {
                 try {
diff --git a/src/com/android/launcher3/icons/LauncherIcons.kt b/src/com/android/launcher3/icons/LauncherIcons.kt
index d173fd6..6c018e8 100644
--- a/src/com/android/launcher3/icons/LauncherIcons.kt
+++ b/src/com/android/launcher3/icons/LauncherIcons.kt
@@ -27,7 +27,6 @@
 import com.android.launcher3.dagger.ApplicationContext
 import com.android.launcher3.dagger.LauncherAppSingleton
 import com.android.launcher3.dagger.LauncherComponentProvider.appComponent
-import com.android.launcher3.graphics.IconShape
 import com.android.launcher3.graphics.ThemeManager
 import com.android.launcher3.pm.UserCache
 import com.android.launcher3.util.UserIconInfo
@@ -46,9 +45,8 @@
 internal constructor(
     @ApplicationContext context: Context,
     idp: InvariantDeviceProfile,
-    themeManager: ThemeManager,
+    private var themeManager: ThemeManager,
     private var userCache: UserCache,
-    private var iconShape: IconShape,
     @Assisted private val pool: ConcurrentLinkedQueue<LauncherIcons>,
 ) : BaseIconFactory(context, idp.fillResIconDpi, idp.iconBitmapSize), AutoCloseable {
 
@@ -70,7 +68,7 @@
 
     public override fun getShapePath(drawable: AdaptiveIconDrawable, iconBounds: Rect): Path {
         if (!Flags.enableLauncherIconShapes()) return drawable.iconMask
-        return iconShape.shape.getPath(iconBounds)
+        return themeManager.iconShape.getPath(iconBounds)
     }
 
     override fun drawAdaptiveIcon(
diff --git a/src/com/android/launcher3/model/GridSizeMigrationDBController.java b/src/com/android/launcher3/model/GridSizeMigrationDBController.java
index 47f13bd..5d0a7bd 100644
--- a/src/com/android/launcher3/model/GridSizeMigrationDBController.java
+++ b/src/com/android/launcher3/model/GridSizeMigrationDBController.java
@@ -238,16 +238,19 @@
         Collections.sort(hotseatToBeAdded);
         Collections.sort(workspaceToBeAdded);
 
-        List<Integer> idsInUse = dstWorkspaceItems.stream()
+        List<DbEntry> remainingDstHotseatItems = destReader.loadHotseatEntries();
+        List<DbEntry> remainingDstWorkspaceItems = destReader.loadAllWorkspaceEntries();
+        List<Integer> idsInUse = remainingDstHotseatItems.stream()
                 .map(entry -> entry.id)
                 .collect(Collectors.toList());
-        idsInUse.addAll(dstHotseatItems.stream()
+        idsInUse.addAll(remainingDstWorkspaceItems.stream()
                 .map(entry -> entry.id)
                 .collect(Collectors.toList()));
 
+
         // Migrate hotseat
         solveHotseatPlacement(helper, destHotseatSize,
-                srcReader, destReader, dstHotseatItems, hotseatToBeAdded, idsInUse);
+                srcReader, destReader, remainingDstHotseatItems, hotseatToBeAdded, idsInUse);
 
         // Migrate workspace.
         // First we create a collection of the screens
@@ -467,7 +470,7 @@
         final Context mContext;
         int mLastScreenId = -1;
 
-        final Map<Integer, List<DbEntry>> mWorkspaceEntriesByScreenId =
+        Map<Integer, List<DbEntry>> mWorkspaceEntriesByScreenId =
                 new ArrayMap<>();
 
         public DbReader(SQLiteDatabase db, String tableName, Context context) {
@@ -539,6 +542,7 @@
         }
 
         protected List<DbEntry> loadAllWorkspaceEntries() {
+            mWorkspaceEntriesByScreenId.clear();
             final List<DbEntry> workspaceEntries = new ArrayList<>();
             Cursor c = queryWorkspace(
                     new String[]{
diff --git a/src/com/android/launcher3/model/GridSizeMigrationLogic.kt b/src/com/android/launcher3/model/GridSizeMigrationLogic.kt
index 9586bf3..5df135a 100644
--- a/src/com/android/launcher3/model/GridSizeMigrationLogic.kt
+++ b/src/com/android/launcher3/model/GridSizeMigrationLogic.kt
@@ -183,9 +183,11 @@
             )
         }
 
+        val remainingDstHotseatItems = destReader.loadHotseatEntries()
+
         placeHotseatItems(
             itemsToBeAdded,
-            dstHotseatItems,
+            remainingDstHotseatItems,
             destHotseatSize,
             helper,
             srcReader,
@@ -265,9 +267,10 @@
             )
         }
 
+        val remainingDstWorkspaceItems = destReader.loadAllWorkspaceEntries()
         placeWorkspaceItems(
             workspaceToBeAdded,
-            dstWorkspaceItems,
+            remainingDstWorkspaceItems,
             targetSize.x,
             targetSize.y,
             helper,
diff --git a/src/com/android/launcher3/model/ModelInitializer.kt b/src/com/android/launcher3/model/ModelInitializer.kt
new file mode 100644
index 0000000..69a320a
--- /dev/null
+++ b/src/com/android/launcher3/model/ModelInitializer.kt
@@ -0,0 +1,165 @@
+/*
+ * 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.model
+
+import android.app.admin.DevicePolicyManager.ACTION_DEVICE_POLICY_RESOURCE_UPDATED
+import android.content.ComponentName
+import android.content.Context
+import android.content.SharedPreferences
+import android.content.pm.LauncherApps
+import android.content.pm.LauncherApps.ArchiveCompatibilityParams
+import com.android.launcher3.BuildConfig
+import com.android.launcher3.Flags
+import com.android.launcher3.InvariantDeviceProfile
+import com.android.launcher3.InvariantDeviceProfile.OnIDPChangeListener
+import com.android.launcher3.LauncherModel
+import com.android.launcher3.LauncherPrefs.Companion.getPrefs
+import com.android.launcher3.Utilities
+import com.android.launcher3.dagger.ApplicationContext
+import com.android.launcher3.graphics.ThemeManager
+import com.android.launcher3.graphics.ThemeManager.ThemeChangeListener
+import com.android.launcher3.icons.IconCache
+import com.android.launcher3.icons.LauncherIconProvider
+import com.android.launcher3.icons.LauncherIcons.IconPool
+import com.android.launcher3.notification.NotificationListener
+import com.android.launcher3.pm.InstallSessionHelper
+import com.android.launcher3.pm.UserCache
+import com.android.launcher3.util.Executors.MODEL_EXECUTOR
+import com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR
+import com.android.launcher3.util.SafeCloseable
+import com.android.launcher3.util.SettingsCache
+import com.android.launcher3.util.SettingsCache.NOTIFICATION_BADGING_URI
+import com.android.launcher3.util.SettingsCache.PRIVATE_SPACE_HIDE_WHEN_LOCKED_URI
+import com.android.launcher3.util.SimpleBroadcastReceiver
+import com.android.launcher3.widget.custom.CustomWidgetManager
+import java.util.function.Consumer
+
+/** Utility class for initializing all model callbacks */
+class ModelInitializer(
+    @ApplicationContext private val context: Context,
+    private val iconPool: IconPool,
+    private val iconCache: IconCache,
+    private val idp: InvariantDeviceProfile,
+    private val themeManager: ThemeManager,
+    private val userCache: UserCache,
+    private val settingsCache: SettingsCache,
+    private val iconProvider: LauncherIconProvider,
+    private val customWidgetManager: CustomWidgetManager,
+    private val installSessionHelper: InstallSessionHelper,
+    private val closeActions: Consumer<SafeCloseable>,
+) {
+
+    fun initialize(model: LauncherModel) {
+        fun refreshAndReloadLauncher() {
+            iconPool.clear()
+            iconCache.updateIconParams(idp.fillResIconDpi, idp.iconBitmapSize)
+            model.forceReload()
+        }
+
+        // IDP changes
+        val idpChangeListener = OnIDPChangeListener { modelChanged ->
+            if (modelChanged) refreshAndReloadLauncher()
+        }
+        idp.addOnChangeListener(idpChangeListener)
+        closeActions.accept { idp.removeOnChangeListener(idpChangeListener) }
+
+        // Theme changes
+        val themeChangeListener = ThemeChangeListener { refreshAndReloadLauncher() }
+        themeManager.addChangeListener(themeChangeListener)
+        closeActions.accept { themeManager.removeChangeListener(themeChangeListener) }
+
+        // System changes
+        val modelCallbacks = model.newModelCallbacks()
+        val launcherApps = context.getSystemService(LauncherApps::class.java)!!
+        launcherApps.registerCallback(modelCallbacks)
+        closeActions.accept { launcherApps.unregisterCallback(modelCallbacks) }
+
+        if (Utilities.ATLEAST_V && Flags.enableSupportForArchiving()) {
+            launcherApps.setArchiveCompatibility(
+                ArchiveCompatibilityParams().apply {
+                    setEnableUnarchivalConfirmation(false)
+                    setEnableIconOverlay(!Flags.useNewIconForArchivedApps())
+                }
+            )
+        }
+
+        // Device profile policy changes
+        val dpUpdateReceiver =
+            SimpleBroadcastReceiver(context, UI_HELPER_EXECUTOR) { model.reloadStringCache() }
+        dpUpdateReceiver.register(ACTION_DEVICE_POLICY_RESOURCE_UPDATED)
+        closeActions.accept { dpUpdateReceiver.unregisterReceiverSafely() }
+
+        // Development helper
+        if (BuildConfig.IS_STUDIO_BUILD) {
+            val reloadReceiver =
+                SimpleBroadcastReceiver(context, UI_HELPER_EXECUTOR) { model.forceReload() }
+            reloadReceiver.register(Context.RECEIVER_EXPORTED, ACTION_FORCE_RELOAD)
+            closeActions.accept { reloadReceiver.unregisterReceiverSafely() }
+        }
+
+        // User changes
+        closeActions.accept(userCache.addUserEventListener(model::onUserEvent))
+
+        // Private space settings changes
+        val psSettingsListener = SettingsCache.OnChangeListener { model.forceReload() }
+        settingsCache.register(PRIVATE_SPACE_HIDE_WHEN_LOCKED_URI, psSettingsListener)
+        closeActions.accept {
+            settingsCache.unregister(PRIVATE_SPACE_HIDE_WHEN_LOCKED_URI, psSettingsListener)
+        }
+
+        // Notification dots changes
+        val notificationChanges =
+            SettingsCache.OnChangeListener { dotsEnabled ->
+                if (dotsEnabled)
+                    NotificationListener.requestRebind(
+                        ComponentName(context, NotificationListener::class.java)
+                    )
+            }
+        settingsCache.register(NOTIFICATION_BADGING_URI, notificationChanges)
+        notificationChanges.onSettingsChanged(settingsCache.getValue(NOTIFICATION_BADGING_URI))
+        closeActions.accept {
+            settingsCache.unregister(NOTIFICATION_BADGING_URI, notificationChanges)
+        }
+
+        // removable smartspace
+        if (Flags.enableSmartspaceRemovalToggle()) {
+            val smartSpacePrefChanges =
+                SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
+                    if (LoaderTask.SMARTSPACE_ON_HOME_SCREEN == key) model.forceReload()
+                }
+            getPrefs(context).registerOnSharedPreferenceChangeListener(smartSpacePrefChanges)
+            closeActions.accept {
+                getPrefs(context).unregisterOnSharedPreferenceChangeListener(smartSpacePrefChanges)
+            }
+        }
+
+        // Custom widgets
+        closeActions.accept(customWidgetManager.addWidgetRefreshCallback(model::rebindCallbacks))
+
+        // Icon changes
+        closeActions.accept(
+            iconProvider.registerIconChangeListener(model::onAppIconChanged, MODEL_EXECUTOR.handler)
+        )
+
+        // Install session changes
+        closeActions.accept(installSessionHelper.registerInstallTracker(modelCallbacks))
+    }
+
+    companion object {
+        private const val ACTION_FORCE_RELOAD = "force-reload-launcher"
+    }
+}
diff --git a/src/com/android/launcher3/pm/InstallSessionTracker.java b/src/com/android/launcher3/pm/InstallSessionTracker.java
index b9c928c..7451ce2 100644
--- a/src/com/android/launcher3/pm/InstallSessionTracker.java
+++ b/src/com/android/launcher3/pm/InstallSessionTracker.java
@@ -34,13 +34,15 @@
 
 import com.android.launcher3.Flags;
 import com.android.launcher3.util.PackageUserKey;
+import com.android.launcher3.util.SafeCloseable;
 
 import java.lang.ref.WeakReference;
 import java.util.Objects;
 
 @SuppressWarnings("NewApi")
 @WorkerThread
-public class InstallSessionTracker extends PackageInstaller.SessionCallback {
+public class InstallSessionTracker extends PackageInstaller.SessionCallback implements
+        SafeCloseable {
 
     public static final String TAG = "InstallSessionTracker";
 
@@ -196,7 +198,8 @@
         }
     }
 
-    public void unregister() {
+    @Override
+    public void close() {
         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
             mInstaller.unregisterSessionCallback(this);
         } else {
diff --git a/src/com/android/launcher3/popup/PopupPopulator.java b/src/com/android/launcher3/popup/PopupPopulator.java
index b748011..bc5064d 100644
--- a/src/com/android/launcher3/popup/PopupPopulator.java
+++ b/src/com/android/launcher3/popup/PopupPopulator.java
@@ -110,13 +110,16 @@
     public static <T extends Context & ActivityContext> Runnable createUpdateRunnable(
             final T context,
             final ItemInfo originalInfo,
-            final Handler uiHandler, final PopupContainerWithArrow container,
-            final List<DeepShortcutView> shortcutViews) {
+            final Handler uiHandler,
+            final PopupContainerWithArrow container,
+            final List<DeepShortcutView> shortcutViews
+    ) {
         final ComponentName activity = originalInfo.getTargetComponent();
         final UserHandle user = originalInfo.user;
+        final String targetPackage = originalInfo.getTargetPackage();
         return () -> {
             ApplicationInfoWrapper infoWrapper =
-                    new ApplicationInfoWrapper(context, originalInfo.getTargetPackage(), user);
+                    new ApplicationInfoWrapper(context, targetPackage, user);
             List<ShortcutInfo> shortcuts = new ShortcutRequest(context, user)
                     .withContainer(activity)
                     .query(ShortcutRequest.PUBLISHED);
diff --git a/src/com/android/launcher3/views/ClipIconView.java b/src/com/android/launcher3/views/ClipIconView.java
index f90a3e4..ddf18df 100644
--- a/src/com/android/launcher3/views/ClipIconView.java
+++ b/src/com/android/launcher3/views/ClipIconView.java
@@ -47,7 +47,8 @@
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.dragndrop.FolderAdaptiveIcon;
-import com.android.launcher3.graphics.IconShape;
+import com.android.launcher3.graphics.ShapeDelegate;
+import com.android.launcher3.graphics.ThemeManager;
 
 /**
  * A view used to draw both layers of an {@link AdaptiveIconDrawable}.
@@ -174,7 +175,7 @@
         if (mIsAdaptiveIcon) {
             if (!isOpening && progress >= shapeProgressStart) {
                 if (mRevealAnimator == null) {
-                    mRevealAnimator = IconShape.INSTANCE.get(getContext()).getShape()
+                    mRevealAnimator = ThemeManager.INSTANCE.get(getContext()).getIconShape()
                             .createRevealAnimator(this, mStartRevealRect,
                                     mOutline, mTaskCornerRadius, !isOpening);
                     mRevealAnimator.addListener(forEndCallback(() -> mRevealAnimator = null));
@@ -259,7 +260,7 @@
 
             if (!isFolderIcon) {
                 Utilities.scaleRectAboutCenter(mStartRevealRect,
-                        IconShape.INSTANCE.get(getContext()).getNormalizationScale());
+                        ShapeDelegate.getNormalizationScale());
             }
 
             if (dp.isLandscape) {
diff --git a/src/com/android/launcher3/widget/picker/OWNERS b/src/com/android/launcher3/widget/picker/OWNERS
index 6aabbfa..991193f 100644
--- a/src/com/android/launcher3/widget/picker/OWNERS
+++ b/src/com/android/launcher3/widget/picker/OWNERS
@@ -6,7 +6,6 @@
 #
 
 # Widget Picker OWNERS
-zakcohen@google.com
 shamalip@google.com
 wvk@google.com
 
diff --git a/tests/Android.bp b/tests/Android.bp
index 4bc654c..fc08e86 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -168,6 +168,7 @@
         "src/**/*Test.java",
         "src/**/*Test.kt",
         "src/**/RoboApiWrapper.kt",
+        "src/**/EventsRule.kt",
         "multivalentTests/src/**/*Test.java",
         "multivalentTests/src/**/*Test.kt",
     ],
diff --git a/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt b/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
index 9d42e1b..8cdf380 100644
--- a/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
@@ -294,6 +294,8 @@
         gridName: String? = GRID_NAME.defaultValue,
     ) {
         setFlagsRule.setFlags(true, Flags.FLAG_ENABLE_TWOLINE_TOGGLE)
+        // TODO: re-enable as part of b/396211437
+        setFlagsRule.setFlags(false, Flags.FLAG_ENABLE_LAUNCHER_ICON_SHAPES)
         val windowsBounds = perDisplayBoundsCache[displayInfo]!!
         val realBounds = windowsBounds[rotation]
         whenever(windowManagerProxy.getDisplayInfo(any())).thenReturn(displayInfo)
diff --git a/tests/multivalentTests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt b/tests/multivalentTests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt
index 060c28c..f855c51 100644
--- a/tests/multivalentTests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt
@@ -71,7 +71,7 @@
             inv,
             info,
             context.appComponent.wmProxy,
-            context.appComponent.iconShape,
+            context.appComponent.themeManager,
             windowBounds,
             SparseArray(),
             /*isMultiWindowMode=*/ false,
diff --git a/tests/multivalentTests/src/com/android/launcher3/graphics/IconShapeTest.kt b/tests/multivalentTests/src/com/android/launcher3/graphics/ShapeDelegateTest.kt
similarity index 93%
rename from tests/multivalentTests/src/com/android/launcher3/graphics/IconShapeTest.kt
rename to tests/multivalentTests/src/com/android/launcher3/graphics/ShapeDelegateTest.kt
index 311676a..7e38f0e 100644
--- a/tests/multivalentTests/src/com/android/launcher3/graphics/IconShapeTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/graphics/ShapeDelegateTest.kt
@@ -28,13 +28,13 @@
 import androidx.core.graphics.PathParser
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.launcher3.graphics.IconShape.Circle
-import com.android.launcher3.graphics.IconShape.Companion.AREA_CALC_SIZE
-import com.android.launcher3.graphics.IconShape.Companion.AREA_DIFF_THRESHOLD
-import com.android.launcher3.graphics.IconShape.Companion.areaDiffCalculator
-import com.android.launcher3.graphics.IconShape.Companion.pickBestShape
-import com.android.launcher3.graphics.IconShape.GenericPathShape
-import com.android.launcher3.graphics.IconShape.RoundedSquare
+import com.android.launcher3.graphics.ShapeDelegate.Circle
+import com.android.launcher3.graphics.ShapeDelegate.Companion.AREA_CALC_SIZE
+import com.android.launcher3.graphics.ShapeDelegate.Companion.AREA_DIFF_THRESHOLD
+import com.android.launcher3.graphics.ShapeDelegate.Companion.areaDiffCalculator
+import com.android.launcher3.graphics.ShapeDelegate.Companion.pickBestShape
+import com.android.launcher3.graphics.ShapeDelegate.GenericPathShape
+import com.android.launcher3.graphics.ShapeDelegate.RoundedSquare
 import com.android.launcher3.icons.GraphicsUtils
 import com.android.launcher3.views.ClipPathView
 import com.google.common.truth.Truth.assertThat
@@ -43,7 +43,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
-class IconShapeTest {
+class ShapeDelegateTest {
 
     @Test
     fun `areaDiffCalculator increases with outwards shape`() {
diff --git a/tests/multivalentTests/src/com/android/launcher3/model/GridSizeMigrationTest.kt b/tests/multivalentTests/src/com/android/launcher3/model/GridSizeMigrationTest.kt
index adf38fe..5607bb4 100644
--- a/tests/multivalentTests/src/com/android/launcher3/model/GridSizeMigrationTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/model/GridSizeMigrationTest.kt
@@ -275,36 +275,15 @@
         val readerGridA = DbReader(db, TMP_TABLE, context)
         val readerGridB = DbReader(db, TABLE_NAME, context)
         // migrate from A -> B
-        if (Flags.gridMigrationRefactor()) {
-            var gridSizeMigrationLogic = GridSizeMigrationLogic()
-            val idsInUse = mutableListOf<Int>()
-            gridSizeMigrationLogic.migrateHotseat(
-                5,
-                idp.numDatabaseHotseatIcons,
-                readerGridA,
-                readerGridB,
-                dbHelper,
-                idsInUse,
-            )
-            gridSizeMigrationLogic.migrateWorkspace(
-                readerGridA,
-                readerGridB,
-                dbHelper,
-                Point(idp.numColumns, idp.numRows),
-                idsInUse,
-            )
-        } else {
-            GridSizeMigrationDBController.migrate(
-                dbHelper,
-                readerGridA,
-                readerGridB,
-                5,
-                idp.numDatabaseHotseatIcons,
-                Point(idp.numColumns, idp.numRows),
-                DeviceGridState(context),
-                DeviceGridState(idp),
-            )
-        }
+        migrateGrid(
+            dbHelper,
+            readerGridA,
+            readerGridB,
+            5,
+            idp.numDatabaseHotseatIcons,
+            idp.numColumns,
+            idp.numRows,
+        )
 
         // Check hotseat items in grid B
         var c =
@@ -388,8 +367,8 @@
         locMap = parseLocMap(c)
         // Expected workspace items in grid A
         // _ _ _ _ _
-        // _ _ _ _ 5
-        // 9 _ 6 _ 7
+        // 9 _ _ _ 5
+        // _ _ 6 _ 7
         // _ _ 8 _ _
         // _ _ _ _ _
         assertThat(locMap.size.toLong()).isEqualTo(5)
@@ -399,7 +378,7 @@
         assertThat(locMap[testPackage7]).isEqualTo(Triple(0, 4, 2))
         assertThat(locMap[testPackage8]).isEqualTo(Triple(0, 2, 3))
         // Verify items that didn't exist in grid A are added in new screen
-        assertThat(locMap[testPackage9]).isEqualTo(Triple(0, 0, 2))
+        assertThat(locMap[testPackage9]).isEqualTo(Triple(0, 0, 1))
 
         // remove item from B
         db.delete(TMP_TABLE, "$_ID=7", null)
@@ -493,37 +472,15 @@
         val readerGridA = DbReader(db, TMP_TABLE, context)
         val readerGridB = DbReader(db, TABLE_NAME, context)
         // migrate from A -> B
-        if (Flags.gridMigrationRefactor()) {
-            var gridSizeMigrationLogic = GridSizeMigrationLogic()
-            val idsInUse = mutableListOf<Int>()
-            gridSizeMigrationLogic.migrateHotseat(
-                5,
-                idp.numDatabaseHotseatIcons,
-                readerGridA,
-                readerGridB,
-                dbHelper,
-                idsInUse,
-            )
-            gridSizeMigrationLogic.migrateWorkspace(
-                readerGridA,
-                readerGridB,
-                dbHelper,
-                Point(idp.numColumns, idp.numRows),
-                idsInUse,
-            )
-        } else {
-            GridSizeMigrationDBController.migrate(
-                dbHelper,
-                readerGridA,
-                readerGridB,
-                5,
-                idp.numDatabaseHotseatIcons,
-                Point(idp.numColumns, idp.numRows),
-                DeviceGridState(context),
-                DeviceGridState(idp),
-            )
-        }
-
+        migrateGrid(
+            dbHelper,
+            readerGridA,
+            readerGridB,
+            5,
+            idp.numDatabaseHotseatIcons,
+            idp.numColumns,
+            idp.numRows,
+        )
         // Check hotseat items in grid B
         var c =
             db.query(
@@ -597,6 +554,112 @@
         )
     }
 
+    @Test
+    @Throws(Exception::class)
+    @EnableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR)
+    fun testMigrationToFullGridFlagOn() {
+        testMigrationToFullGrid()
+    }
+
+    @Test
+    @Throws(Exception::class)
+    @DisableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR)
+    fun testHotseatMigrationToFullGridFlagOff() {
+        testMigrationToFullGrid()
+    }
+
+    @Throws(Exception::class)
+    fun testMigrationToFullGrid() {
+        // Hotseat items in grid A
+        // 1 2 3 4 5
+        addItem(ITEM_TYPE_APPLICATION, 0, CONTAINER_HOTSEAT, 0, 0, testPackage1, 1, TMP_TABLE)
+        addItem(ITEM_TYPE_DEEP_SHORTCUT, 1, CONTAINER_HOTSEAT, 0, 0, testPackage2, 2, TMP_TABLE)
+        addItem(ITEM_TYPE_DEEP_SHORTCUT, 2, CONTAINER_HOTSEAT, 0, 0, testPackage3, 3, TMP_TABLE)
+        addItem(ITEM_TYPE_APPLICATION, 3, CONTAINER_HOTSEAT, 0, 0, testPackage4, 4, TMP_TABLE)
+        addItem(ITEM_TYPE_APPLICATION, 4, CONTAINER_HOTSEAT, 0, 0, testPackage5, 5, TMP_TABLE)
+
+        // Hotseat items in grid B
+        // 6 7 8 9
+        addItem(ITEM_TYPE_DEEP_SHORTCUT, 0, CONTAINER_HOTSEAT, 0, 0, testPackage6)
+        addItem(ITEM_TYPE_DEEP_SHORTCUT, 1, CONTAINER_HOTSEAT, 0, 0, testPackage7)
+        addItem(ITEM_TYPE_APPLICATION, 2, CONTAINER_HOTSEAT, 0, 0, testPackage8)
+        addItem(ITEM_TYPE_APPLICATION, 3, CONTAINER_HOTSEAT, 0, 0, testPackage9)
+
+        // Workspace items in grid A
+        // _ _ _ _ _
+        // 6 _ _ _ _
+        // _ _ _ _ _
+        // _ _ _ _ _
+        // _ _ _ _ _
+        addItem(ITEM_TYPE_APPLICATION, 0, CONTAINER_DESKTOP, 0, 1, testPackage6, 6, TMP_TABLE)
+
+        // Workspace items in grid B
+        // _ _ _ _
+        // 1 2 3 4
+        // _ _ _ _
+        // _ _ _ _
+        addItem(ITEM_TYPE_APPLICATION, 0, CONTAINER_DESKTOP, 0, 1, testPackage1)
+        addItem(ITEM_TYPE_APPLICATION, 0, CONTAINER_DESKTOP, 1, 1, testPackage2)
+        addItem(ITEM_TYPE_APPLICATION, 0, CONTAINER_DESKTOP, 2, 1, testPackage3)
+        addItem(ITEM_TYPE_APPLICATION, 0, CONTAINER_DESKTOP, 3, 1, testPackage4)
+
+        idp.numDatabaseHotseatIcons = 4
+        idp.numColumns = 4
+        idp.numRows = 4
+        val readerGridA = DbReader(db, TMP_TABLE, context)
+        val readerGridB = DbReader(db, TABLE_NAME, context)
+
+        // migrate from A -> B
+        migrateGrid(
+            dbHelper,
+            readerGridA,
+            readerGridB,
+            5,
+            idp.numDatabaseHotseatIcons,
+            idp.numColumns,
+            idp.numRows,
+        )
+
+        // Check hotseat items in grid B
+        var c =
+            db.query(
+                TABLE_NAME,
+                arrayOf(SCREEN, INTENT),
+                "container=$CONTAINER_HOTSEAT",
+                null,
+                SCREEN,
+                null,
+                null,
+            ) ?: throw IllegalStateException()
+        // Expected hotseat items in grid B
+        // 1 2 3 4
+        verifyHotseat(
+            c,
+            mutableListOf(testPackage1, testPackage2, testPackage3, testPackage4).toList(),
+            4,
+        )
+
+        // Check workspace items in grid B
+        c =
+            db.query(
+                TABLE_NAME,
+                arrayOf(SCREEN, CELLX, CELLY, INTENT),
+                "container=$CONTAINER_DESKTOP",
+                null,
+                null,
+                null,
+                null,
+            ) ?: throw IllegalStateException()
+        val locMap = parseLocMap(c)
+        // Expected workspace items in grid B
+        // _ _ _ _
+        // 6 _ _ _
+        // _ _ _ _
+        // _ _ _ _
+        assertThat(locMap.size.toLong()).isEqualTo(1)
+        assertThat(locMap[testPackage6]).isEqualTo(Triple(0, 0, 1))
+    }
+
     private fun migrateGrid(
         dbHelper: DatabaseHelper,
         srcReader: DbReader,
@@ -621,7 +684,7 @@
                 srcReader,
                 destReader,
                 dbHelper,
-                Point(idp.numColumns, idp.numRows),
+                Point(pointX, pointY),
                 idsInUse,
             )
         } else {
diff --git a/tests/multivalentTests/src/com/android/launcher3/model/WorkspaceItemProcessorTest.kt b/tests/multivalentTests/src/com/android/launcher3/model/WorkspaceItemProcessorTest.kt
index 7a403e1..a55d64b 100644
--- a/tests/multivalentTests/src/com/android/launcher3/model/WorkspaceItemProcessorTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/model/WorkspaceItemProcessorTest.kt
@@ -454,7 +454,7 @@
         assertThat(mAllDeepShortcuts).isEmpty()
         verify(mockCursor)
             .markDeleted(
-                "Pinned shortcut not found from request. package=pkg, user=UserHandle{0}",
+                "Pinned shortcut not found from request. package=pkg, user=$mUserHandle",
                 "shortcut_not_found",
             )
     }
@@ -513,7 +513,7 @@
         verify(mockCursor, times(0)).checkAndAddItem(any(), any(), anyOrNull())
         verify(mockCursor)
             .markDeleted(
-                "Pinned shortcut not found from request. package=pkg, user=UserHandle{0}",
+                "Pinned shortcut not found from request. package=pkg, user=$mUserHandle",
                 "shortcut_not_found",
             )
     }
@@ -533,7 +533,7 @@
                 whenever(disabledMessage).thenReturn("")
                 whenever(disabledReason).thenReturn(0)
                 whenever(persons).thenReturn(EMPTY_PERSON_ARRAY)
-                whenever(userHandle).thenReturn(Process.myUserHandle())
+                whenever(userHandle).thenReturn(mUserHandle)
             }
         mIconRequestInfos = mutableListOf()
         // Make sure shortcuts map has expected key from expected package
@@ -565,7 +565,7 @@
         mockBgDataModel = mock<BgDataModel>()
         mockCursor =
             mock<LoaderCursor>().apply {
-                user = UserHandle(0)
+                user = mUserHandle
                 itemType = ITEM_TYPE_FOLDER
                 id = 1
                 container = 100
diff --git a/tests/multivalentTests/src/com/android/launcher3/pm/InstallSessionTrackerTest.kt b/tests/multivalentTests/src/com/android/launcher3/pm/InstallSessionTrackerTest.kt
index 15a9964..23c1da9 100644
--- a/tests/multivalentTests/src/com/android/launcher3/pm/InstallSessionTrackerTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/pm/InstallSessionTrackerTest.kt
@@ -219,7 +219,7 @@
             .whenever(launcherApps)
             .unregisterPackageInstallerSessionCallback(installSessionTracker)
         // When
-        installSessionTracker.unregister()
+        installSessionTracker.close()
         // Then
         verify(launcherApps).unregisterPackageInstallerSessionCallback(installSessionTracker)
     }
diff --git a/tests/multivalentTests/src/com/android/launcher3/shapes/ShapesProviderTest.kt b/tests/multivalentTests/src/com/android/launcher3/shapes/ShapesProviderTest.kt
index 2b8896e..508c9a4 100644
--- a/tests/multivalentTests/src/com/android/launcher3/shapes/ShapesProviderTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/shapes/ShapesProviderTest.kt
@@ -22,7 +22,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.launcher3.Flags.FLAG_ENABLE_LAUNCHER_ICON_SHAPES
-import com.android.launcher3.graphics.IconShape.GenericPathShape
+import com.android.launcher3.graphics.ShapeDelegate.GenericPathShape
 import com.android.systemui.shared.Flags.FLAG_NEW_CUSTOMIZATION_PICKER_UI
 import org.junit.Rule
 import org.junit.Test
diff --git a/tests/src/com/android/launcher3/celllayout/integrationtest/events/EventWaiter.kt b/tests/src/com/android/launcher3/celllayout/integrationtest/events/EventWaiter.kt
new file mode 100644
index 0000000..20ad60f
--- /dev/null
+++ b/tests/src/com/android/launcher3/celllayout/integrationtest/events/EventWaiter.kt
@@ -0,0 +1,52 @@
+/*
+ * 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.launcher3.celllayout.integrationtest.events
+
+import com.android.launcher3.debug.TestEventEmitter.TestEvent
+import java.util.concurrent.TimeUnit
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withTimeoutOrNull
+
+enum class EventStatus() {
+    SUCCESS,
+    FAILURE,
+    TIMEOUT,
+}
+
+class EventWaiter(val eventToWait: TestEvent) {
+    private val deferrable = CompletableDeferred<EventStatus>()
+
+    companion object {
+        private const val TAG = "EventWaiter"
+        private val SIGNAL_TIMEOUT = TimeUnit.SECONDS.toMillis(5)
+    }
+
+    fun waitForSignal(timeout: Long = SIGNAL_TIMEOUT) = runBlocking {
+        var status = withTimeoutOrNull(timeout) { deferrable.await() }
+        if (status == null) {
+            status = EventStatus.TIMEOUT
+        }
+        if (status != EventStatus.SUCCESS) {
+            throw Exception("Failure waiting for event $eventToWait, failure = $status")
+        }
+    }
+
+    fun terminate() {
+        deferrable.complete(EventStatus.SUCCESS)
+    }
+}
diff --git a/tests/src/com/android/launcher3/celllayout/integrationtest/events/EventsRule.kt b/tests/src/com/android/launcher3/celllayout/integrationtest/events/EventsRule.kt
index fb61ced..45eb5e1 100644
--- a/tests/src/com/android/launcher3/celllayout/integrationtest/events/EventsRule.kt
+++ b/tests/src/com/android/launcher3/celllayout/integrationtest/events/EventsRule.kt
@@ -17,11 +17,15 @@
 package com.android.launcher3.celllayout.integrationtest.events
 
 import android.content.Context
-import com.android.launcher3.debug.TestEvent
+import android.util.Log
+import com.android.dx.mockito.inline.extended.ExtendedMockito.*
+import com.android.dx.mockito.inline.extended.StaticMockitoSession
 import com.android.launcher3.debug.TestEventEmitter
+import com.android.launcher3.debug.TestEventEmitter.TestEvent
 import org.junit.rules.TestRule
 import org.junit.runner.Description
 import org.junit.runners.model.Statement
+import org.mockito.quality.Strictness
 
 /**
  * Rule to create EventWaiters to wait for events that happens on the Launcher. For reference look
@@ -30,35 +34,65 @@
  * Waiting for event should be used to prevent race conditions, it provides a more precise way of
  * waiting for events compared to [AbstractLauncherUiTest#waitForLauncherCondition].
  *
- * This class overrides the [TestEventEmitter] with [TestEventsEmitterImplementation] and makes sure
- * to return the [TestEventEmitter] to the previous value when finished.
+ * This class mocks the static method [TestEventEmitter.sendEvent]
  */
 class EventsRule(val context: Context) : TestRule {
 
-    private var prevEventEmitter: TestEventEmitter? = null
+    private val expectedEvents: ArrayDeque<EventWaiter> = ArrayDeque()
 
-    private val eventEmitter = TestEventsEmitterImplementation()
+    private lateinit var mockitoSession: StaticMockitoSession
 
     override fun apply(base: Statement, description: Description?): Statement {
         return object : Statement() {
             override fun evaluate() {
-                beforeTest()
-                base.evaluate()
-                afterTest()
+                try {
+                    beforeTest()
+                    base.evaluate()
+                } finally {
+                    afterTest()
+                }
             }
         }
     }
 
     fun createEventWaiter(expectedEvent: TestEvent): EventWaiter {
-        return eventEmitter.createEventWaiter(expectedEvent)
+        val eventWaiter = EventWaiter(expectedEvent)
+        expectedEvents.add(eventWaiter)
+        return eventWaiter
     }
 
     private fun beforeTest() {
-        prevEventEmitter = TestEventEmitter.INSTANCE.get(context)
-        TestEventEmitter.INSTANCE.initializeForTesting(eventEmitter)
+        mockitoSession =
+            mockitoSession()
+                .strictness(Strictness.LENIENT)
+                .spyStatic(TestEventEmitter::class.java)
+                .startMocking()
+
+        doAnswer { invocation ->
+                val event = (invocation.arguments[0] as TestEvent)
+                Log.d(TAG, "Signal received $event")
+                Log.d(TAG, "Total expected events ${expectedEvents.size}")
+                if (!expectedEvents.isEmpty()) {
+                    val eventWaiter = expectedEvents.last()
+                    if (eventWaiter.eventToWait == event) {
+                        Log.d(TAG, "Removing $event")
+                        expectedEvents.removeLast()
+                        eventWaiter.terminate()
+                    } else {
+                        Log.d(TAG, "Not matching $event")
+                    }
+                }
+                null
+            }
+            .`when` { TestEventEmitter.sendEvent(any()) }
     }
 
     private fun afterTest() {
-        TestEventEmitter.INSTANCE.initializeForTesting(prevEventEmitter)
+        mockitoSession.finishMocking()
+        expectedEvents.clear()
+    }
+
+    companion object {
+        private const val TAG = "TestEvents"
     }
 }
diff --git a/tests/src/com/android/launcher3/celllayout/integrationtest/events/TestEventsEmitterImplementation.kt b/tests/src/com/android/launcher3/celllayout/integrationtest/events/TestEventsEmitterImplementation.kt
deleted file mode 100644
index 5e062d0..0000000
--- a/tests/src/com/android/launcher3/celllayout/integrationtest/events/TestEventsEmitterImplementation.kt
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * 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.launcher3.celllayout.integrationtest.events
-
-import android.util.Log
-import com.android.launcher3.debug.TestEvent
-import com.android.launcher3.debug.TestEventEmitter
-import java.util.concurrent.TimeUnit
-import kotlinx.coroutines.CompletableDeferred
-import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.withTimeoutOrNull
-
-enum class EventStatus() {
-    SUCCESS,
-    FAILURE,
-    TIMEOUT,
-}
-
-class EventWaiter(val eventToWait: TestEvent) {
-    private val deferrable = CompletableDeferred<EventStatus>()
-
-    companion object {
-        private const val TAG = "EventWaiter"
-        private val SIGNAL_TIMEOUT = TimeUnit.SECONDS.toMillis(5)
-    }
-
-    fun waitForSignal(timeout: Long = SIGNAL_TIMEOUT) = runBlocking {
-        var status = withTimeoutOrNull(timeout) { deferrable.await() }
-        if (status == null) {
-            status = EventStatus.TIMEOUT
-        }
-        if (status != EventStatus.SUCCESS) {
-            throw Exception("Failure waiting for event $eventToWait, failure = $status")
-        }
-    }
-
-    fun terminate() {
-        deferrable.complete(EventStatus.SUCCESS)
-    }
-}
-
-class TestEventsEmitterImplementation() : TestEventEmitter {
-    companion object {
-        private const val TAG = "TestEvents"
-    }
-
-    private val expectedEvents: ArrayDeque<EventWaiter> = ArrayDeque()
-
-    fun createEventWaiter(expectedEvent: TestEvent): EventWaiter {
-        val eventWaiter = EventWaiter(expectedEvent)
-        expectedEvents.add(eventWaiter)
-        return eventWaiter
-    }
-
-    private fun clearQueue() {
-        expectedEvents.clear()
-    }
-
-    override fun sendEvent(event: TestEvent) {
-        Log.d(TAG, "Signal received $event")
-        Log.d(TAG, "Total expected events ${expectedEvents.size}")
-        if (expectedEvents.isEmpty()) return
-        val eventWaiter = expectedEvents.last()
-        if (eventWaiter.eventToWait == event) {
-            Log.d(TAG, "Removing $event")
-            expectedEvents.removeLast()
-            eventWaiter.terminate()
-        } else {
-            Log.d(TAG, "Not matching $event")
-        }
-    }
-
-    override fun close() {
-        clearQueue()
-    }
-}
diff --git a/tests/src/com/android/launcher3/nonquickstep/DeviceProfileDumpTest.kt b/tests/src/com/android/launcher3/nonquickstep/DeviceProfileDumpTest.kt
index f6e45a3..05cf926 100644
--- a/tests/src/com/android/launcher3/nonquickstep/DeviceProfileDumpTest.kt
+++ b/tests/src/com/android/launcher3/nonquickstep/DeviceProfileDumpTest.kt
@@ -40,7 +40,6 @@
             Flags.FLAG_ENABLE_SCALING_REVEAL_HOME_ANIMATION,
         )
         setFlagsRule.setFlags(false, Flags.FLAG_ONE_GRID_SPECS)
-        setFlagsRule.setFlags(false, Flags.FLAG_ENABLE_LAUNCHER_ICON_SHAPES)
     }
 
     @Test
diff --git a/tests/src/com/android/launcher3/widget/picker/OWNERS b/tests/src/com/android/launcher3/widget/picker/OWNERS
index 775b0c7..716ab90 100644
--- a/tests/src/com/android/launcher3/widget/picker/OWNERS
+++ b/tests/src/com/android/launcher3/widget/picker/OWNERS
@@ -5,7 +5,6 @@
 #
 
 # Widget Picker OWNERS
-zakcohen@google.com
 shamalip@google.com
 wvk@google.com
 
diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
index 1158521..4e94e1e 100644
--- a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
+++ b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
@@ -129,7 +129,7 @@
         return mTask.getVisibleCenter().x;
     }
 
-    int getTaskCenterY() {
+    public int getTaskCenterY() {
         return mTask.getVisibleCenter().y;
     }