Merge "Pulling secondary user artifacts from tests" into main
diff --git a/Android.bp b/Android.bp
index 1e1e0ad..a4a058f 100644
--- a/Android.bp
+++ b/Android.bp
@@ -455,6 +455,9 @@
         extra_check_modules: ["Launcher3LintChecker"],
         baseline_filename: "lint-baseline.xml",
     },
+    kotlincflags: [
+        "-Xjvm-default=all",
+    ],
 }
 
 // Library with all the dependencies for building quickstep
@@ -519,6 +522,9 @@
     min_sdk_version: "current",
     // TODO(b/319712088): re-enable use_resource_processor
     use_resource_processor: false,
+    kotlincflags: [
+        "-Xjvm-default=all",
+    ],
 }
 
 // Library with all the source code and dependencies for building Quickstep
@@ -552,6 +558,9 @@
     min_sdk_version: "current",
     // TODO(b/319712088): re-enable use_resource_processor
     use_resource_processor: false,
+    kotlincflags: [
+        "-Xjvm-default=all",
+    ],
 }
 
 // Build rule for Quickstep app.
diff --git a/AndroidManifest-common.xml b/AndroidManifest-common.xml
index 80d2eac..46f0e41 100644
--- a/AndroidManifest-common.xml
+++ b/AndroidManifest-common.xml
@@ -131,13 +131,11 @@
             android:writePermission="${applicationId}.permission.WRITE_SETTINGS"
             android:readPermission="${applicationId}.permission.READ_SETTINGS" />
 
-        <!--
-        The content provider for exposing various launcher grid options.
-        TODO: Add proper permissions
-        -->
+        <!-- The content provider for exposing various launcher grid options. -->
         <provider
-            android:name="com.android.launcher3.graphics.GridCustomizationsProvider"
+            android:name="com.android.launcher3.graphics.LauncherCustomizationProvider"
             android:authorities="${applicationId}.grid_control"
+            android:permission="android.permission.BIND_WALLPAPER"
             android:exported="true" />
 
         <!--
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/aconfig/launcher_growth.aconfig b/aconfig/launcher_growth.aconfig
new file mode 100644
index 0000000..35a91d7
--- /dev/null
+++ b/aconfig/launcher_growth.aconfig
@@ -0,0 +1,9 @@
+package: "com.android.launcher3"
+container: "system"
+
+flag {
+    name: "enable_growth_nudge"
+    namespace: "desktop_oobe"
+    description: "Add growth nudge in launcher"
+    bug: "396165728"
+}
diff --git a/aconfig/launcher_overview.aconfig b/aconfig/launcher_overview.aconfig
index f379e22..5749c51 100644
--- a/aconfig/launcher_overview.aconfig
+++ b/aconfig/launcher_overview.aconfig
@@ -96,3 +96,17 @@
     description: "Enable overview on connected displays."
     bug: "363251602"
 }
+
+flag {
+    name: "enable_overview_background_wallpaper_blur"
+    namespace: "launcher_overview"
+    description: "Enable wallpaper blur in overview."
+    bug: "369975912"
+}
+
+flag {
+    name: "enable_overview_desktop_tile_wallpaper_background"
+    namespace: "launcher_overview"
+    description: "Enable wallpaper background for desktop tasks in overview."
+    bug: "363257721"
+}
diff --git a/quickstep/dagger/com/android/launcher3/dagger/AppModule.kt b/quickstep/dagger/com/android/launcher3/dagger/AppModule.kt
index 29586c4..d88fc94 100644
--- a/quickstep/dagger/com/android/launcher3/dagger/AppModule.kt
+++ b/quickstep/dagger/com/android/launcher3/dagger/AppModule.kt
@@ -16,10 +16,17 @@
 
 package com.android.launcher3.dagger
 
+import com.android.launcher3.model.ModelDelegate
+import com.android.launcher3.model.QuickstepModelDelegate
+import dagger.Binds
 import dagger.Module
 
 /**
  * Module containing bindings for the final derivative app, an implementation of this module should
  * be included in the final app code.
  */
-@Module abstract class AppModule {}
+@Module
+abstract class AppModule {
+
+    @Binds abstract fun bindModelDelegate(impl: QuickstepModelDelegate): ModelDelegate
+}
diff --git a/quickstep/res/layout/activity_allset.xml b/quickstep/res/layout/activity_allset.xml
index 625d9b3..3d68dfb 100644
--- a/quickstep/res/layout/activity_allset.xml
+++ b/quickstep/res/layout/activity_allset.xml
@@ -29,7 +29,6 @@
         android:layout_height="match_parent"
         android:gravity="center"
         android:scaleType="centerCrop"
-        app:lottie_autoPlay="true"
         app:lottie_loop="true"
 
         app:layout_constraintTop_toTopOf="parent"
@@ -49,11 +48,10 @@
         app:layout_constraintEnd_toEndOf="parent">
 
         <androidx.constraintlayout.widget.ConstraintLayout
-            android:id="@+id/text_content_view"
+            android:id="@+id/content"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
-            android:layout_marginStart="@dimen/allset_page_margin_horizontal"
-            android:layout_marginEnd="@dimen/allset_page_margin_horizontal"
+            android:paddingHorizontal="@dimen/allset_page_padding_horizontal"
             android:layoutDirection="locale"
             android:textDirection="locale"
             android:forceHasOverlappingRendering="false"
diff --git a/quickstep/res/layout/icon_app_chip_view.xml b/quickstep/res/layout/icon_app_chip_view.xml
index 00b5392..0972be1 100644
--- a/quickstep/res/layout/icon_app_chip_view.xml
+++ b/quickstep/res/layout/icon_app_chip_view.xml
@@ -51,7 +51,7 @@
     <TextView
         android:id="@+id/icon_text_collapsed"
         android:layout_width="@dimen/task_thumbnail_icon_menu_text_collapsed_max_width"
-        android:layout_height="@dimen/task_thumbnail_icon_menu_app_icon_collapsed_size"
+        android:layout_height="wrap_content"
         android:gravity="start|center_vertical"
         android:maxLines="1"
         android:ellipsize="end"
@@ -62,7 +62,7 @@
     <TextView
         android:id="@+id/icon_text_expanded"
         android:layout_width="@dimen/task_thumbnail_icon_menu_text_expanded_max_width"
-        android:layout_height="@dimen/task_thumbnail_icon_menu_app_icon_collapsed_size"
+        android:layout_height="wrap_content"
         android:gravity="start|center_vertical"
         android:maxLines="1"
         android:ellipsize="end"
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/task_thumbnail_view_header.xml b/quickstep/res/layout/task_thumbnail_view_header.xml
index ecc1559..70e4a42 100644
--- a/quickstep/res/layout/task_thumbnail_view_header.xml
+++ b/quickstep/res/layout/task_thumbnail_view_header.xml
@@ -18,6 +18,7 @@
     xmlns:app="http://schemas.android.com/apk/res-auto"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
+    android:id="@+id/task_thumbnail_view_header"
     android:background="@drawable/task_thumbnail_header_bg">
 
     <androidx.constraintlayout.widget.ConstraintLayout
@@ -61,6 +62,7 @@
             android:layout_marginStart="@dimen/task_thumbnail_header_margin_between_views"
             android:src="@drawable/task_header_close_button"
             android:tint="@android:color/darker_gray"
+            android:background="@null"
             app:layout_constraintEnd_toEndOf="parent"
             app:layout_constraintTop_toTopOf="parent"
             app:layout_constraintBottom_toBottomOf="parent"
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-fr-rCA/strings.xml b/quickstep/res/values-fr-rCA/strings.xml
index e7619c2..3201886 100644
--- a/quickstep/res/values-fr-rCA/strings.xml
+++ b/quickstep/res/values-fr-rCA/strings.xml
@@ -90,7 +90,7 @@
     <string name="allset_title" msgid="5021126669778966707">"Tout est prêt!"</string>
     <string name="allset_hint" msgid="459504134589971527">"Balayez l\'écran vers le haut pour accéder à l\'écran d\'accueil"</string>
     <string name="allset_button_hint" msgid="2395219947744706291">"Toucher le bouton d\'accueil pour passer sur votre écran d\'accueil"</string>
-    <string name="allset_description_generic" msgid="5385500062202019855">"Vous êtes maintenant prêt à utiliser votre <xliff:g id="DEVICE">%1$s</xliff:g>"</string>
+    <string name="allset_description_generic" msgid="5385500062202019855">"Vous êtes maintenant prêt à utiliser votre <xliff:g id="DEVICE">%1$s</xliff:g>."</string>
     <string name="default_device_name" msgid="6660656727127422487">"appareil"</string>
     <string name="allset_navigation_settings" msgid="4713404605961476027"><annotation id="link">"Paramètres de navigation du système"</annotation></string>
     <string name="action_share" msgid="2648470652637092375">"Partager"</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/res/values-sw600dp-land/dimens.xml b/quickstep/res/values-sw600dp-land/dimens.xml
index 0052a73..cf7ba00 100644
--- a/quickstep/res/values-sw600dp-land/dimens.xml
+++ b/quickstep/res/values-sw600dp-land/dimens.xml
@@ -16,7 +16,7 @@
 -->
 <resources>
     <!-- All Set page -->
-    <dimen name="allset_page_margin_horizontal">48dp</dimen>
+    <dimen name="allset_page_padding_horizontal">48dp</dimen>
 
     <!-- Gesture Tutorial menu page -->
     <dimen name="gesture_tutorial_menu_padding_horizontal">48dp</dimen>
diff --git a/quickstep/res/values-sw600dp/dimens.xml b/quickstep/res/values-sw600dp/dimens.xml
index 4996582..3e72651 100644
--- a/quickstep/res/values-sw600dp/dimens.xml
+++ b/quickstep/res/values-sw600dp/dimens.xml
@@ -37,7 +37,7 @@
     <dimen name="overview_actions_top_margin">24dp</dimen>
 
     <!-- All Set page -->
-    <dimen name="allset_page_margin_horizontal">120dp</dimen>
+    <dimen name="allset_page_padding_horizontal">120dp</dimen>
     <dimen name="allset_page_allset_text_size">38sp</dimen>
     <dimen name="allset_page_swipe_up_text_size">15sp</dimen>
 </resources>
diff --git a/quickstep/res/values-uk/strings.xml b/quickstep/res/values-uk/strings.xml
index dab58d7..2313a07 100644
--- a/quickstep/res/values-uk/strings.xml
+++ b/quickstep/res/values-uk/strings.xml
@@ -92,7 +92,7 @@
     <string name="allset_button_hint" msgid="2395219947744706291">"Натисніть кнопку головного екрана, щоб відкрити його"</string>
     <string name="allset_description_generic" msgid="5385500062202019855">"Тепер ви можете використовувати <xliff:g id="DEVICE">%1$s</xliff:g>"</string>
     <string name="default_device_name" msgid="6660656727127422487">"пристрій"</string>
-    <string name="allset_navigation_settings" msgid="4713404605961476027"><annotation id="link">"Системні налаштування навігації"</annotation></string>
+    <string name="allset_navigation_settings" msgid="4713404605961476027"><annotation id="link">"Налаштування навігації в системі"</annotation></string>
     <string name="action_share" msgid="2648470652637092375">"Поділитися"</string>
     <string name="action_screenshot" msgid="8171125848358142917">"Знімок екрана"</string>
     <string name="action_split" msgid="2098009717623550676">"Розділити"</string>
diff --git a/quickstep/res/values-uz/strings.xml b/quickstep/res/values-uz/strings.xml
index 1cd2e87..06ca97a 100644
--- a/quickstep/res/values-uz/strings.xml
+++ b/quickstep/res/values-uz/strings.xml
@@ -90,7 +90,7 @@
     <string name="allset_title" msgid="5021126669778966707">"Hammasi tayyor!"</string>
     <string name="allset_hint" msgid="459504134589971527">"Boshiga qaytish uchun tepaga suring"</string>
     <string name="allset_button_hint" msgid="2395219947744706291">"Bosh ekranga oʻtish uchun bosh ekran tugmasini bosing"</string>
-    <string name="allset_description_generic" msgid="5385500062202019855">"<xliff:g id="DEVICE">%1$s</xliff:g> xizmatga tayyor"</string>
+    <string name="allset_description_generic" msgid="5385500062202019855">"Sizning <xliff:g id="DEVICE">%1$s</xliff:g> xizmatga tayyor"</string>
     <string name="default_device_name" msgid="6660656727127422487">"qurilma"</string>
     <string name="allset_navigation_settings" msgid="4713404605961476027"><annotation id="link">"Tizim navigatsiya sozlamalari"</annotation></string>
     <string name="action_share" msgid="2648470652637092375">"Ulashish"</string>
diff --git a/quickstep/res/values/config.xml b/quickstep/res/values/config.xml
index a530325..d699cdf 100644
--- a/quickstep/res/values/config.xml
+++ b/quickstep/res/values/config.xml
@@ -27,7 +27,6 @@
     <string name="instant_app_resolver_class" translatable="false">com.android.quickstep.InstantAppResolverImpl</string>
     <string name="app_launch_tracker_class" translatable="false">com.android.launcher3.appprediction.PredictionAppTracker</string>
     <string name="main_process_initializer_class" translatable="false">com.android.quickstep.QuickstepProcessInitializer</string>
-    <string name="model_delegate_class" translatable="false">com.android.launcher3.model.QuickstepModelDelegate</string>
     <string name="secondary_display_predictions_class" translatable="false">com.android.launcher3.secondarydisplay.SecondaryDisplayPredictionsImpl</string>
     <string name="taskbar_model_callbacks_factory_class" translatable="false">com.android.launcher3.taskbar.TaskbarModelCallbacksFactory</string>
     <string name="taskbar_view_callbacks_factory_class" translatable="false">com.android.launcher3.taskbar.TaskbarViewCallbacksFactory</string>
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index 05f0695..52ebdae 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -277,7 +277,7 @@
     <dimen name="gesture_tutorial_taskbar_margin_bottom">24dp</dimen>
 
     <!-- All Set page -->
-    <dimen name="allset_page_margin_horizontal">40dp</dimen>
+    <dimen name="allset_page_padding_horizontal">40dp</dimen>
     <dimen name="allset_page_allset_text_size">36sp</dimen>
     <dimen name="allset_page_swipe_up_text_size">14sp</dimen>
 
diff --git a/quickstep/res/values/styles.xml b/quickstep/res/values/styles.xml
index 5f2a63d..f8ca8d9 100644
--- a/quickstep/res/values/styles.xml
+++ b/quickstep/res/values/styles.xml
@@ -321,6 +321,7 @@
         <item name="android:fontFamily">google-sans-text-medium</item>
         <item name="android:textSize">@dimen/task_thumbnail_icon_menu_text_size</item>
         <item name="android:textColor">@color/materialColorOnSurface</item>
+        <item name="android:includeFontPadding">false</item>
         <item name="android:letterSpacing">0.025</item>
         <item name="android:lineHeight">20sp</item>
     </style>
diff --git a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
index 4ba4e2b..84ae0fe 100644
--- a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
+++ b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
@@ -56,7 +56,6 @@
 import static com.android.launcher3.testing.shared.TestProtocol.WALLPAPER_OPEN_ANIMATION_FINISHED_MESSAGE;
 import static com.android.launcher3.util.DisplayController.isTransientTaskbar;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-import static com.android.launcher3.util.Executors.ORDERED_BG_EXECUTOR;
 import static com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE;
 import static com.android.launcher3.util.window.RefreshRateTracker.getSingleFrameMs;
 import static com.android.launcher3.views.FloatingIconView.SHAPE_PROGRESS_DURATION;
@@ -79,7 +78,6 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.res.Resources;
-import android.database.ContentObserver;
 import android.graphics.Color;
 import android.graphics.Matrix;
 import android.graphics.Point;
@@ -94,7 +92,6 @@
 import android.os.SystemProperties;
 import android.os.UserHandle;
 import android.provider.Settings;
-import android.provider.Settings.Global;
 import android.util.Pair;
 import android.util.Size;
 import android.view.CrossWindowBlurListeners;
@@ -226,7 +223,6 @@
     private static final int TASKBAR_TO_HOME_DURATION_FAST = 300;
     private static final int TASKBAR_TO_HOME_DURATION_SLOW = 1000;
     protected static final int CONTENT_SCALE_DURATION = 350;
-    protected static final int CONTENT_SCRIM_DURATION = 350;
 
     private static final int MAX_NUM_TASKS = 5;
 
@@ -244,16 +240,12 @@
 
     private final StartingWindowListener mStartingWindowListener =
             new StartingWindowListener(this);
-    private ContentObserver mAnimationRemovalObserver = new ContentObserver(
-            ORDERED_BG_EXECUTOR.getHandler()) {
-        @Override
-        public void onChange(boolean selfChange) {
-            mAreAnimationsEnabled = Global.getFloat(mLauncher.getContentResolver(),
-                    Global.ANIMATOR_DURATION_SCALE, 1f) > 0
-                    || Global.getFloat(mLauncher.getContentResolver(),
-                    Global.TRANSITION_ANIMATION_SCALE, 1f) > 0;
-        }
-    };
+
+    // TODO(b/397690719): Investigate the memory leak from TaskStackChangeListeners#mImpl
+    // This is a temporary fix of memory leak b/397690719. We track registered
+    // {@link TaskRestartedDuringLaunchListener}, and remove them on activity destroy.
+    private final List<TaskRestartedDuringLaunchListener> mRegisteredTaskStackChangeListener =
+            new ArrayList<>();
 
     private DeviceProfile mDeviceProfile;
 
@@ -282,7 +274,6 @@
     // Pairs of window starting type and starting window background color for starting tasks
     // Will never be larger than MAX_NUM_TASKS
     private LinkedHashMap<Integer, Pair<Integer, Integer>> mTaskStartParams;
-    private boolean mAreAnimationsEnabled = true;
 
     private final Interpolator mOpeningXInterpolator;
     private final Interpolator mOpeningInterpolator;
@@ -293,7 +284,6 @@
         mHandler = new Handler(Looper.getMainLooper());
         mDeviceProfile = mLauncher.getDeviceProfile();
         mBackAnimationController = new LauncherBackAnimationController(mLauncher, this);
-        checkAndMonitorIfAnimationsAreEnabled();
 
         Resources res = mLauncher.getResources();
         mClosingWindowTransY = res.getDimensionPixelSize(R.dimen.closing_window_trans_y);
@@ -338,7 +328,14 @@
         TaskRestartedDuringLaunchListener restartedListener =
                 new TaskRestartedDuringLaunchListener();
         restartedListener.register(onEndCallback::executeAllAndDestroy);
-        onEndCallback.add(restartedListener::unregister);
+        mRegisteredTaskStackChangeListener.add(restartedListener);
+        onEndCallback.add(new Runnable() {
+            @Override
+            public void run() {
+                restartedListener.unregister();
+                mRegisteredTaskStackChangeListener.remove(restartedListener);
+            }
+        });
 
         RemoteAnimationRunnerCompat runner = createAppLaunchRunner(v, onEndCallback);
 
@@ -411,7 +408,8 @@
             @NonNull RemoteAnimationTarget[] nonAppTargets, boolean launcherClosing) {
         TaskViewUtils.composeRecentsLaunchAnimator(anim, v, appTargets, wallpaperTargets,
                 nonAppTargets, launcherClosing, mLauncher.getStateManager(),
-                mLauncher.getOverviewPanel(), mLauncher.getDepthController());
+                mLauncher.getOverviewPanel(), mLauncher.getDepthController(),
+                /* transitionInfo= */ null);
     }
 
     private boolean areAllTargetsTranslucent(@NonNull RemoteAnimationTarget[] targets) {
@@ -1207,8 +1205,12 @@
         unregisterRemoteTransitions();
         mLauncher.removeOnDeviceProfileChangeListener(this);
         SystemUiProxy.INSTANCE.get(mLauncher).setStartingWindowListener(null);
-        ORDERED_BG_EXECUTOR.execute(() -> mLauncher.getContentResolver()
-                .unregisterContentObserver(mAnimationRemovalObserver));
+        if (BuildConfig.IS_STUDIO_BUILD && !mRegisteredTaskStackChangeListener.isEmpty()) {
+            throw new IllegalStateException("Failed to run onEndCallback created from"
+                    + " getActivityLaunchOptions()");
+        }
+        mRegisteredTaskStackChangeListener.forEach(TaskRestartedDuringLaunchListener::unregister);
+        mRegisteredTaskStackChangeListener.clear();
     }
 
     /**
@@ -1256,17 +1258,6 @@
         }
     }
 
-    private void checkAndMonitorIfAnimationsAreEnabled() {
-        ORDERED_BG_EXECUTOR.execute(() -> {
-            mAnimationRemovalObserver.onChange(true);
-            mLauncher.getContentResolver().registerContentObserver(Global.getUriFor(
-                    Global.ANIMATOR_DURATION_SCALE), false, mAnimationRemovalObserver);
-            mLauncher.getContentResolver().registerContentObserver(Global.getUriFor(
-                    Global.TRANSITION_ANIMATION_SCALE), false, mAnimationRemovalObserver);
-
-        });
-    }
-
     private boolean launcherIsATargetWithMode(RemoteAnimationTarget[] targets, int mode) {
         for (RemoteAnimationTarget target : targets) {
             if (target.mode == mode && target.taskInfo != null
@@ -1402,7 +1393,8 @@
                     (LauncherAppWidgetHostView) launcherView, targetRect, windowSize,
                     mDeviceProfile.isMultiWindowMode ? 0 : getWindowCornerRadius(mLauncher),
                     isTransluscent, fallbackBackgroundColor);
-        } else if (launcherView != null && mAreAnimationsEnabled) {
+        } else if (launcherView != null && !RemoveAnimationSettingsTracker.INSTANCE.get(
+                mLauncher).isRemoveAnimationEnabled()) {
             floatingIconView = getFloatingIconView(mLauncher, launcherView, null,
                     mLauncher.getTaskbarUIController() == null
                             ? null
@@ -1792,8 +1784,8 @@
     }
 
     /** Get animation duration for taskbar for going to home. */
-    public static int getTaskbarToHomeDuration(boolean isPinnedTaskbar) {
-        return getTaskbarToHomeDuration(false, isPinnedTaskbar);
+    public static int getTaskbarToHomeDuration(boolean isPinnedTaskbarAndNotInDesktopMode) {
+        return getTaskbarToHomeDuration(false, isPinnedTaskbarAndNotInDesktopMode);
     }
 
     /**
@@ -1802,8 +1794,8 @@
      * @param shouldOverrideToFastAnimation should overwrite scaling reveal home animation duration
      */
     public static int getTaskbarToHomeDuration(boolean shouldOverrideToFastAnimation,
-            boolean isPinnedTaskbar) {
-        if (isPinnedTaskbar) {
+            boolean isPinnedTaskbarAndNotInDesktopMode) {
+        if (isPinnedTaskbarAndNotInDesktopMode) {
             return PINNED_TASKBAR_TRANSITION_DURATION;
         } else if (enableScalingRevealHomeAnimation() && !shouldOverrideToFastAnimation) {
             return TASKBAR_TO_HOME_DURATION_SLOW;
diff --git a/quickstep/src/com/android/launcher3/WidgetPickerActivity.java b/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
index 1cf7dda..f992913 100644
--- a/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
+++ b/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
@@ -322,13 +322,14 @@
             stringCache.loadStrings(this);
 
             bindStringCache(stringCache);
-            bindWidgets(mModel.getWidgetsByPackageItem(), mModel.getDefaultWidgetsFilter());
+            bindWidgets(mModel.getWidgetsByPackageItemForPicker(),
+                    mModel.getDefaultWidgetsFilter());
             // Open sheet once widgets are available, so that it doesn't interrupt the open
             // animation.
             openWidgetsSheet();
             if (mUiSurface != null) {
                 mWidgetPredictionsRequester = new WidgetPredictionsRequester(app.getContext(),
-                        mUiSurface, mModel.getWidgetsByComponentKey());
+                        mUiSurface, mModel.getWidgetsByComponentKeyForPicker());
                 mWidgetPredictionsRequester.request(mAddedWidgets, this::bindRecommendedWidgets);
             }
         });
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..688018b
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchAnimatorHelper.kt
@@ -0,0 +1,211 @@
+/*
+ * 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.util.Log
+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) {
+            val changesString =
+                info.changes.joinToString(", ") { change ->
+                    "Change: mode=${change.mode}, " +
+                        "taskId=${change.taskInfo?.id}, " +
+                        "isFreeform=${change.taskInfo?.isFreeform}"
+                }
+            Log.e(
+                TAG,
+                "No launch change found: Transition type=${info.type}, changes=$changesString",
+            )
+            "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,
+            change,
+            transaction,
+            onAnimFinish,
+            interactionJankMonitor,
+            context.mainThreadHandler,
+        )
+    }
+
+    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()
+            }
+        }
+    }
+
+    private companion object {
+        const val TAG = "DesktopAppLaunchAnimatorHelper"
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransition.kt b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransition.kt
index 2406fb6..79072a6 100644
--- a/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransition.kt
+++ b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransition.kt
@@ -17,27 +17,19 @@
 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.util.Log
 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 +40,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,
+class DesktopAppLaunchTransition
+@JvmOverloads
+constructor(
+    context: Context,
     private val 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,15 +64,16 @@
     override fun startAnimation(
         token: IBinder,
         info: TransitionInfo,
-        t: Transaction,
+        transaction: Transaction,
         transitionFinishedCallback: IRemoteTransitionFinishedCallback,
     ) {
+        Log.v(TAG, "startAnimation: launchType=$launchType, cujType=$cujType")
         val safeTransitionFinishedCallback = RemoteRunnable {
             transitionFinishedCallback.onTransitionFinished(/* wct= */ null, /* sct= */ null)
         }
         mainExecutor.execute {
             runAnimators(info, safeTransitionFinishedCallback)
-            t.apply()
+            transaction.apply()
         }
     }
 
@@ -86,78 +83,12 @@
             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 {
+        const val TAG = "DesktopAppLaunchTransition"
         /** 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/model/QuickstepModelDelegate.java b/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java
index 40e8fc2..74b73d4 100644
--- a/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java
+++ b/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java
@@ -47,6 +47,7 @@
 import android.content.pm.ShortcutInfo;
 import android.os.Bundle;
 import android.os.UserHandle;
+import android.text.TextUtils;
 import android.util.Log;
 import android.util.StatsEvent;
 
@@ -61,6 +62,7 @@
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherPrefs;
+import com.android.launcher3.dagger.ApplicationContext;
 import com.android.launcher3.icons.cache.CacheLookupFlag;
 import com.android.launcher3.logger.LauncherAtom;
 import com.android.launcher3.logging.InstanceId;
@@ -89,6 +91,9 @@
 import java.util.Objects;
 import java.util.stream.IntStream;
 
+import javax.inject.Inject;
+import javax.inject.Named;
+
 /**
  * Model delegate which loads prediction items
  */
@@ -114,18 +119,29 @@
             CONTAINER_WIDGETS_PREDICTION, "widgets_prediction", DESKTOP_ICON_FLAG);
 
     private final InvariantDeviceProfile mIDP;
+    private final PackageManagerHelper mPmHelper;
     private final AppEventProducer mAppEventProducer;
+
     private final StatsManager mStatsManager;
 
     protected boolean mActive = false;
 
-    public QuickstepModelDelegate(Context context) {
+    @Inject
+    public QuickstepModelDelegate(@ApplicationContext Context context,
+            InvariantDeviceProfile idp,
+            PackageManagerHelper pmHelper,
+            @Nullable @Named("ICONS_DB") String dbFileName) {
         super(context);
-        mAppEventProducer = new AppEventProducer(context, this::onAppTargetEvent);
+        mIDP = idp;
+        mPmHelper = pmHelper;
 
-        mIDP = InvariantDeviceProfile.INSTANCE.get(context);
+        mAppEventProducer = new AppEventProducer(context, this::onAppTargetEvent);
         StatsLogCompatManager.LOGS_CONSUMER.add(mAppEventProducer);
-        mStatsManager = context.getSystemService(StatsManager.class);
+
+        // Only register for launcher snapshot logging if this is the primary ModelDelegate
+        // instance, as there will be additional instances that may be destroyed at any time.
+        mStatsManager = TextUtils.isEmpty(dbFileName)
+                ? null : context.getSystemService(StatsManager.class);
     }
 
     @CallSuper
@@ -154,10 +170,10 @@
         // TODO: Implement caching and preloading
 
         WorkspaceItemFactory factory =
-                new WorkspaceItemFactory(mApp, ums, mPmHelper, pinnedShortcuts, numColumns,
+                new WorkspaceItemFactory(mContext, ums, mPmHelper, pinnedShortcuts, numColumns,
                         state.containerId, state.lookupFlag);
         FixedContainerItems fci = new FixedContainerItems(state.containerId,
-                state.storage.read(mApp.getContext(), factory, ums.allUsers::get));
+                state.storage.read(mContext, factory, ums.allUsers::get));
         mDataModel.extraItems.put(state.containerId, fci);
     }
 
@@ -220,7 +236,7 @@
         super.modelLoadComplete();
 
         // Log snapshot of the model
-        LauncherPrefs prefs = LauncherPrefs.get(mApp.getContext());
+        LauncherPrefs prefs = LauncherPrefs.get(mContext);
         long lastSnapshotTimeMillis = prefs.get(LAST_SNAPSHOT_TIME_MILLIS);
         // Log snapshot only if previous snapshot was older than a day
         long now = System.currentTimeMillis();
@@ -245,11 +261,7 @@
             prefs.put(LAST_SNAPSHOT_TIME_MILLIS, now);
         }
 
-        // Only register for launcher snapshot logging if this is the primary ModelDelegate
-        // instance, as there will be additional instances that may be destroyed at any time.
-        if (mIsPrimaryInstance) {
-            registerSnapshotLoggingCallback();
-        }
+        registerSnapshotLoggingCallback();
     }
 
     protected void additionalSnapshotEvents(InstanceId snapshotInstanceId){}
@@ -257,9 +269,9 @@
     /**
      * Registers a callback to log launcher workspace layout using Statsd pulled atom.
      */
-    protected void registerSnapshotLoggingCallback() {
+    private void registerSnapshotLoggingCallback() {
         if (mStatsManager == null) {
-            Log.d(TAG, "Failed to get StatsManager");
+            Log.d(TAG, "Skipping snapshot logging");
         }
 
         try {
@@ -332,7 +344,7 @@
         super.destroy();
         mActive = false;
         StatsLogCompatManager.LOGS_CONSUMER.remove(mAppEventProducer);
-        if (mIsPrimaryInstance && mStatsManager != null) {
+        if (mStatsManager != null) {
             try {
                 mStatsManager.clearPullAtomCallback(SysUiStatsLog.LAUNCHER_LAYOUT_SNAPSHOT);
             } catch (RuntimeException e) {
@@ -354,25 +366,24 @@
         if (!mActive) {
             return;
         }
-        Context context = mApp.getContext();
-        AppPredictionManager apm = context.getSystemService(AppPredictionManager.class);
+        AppPredictionManager apm = mContext.getSystemService(AppPredictionManager.class);
         if (apm == null) {
             return;
         }
 
         registerPredictor(mAllAppsState, apm.createAppPredictionSession(
-                new AppPredictionContext.Builder(context)
+                new AppPredictionContext.Builder(mContext)
                         .setUiSurface("home")
                         .setPredictedTargetCount(mIDP.numDatabaseAllAppsColumns)
                         .build()));
 
         // TODO: get bundle
-        registerHotseatPredictor(apm, context);
+        registerHotseatPredictor(apm, mContext);
 
         registerWidgetsPredictor(apm.createAppPredictionSession(
-                new AppPredictionContext.Builder(context)
+                new AppPredictionContext.Builder(mContext)
                         .setUiSurface("widgets")
-                        .setExtras(getBundleForWidgetsOnWorkspace(context, mDataModel))
+                        .setExtras(getBundleForWidgetsOnWorkspace(mContext, mDataModel))
                         .setPredictedTargetCount(NUM_OF_RECOMMENDED_WIDGETS_PREDICATION)
                         .build()));
     }
@@ -383,12 +394,11 @@
         if (!mActive) {
             return;
         }
-        Context context = mApp.getContext();
-        AppPredictionManager apm = context.getSystemService(AppPredictionManager.class);
+        AppPredictionManager apm = mContext.getSystemService(AppPredictionManager.class);
         if (apm == null) {
             return;
         }
-        registerHotseatPredictor(apm, context);
+        registerHotseatPredictor(apm, mContext);
     }
 
     private void registerHotseatPredictor(AppPredictionManager apm, Context context) {
@@ -413,7 +423,7 @@
             // No diff, skip
             return;
         }
-        mApp.getModel().enqueueModelUpdateTask(new PredictionUpdateTask(state, targets));
+        mModel.enqueueModelUpdateTask(new PredictionUpdateTask(state, targets));
     }
 
     private void registerWidgetsPredictor(AppPredictor predictor) {
@@ -424,7 +434,7 @@
                         // No diff, skip
                         return;
                     }
-                    mApp.getModel().enqueueModelUpdateTask(
+                    mModel.enqueueModelUpdateTask(
                             new WidgetsPredictionUpdateTask(mWidgetsRecommendationState, targets));
                 });
         mWidgetsRecommendationState.predictor.requestPredictionUpdate();
@@ -536,7 +546,7 @@
 
     private static class WorkspaceItemFactory implements PersistedItemArray.ItemFactory<ItemInfo> {
 
-        private final LauncherAppState mAppState;
+        private final Context mContext;
         private final UserManagerState mUMS;
         private final PackageManagerHelper mPmHelper;
         private final Map<ShortcutKey, ShortcutInfo> mPinnedShortcuts;
@@ -546,10 +556,11 @@
 
         private int mReadCount = 0;
 
-        protected WorkspaceItemFactory(LauncherAppState appState, UserManagerState ums,
+        protected WorkspaceItemFactory(
+                Context context, UserManagerState ums,
                 PackageManagerHelper pmHelper, Map<ShortcutKey, ShortcutInfo> pinnedShortcuts,
                 int maxCount, int container, CacheLookupFlag lookupFlag) {
-            mAppState = appState;
+            mContext = context;
             mUMS = ums;
             mPmHelper = pmHelper;
             mPinnedShortcuts = pinnedShortcuts;
@@ -566,7 +577,7 @@
             }
             switch (itemType) {
                 case ITEM_TYPE_APPLICATION: {
-                    LauncherActivityInfo lai = mAppState.getContext()
+                    LauncherActivityInfo lai = mContext
                             .getSystemService(LauncherApps.class)
                             .resolveActivity(intent, user);
                     if (lai == null) {
@@ -574,14 +585,15 @@
                     }
                     AppInfo info = new AppInfo(
                             lai,
-                            UserCache.INSTANCE.get(mAppState.getContext()).getUserInfo(user),
-                            ApiWrapper.INSTANCE.get(mAppState.getContext()),
+                            UserCache.INSTANCE.get(mContext).getUserInfo(user),
+                            ApiWrapper.INSTANCE.get(mContext),
                             mPmHelper,
                             mUMS.isUserQuiet(user));
                     info.container = mContainer;
-                    mAppState.getIconCache().getTitleAndIcon(info, lai, mLookupFlag);
+                    LauncherAppState.getInstance(mContext).getIconCache()
+                            .getTitleAndIcon(info, lai, mLookupFlag);
                     mReadCount++;
-                    return info.makeWorkspaceItem(mAppState.getContext());
+                    return info.makeWorkspaceItem(mContext);
                 }
                 case ITEM_TYPE_DEEP_SHORTCUT: {
                     ShortcutKey key = ShortcutKey.fromIntent(intent, user);
@@ -592,9 +604,9 @@
                     if (si == null) {
                         return null;
                     }
-                    WorkspaceItemInfo wii = new WorkspaceItemInfo(si, mAppState.getContext());
+                    WorkspaceItemInfo wii = new WorkspaceItemInfo(si, mContext);
                     wii.container = mContainer;
-                    mAppState.getIconCache().getShortcutIcon(wii, si);
+                    LauncherAppState.getInstance(mContext).getIconCache().getShortcutIcon(wii, si);
                     mReadCount++;
                     return wii;
                 }
diff --git a/quickstep/src/com/android/launcher3/model/WellbeingModel.java b/quickstep/src/com/android/launcher3/model/WellbeingModel.java
index 0f3aaa6..09433c5 100644
--- a/quickstep/src/com/android/launcher3/model/WellbeingModel.java
+++ b/quickstep/src/com/android/launcher3/model/WellbeingModel.java
@@ -109,9 +109,9 @@
                 ? Executors.UI_HELPER_EXECUTOR.getLooper()
                 : Executors.getPackageExecutor(mWellbeingProviderPkg).getLooper());
         mWellbeingAppChangeReceiver =
-                new SimpleBroadcastReceiver(mWorkerHandler, t -> restartObserver());
+                new SimpleBroadcastReceiver(context, mWorkerHandler, t -> restartObserver());
         mAppAddRemoveReceiver =
-                new SimpleBroadcastReceiver(mWorkerHandler, this::onAppPackageChanged);
+                new SimpleBroadcastReceiver(context, mWorkerHandler, this::onAppPackageChanged);
 
 
         mContentObserver = new ContentObserver(mWorkerHandler) {
@@ -148,8 +148,8 @@
     public void close() {
         if (!TextUtils.isEmpty(mWellbeingProviderPkg)) {
             mWorkerHandler.post(() -> {
-                mWellbeingAppChangeReceiver.unregisterReceiverSafely(mContext);
-                mAppAddRemoveReceiver.unregisterReceiverSafely(mContext);
+                mWellbeingAppChangeReceiver.unregisterReceiverSafely();
+                mAppAddRemoveReceiver.unregisterReceiverSafely();
                 mContext.getContentResolver().unregisterContentObserver(mContentObserver);
             });
         }
diff --git a/quickstep/src/com/android/launcher3/model/WidgetPredictionsRequester.java b/quickstep/src/com/android/launcher3/model/WidgetPredictionsRequester.java
index 8c98bab..d3ac975 100644
--- a/quickstep/src/com/android/launcher3/model/WidgetPredictionsRequester.java
+++ b/quickstep/src/com/android/launcher3/model/WidgetPredictionsRequester.java
@@ -203,7 +203,7 @@
         List<ItemInfo> items;
         if (enableCategorizedWidgetSuggestions()) {
             WidgetRecommendationCategoryProvider categoryProvider =
-                    WidgetRecommendationCategoryProvider.newInstance(mContext);
+                    new WidgetRecommendationCategoryProvider();
             items = widgetItems.stream()
                     .map(it -> new PendingAddWidgetInfo(it.widgetInfo, CONTAINER_WIDGETS_PREDICTION,
                             categoryProvider.getWidgetRecommendationCategory(mContext, it)))
diff --git a/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java b/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java
index 40e1c10..8bd2ba8 100644
--- a/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java
+++ b/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java
@@ -79,7 +79,7 @@
         // Widgets (excluding shortcuts & already added widgets) that belong to apps eligible for
         // being in predictions.
         Map<ComponentKey, WidgetItem> allEligibleWidgets =
-                dataModel.widgetsModel.getWidgetsByComponentKey()
+                dataModel.widgetsModel.getWidgetsByComponentKeyForPicker()
                         .entrySet()
                         .stream()
                         .filter(entry -> entry.getValue().widgetInfo != null
@@ -137,7 +137,7 @@
         List<ItemInfo> items;
         if (enableCategorizedWidgetSuggestions()) {
             WidgetRecommendationCategoryProvider categoryProvider =
-                    WidgetRecommendationCategoryProvider.newInstance(context);
+                    new WidgetRecommendationCategoryProvider();
             items = servicePredictedItems.stream()
                     .map(it -> new PendingAddWidgetInfo(it.widgetInfo, CONTAINER_WIDGETS_PREDICTION,
                             categoryProvider.getWidgetRecommendationCategory(context, it)))
diff --git a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.kt b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.kt
index 03f5d96..eb24df1 100644
--- a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.kt
+++ b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.kt
@@ -18,8 +18,11 @@
 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
+import androidx.core.util.forEach
 import com.android.launcher3.LauncherState
 import com.android.launcher3.dagger.ApplicationContext
 import com.android.launcher3.dagger.LauncherAppComponent
@@ -29,7 +32,8 @@
 import com.android.launcher3.uioverrides.QuickstepLauncher
 import com.android.launcher3.util.DaggerSingletonObject
 import com.android.launcher3.util.DaggerSingletonTracker
-import com.android.launcher3.util.Executors
+import com.android.launcher3.util.DisplayController
+import com.android.launcher3.util.Executors.MAIN_EXECUTOR
 import com.android.launcher3.util.window.WindowManagerProxy.DesktopVisibilityListener
 import com.android.quickstep.GestureState.GestureEndTarget
 import com.android.quickstep.SystemUiProxy
@@ -59,27 +63,47 @@
      * (Used only when multiple desks are enabled).
      *
      * @property displayId The ID of the display this object represents.
-     * @property canCreateDesks true if it's possible to create new desks on the display represented
-     *   by this object.
      * @property activeDeskId The ID of the active desk on the associated display (if any). It has a
-     *   value of `-1` if there are no active desks. Note that there can only be at most one active
-     *   desk on each display.
+     *   value of `INACTIVE_DESK_ID` (-1) if there are no active desks. Note that there can only be
+     *   at most one active desk on each display.
      * @property deskIds a set containing the IDs of the desks on the associated display.
      */
     private data class DisplayDeskConfig(
         val displayId: Int,
-        var canCreateDesks: Boolean,
-        var activeDeskId: Int = -1,
+        var activeDeskId: Int = INACTIVE_DESK_ID,
         val deskIds: MutableSet<Int>,
     )
 
+    /** True if it is possible to create new desks on current setup. */
+    var canCreateDesks: Boolean = false
+        private set(value) {
+            if (field == value) return
+            field = value
+            desktopVisibilityListeners.forEach { it.onCanCreateDesksChanged(field) }
+        }
+
     /** Maps each display by its ID to its desks configuration. */
     private val displaysDesksConfigsMap = SparseArray<DisplayDeskConfig>()
 
     private val desktopVisibilityListeners: MutableSet<DesktopVisibilityListener> = HashSet()
     private val taskbarDesktopModeListeners: MutableSet<TaskbarDesktopModeListener> = HashSet()
 
-    /** Number of visible desktop windows in desktop mode. */
+    // This simply indicates that user is currently in desktop mode or not.
+    var isInDesktopMode = false
+        private set
+
+    // to track if any pending notification to be done.
+    var isNotifyingDesktopVisibilityPending = false
+
+    // to let launcher hold off on notifying desktop visibility listeners.
+    var launcherAnimationRunning = false
+
+    // TODO: b/394387739 - Deprecate this and replace it with something that tracks the count per
+    //  desk.
+    /**
+     * Number of visible desktop windows in desktop mode. This can be > 0 when user goes to overview
+     * from desktop window mode.
+     */
     var visibleDesktopTasksCount: Int = 0
         /**
          * Sets the number of desktop windows that are visible and updates launcher visibility based
@@ -97,13 +121,27 @@
             }
 
             if (visibleTasksCount != field) {
+                if (visibleDesktopTasksCount == 0 && visibleTasksCount == 1) {
+                    isInDesktopMode = true
+                }
+                if (visibleDesktopTasksCount == 1 && visibleTasksCount == 0) {
+                    isInDesktopMode = false
+                }
                 val wasVisible = field > 0
                 val isVisible = visibleTasksCount > 0
                 val wereDesktopTasksVisibleBefore = areDesktopTasksVisibleAndNotInOverview()
                 field = visibleTasksCount
                 val areDesktopTasksVisibleNow = areDesktopTasksVisibleAndNotInOverview()
-                if (wereDesktopTasksVisibleBefore != areDesktopTasksVisibleNow) {
-                    notifyDesktopVisibilityListeners(areDesktopTasksVisibleNow)
+
+                if (
+                    wereDesktopTasksVisibleBefore != areDesktopTasksVisibleNow ||
+                        wasVisible != isVisible
+                ) {
+                    if (!launcherAnimationRunning) {
+                        notifyIsInDesktopModeChanged(DEFAULT_DISPLAY, areDesktopTasksVisibleNow)
+                    } else {
+                        isNotifyingDesktopVisibilityPending = true
+                    }
                 }
 
                 if (
@@ -142,17 +180,51 @@
         }
     }
 
-    /** Whether desktop tasks are visible in desktop mode. */
-    fun areDesktopTasksVisible(): Boolean {
-        val desktopTasksVisible: Boolean = visibleDesktopTasksCount > 0
-        if (DEBUG) {
-            Log.d(TAG, "areDesktopTasksVisible: desktopVisible=$desktopTasksVisible")
+    /**
+     * Returns the ID of the active desk (if any) on the display whose ID is [displayId], or
+     * [INACTIVE_DESK_ID] if no desk is currently active or the multiple desks feature is disabled.
+     */
+    fun getActiveDeskId(displayId: Int): Int {
+        if (!DesktopModeStatus.enableMultipleDesktops(context)) {
+            // When the multiple desks feature is disabled, callers should not rely on the concept
+            // of a desk ID.
+            return INACTIVE_DESK_ID
         }
-        return desktopTasksVisible
+
+        return getDisplayDeskConfig(displayId)?.activeDeskId ?: INACTIVE_DESK_ID
+    }
+
+    /** Returns whether a desk is currently active on the display with the given [displayId]. */
+    fun isInDesktopMode(displayId: Int): Boolean {
+        if (!DesktopModeStatus.enableMultipleDesktops(context)) {
+            return isInDesktopMode
+        }
+
+        val activeDeskId = getDisplayDeskConfig(displayId)?.activeDeskId ?: INACTIVE_DESK_ID
+        val isInDesktopMode = activeDeskId != INACTIVE_DESK_ID
+        if (DEBUG) {
+            Log.d(TAG, "isInDesktopMode: $isInDesktopMode")
+        }
+        return isInDesktopMode
+    }
+
+    /**
+     * Returns whether a desk is currently active on the display with the given [displayId] and
+     * Overview is not active.
+     */
+    fun isInDesktopModeAndNotInOverview(displayId: Int): Boolean {
+        if (!DesktopModeStatus.enableMultipleDesktops(context)) {
+            return areDesktopTasksVisibleAndNotInOverview()
+        }
+
+        if (DEBUG) {
+            Log.d(TAG, "isInDesktopModeAndNotInOverview: overview=$inOverviewState")
+        }
+        return isInDesktopMode(displayId) && !inOverviewState
     }
 
     /** Whether desktop tasks are visible in desktop mode. */
-    fun areDesktopTasksVisibleAndNotInOverview(): Boolean {
+    private fun areDesktopTasksVisibleAndNotInOverview(): Boolean {
         val desktopTasksVisible: Boolean = visibleDesktopTasksCount > 0
         if (DEBUG) {
             Log.d(
@@ -184,6 +256,22 @@
         )
     }
 
+    /**
+     * Launcher Driven Desktop Mode changes. For example, swipe to home and quick switch from
+     * Desktop Windowing Mode. if there is any pending notification please notify desktop visibility
+     * listeners.
+     */
+    fun onLauncherAnimationFromDesktopEnd() {
+        launcherAnimationRunning = false
+        if (isNotifyingDesktopVisibilityPending) {
+            isNotifyingDesktopVisibilityPending = false
+            notifyIsInDesktopModeChanged(
+                DEFAULT_DISPLAY,
+                isInDesktopModeAndNotInOverview(DEFAULT_DISPLAY),
+            )
+        }
+    }
+
     fun onLauncherStateChanged(state: RecentsState) {
         onLauncherStateChanged(
             state,
@@ -220,8 +308,23 @@
             val wereDesktopTasksVisibleBefore = areDesktopTasksVisibleAndNotInOverview()
             inOverviewState = overviewStateEnabled
             val areDesktopTasksVisibleNow = areDesktopTasksVisibleAndNotInOverview()
-            if (wereDesktopTasksVisibleBefore != areDesktopTasksVisibleNow) {
-                notifyDesktopVisibilityListeners(areDesktopTasksVisibleNow)
+
+            if (!DesktopModeStatus.enableMultipleDesktops(context)) {
+                if (wereDesktopTasksVisibleBefore != areDesktopTasksVisibleNow) {
+                    notifyIsInDesktopModeChanged(DEFAULT_DISPLAY, areDesktopTasksVisibleNow)
+                }
+            } else {
+                // When overview state changes, it changes together on all displays.
+                displaysDesksConfigsMap.forEach { displayId, deskConfig ->
+                    // Overview affects the state of desks only if desktop mode is active on this
+                    // display.
+                    if (isInDesktopMode(displayId)) {
+                        notifyIsInDesktopModeChanged(
+                            displayId,
+                            isInDesktopModeAndNotInOverview(displayId),
+                        )
+                    }
+                }
             }
 
             if (ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue) {
@@ -249,12 +352,19 @@
         desktopVisibilityListeners.remove(listener)
     }
 
-    private fun notifyDesktopVisibilityListeners(areDesktopTasksVisible: Boolean) {
+    private fun notifyIsInDesktopModeChanged(
+        displayId: Int,
+        isInDesktopModeAndNotInOverview: Boolean,
+    ) {
         if (DEBUG) {
-            Log.d(TAG, "notifyDesktopVisibilityListeners: visible=$areDesktopTasksVisible")
+            Log.d(
+                TAG,
+                "notifyIsInDesktopModeChanged: displayId=$displayId, isInDesktopModeAndNotInOverview=$isInDesktopModeAndNotInOverview",
+            )
         }
+
         for (listener in desktopVisibilityListeners) {
-            listener.onDesktopVisibilityChanged(areDesktopTasksVisible)
+            listener.onIsInDesktopModeChanged(displayId, isInDesktopModeAndNotInOverview)
         }
     }
 
@@ -271,6 +381,26 @@
         }
     }
 
+    private fun notifyTaskbarDesktopModeListenersForEntry(duration: Int) {
+        if (DEBUG) {
+            Log.d(TAG, "notifyTaskbarDesktopModeListenersForEntry: duration=" + duration)
+        }
+        for (listener in taskbarDesktopModeListeners) {
+            listener.onEnterDesktopMode(duration)
+        }
+        DisplayController.INSTANCE.get(context).notifyConfigChange()
+    }
+
+    private fun notifyTaskbarDesktopModeListenersForExit(duration: Int) {
+        if (DEBUG) {
+            Log.d(TAG, "notifyTaskbarDesktopModeListenersForExit: duration=" + duration)
+        }
+        for (listener in taskbarDesktopModeListeners) {
+            listener.onExitDesktopMode(duration)
+        }
+        DisplayController.INSTANCE.get(context).notifyConfigChange()
+    }
+
     /** TODO: b/333533253 - Remove after flag rollout */
     private fun setBackgroundStateEnabled(backgroundStateEnabled: Boolean) {
         if (DEBUG) {
@@ -337,7 +467,10 @@
         }
     }
 
-    private fun onListenerConnected(displayDeskStates: Array<DisplayDeskState>) {
+    private fun onListenerConnected(
+        displayDeskStates: Array<DisplayDeskState>,
+        canCreateDesks: Boolean,
+    ) {
         if (!DesktopModeStatus.enableMultipleDesktops(context)) {
             return
         }
@@ -348,25 +481,24 @@
             displaysDesksConfigsMap[displayDeskState.displayId] =
                 DisplayDeskConfig(
                     displayId = displayDeskState.displayId,
-                    canCreateDesks = displayDeskState.canCreateDesk,
                     activeDeskId = displayDeskState.activeDeskId,
                     deskIds = displayDeskState.deskIds.toMutableSet(),
                 )
         }
+
+        this.canCreateDesks = canCreateDesks
     }
 
-    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) {
+    private fun onCanCreateDesksChanged(canCreateDesks: Boolean) {
         if (!DesktopModeStatus.enableMultipleDesktops(context)) {
             return
         }
 
-        getDisplayDeskConfig(displayId).canCreateDesks = canCreateDesks
+        this.canCreateDesks = canCreateDesks
     }
 
     private fun onDeskAdded(displayId: Int, deskId: Int) {
@@ -374,7 +506,7 @@
             return
         }
 
-        getDisplayDeskConfig(displayId).also {
+        getDisplayDeskConfig(displayId)?.also {
             check(it.deskIds.add(deskId)) {
                 "Found a duplicate desk Id: $deskId on display: $displayId"
             }
@@ -386,12 +518,12 @@
             return
         }
 
-        getDisplayDeskConfig(displayId).also {
+        getDisplayDeskConfig(displayId)?.also {
             check(it.deskIds.remove(deskId)) {
                 "Removing non-existing desk Id: $deskId on display: $displayId"
             }
             if (it.activeDeskId == deskId) {
-                it.activeDeskId = -1
+                it.activeDeskId = INACTIVE_DESK_ID
             }
         }
     }
@@ -401,7 +533,9 @@
             return
         }
 
-        getDisplayDeskConfig(displayId).also {
+        val wasInDesktopMode = isInDesktopModeAndNotInOverview(displayId)
+
+        getDisplayDeskConfig(displayId)?.also {
             check(oldActiveDesk == it.activeDeskId) {
                 "Mismatch between the Shell's oldActiveDesk: $oldActiveDesk, and Launcher's: ${it.activeDeskId}"
             }
@@ -410,6 +544,10 @@
             }
             it.activeDeskId = newActiveDesk
         }
+
+        if (wasInDesktopMode != isInDesktopModeAndNotInOverview(displayId)) {
+            notifyIsInDesktopModeChanged(displayId, !wasInDesktopMode)
+        }
     }
 
     /** TODO: b/333533253 - Remove after flag rollout */
@@ -465,15 +603,18 @@
     ) : Stub() {
         private val controller = WeakReference(controller)
 
-        override fun onListenerConnected(displayDeskStates: Array<DisplayDeskState>) {
-            Executors.MAIN_EXECUTOR.execute {
-                controller.get()?.onListenerConnected(displayDeskStates)
+        override fun onListenerConnected(
+            displayDeskStates: Array<DisplayDeskState>,
+            canCreateDesks: Boolean,
+        ) {
+            MAIN_EXECUTOR.execute {
+                controller.get()?.onListenerConnected(displayDeskStates, canCreateDesks)
             }
         }
 
         override fun onTasksVisibilityChanged(displayId: Int, visibleTasksCount: Int) {
             if (displayId != this.displayId) return
-            Executors.MAIN_EXECUTOR.execute {
+            MAIN_EXECUTOR.execute {
                 controller.get()?.apply {
                     if (DEBUG) {
                         Log.d(TAG, "desktop visible tasks count changed=$visibleTasksCount")
@@ -489,7 +630,7 @@
 
         override fun onTaskbarCornerRoundingUpdate(doesAnyTaskRequireTaskbarRounding: Boolean) {
             if (!DesktopModeStatus.useRoundedCorners()) return
-            Executors.MAIN_EXECUTOR.execute {
+            MAIN_EXECUTOR.execute {
                 controller.get()?.apply {
                     Log.d(
                         TAG,
@@ -501,26 +642,46 @@
             }
         }
 
-        override fun onEnterDesktopModeTransitionStarted(transitionDuration: Int) {}
-
-        override fun onExitDesktopModeTransitionStarted(transitionDuration: Int) {}
-
-        override fun onCanCreateDesksChanged(displayId: Int, canCreateDesks: Boolean) {
-            Executors.MAIN_EXECUTOR.execute {
-                controller.get()?.onCanCreateDesksChanged(displayId, canCreateDesks)
+        override fun onEnterDesktopModeTransitionStarted(transitionDuration: Int) {
+            MAIN_EXECUTOR.execute {
+                Log.d(
+                    TAG,
+                    ("DesktopTaskListenerImpl: onEnterDesktopModeTransitionStarted with " +
+                        "duration= " +
+                        transitionDuration),
+                )
+                controller.get()?.isInDesktopMode = true
+                controller.get()?.notifyTaskbarDesktopModeListenersForEntry(transitionDuration)
             }
         }
 
+        override fun onExitDesktopModeTransitionStarted(transitionDuration: Int) {
+            MAIN_EXECUTOR.execute {
+                Log.d(
+                    TAG,
+                    ("DesktopTaskListenerImpl: onExitDesktopModeTransitionStarted with " +
+                        "duration= " +
+                        transitionDuration),
+                )
+                controller.get()?.isInDesktopMode = false
+                controller.get()?.notifyTaskbarDesktopModeListenersForExit(transitionDuration)
+            }
+        }
+
+        override fun onCanCreateDesksChanged(canCreateDesks: Boolean) {
+            MAIN_EXECUTOR.execute { controller.get()?.onCanCreateDesksChanged(canCreateDesks) }
+        }
+
         override fun onDeskAdded(displayId: Int, deskId: Int) {
-            Executors.MAIN_EXECUTOR.execute { controller.get()?.onDeskAdded(displayId, deskId) }
+            MAIN_EXECUTOR.execute { controller.get()?.onDeskAdded(displayId, deskId) }
         }
 
         override fun onDeskRemoved(displayId: Int, deskId: Int) {
-            Executors.MAIN_EXECUTOR.execute { controller.get()?.onDeskRemoved(displayId, deskId) }
+            MAIN_EXECUTOR.execute { controller.get()?.onDeskRemoved(displayId, deskId) }
         }
 
         override fun onActiveDeskChanged(displayId: Int, newActiveDesk: Int, oldActiveDesk: Int) {
-            Executors.MAIN_EXECUTOR.execute {
+            MAIN_EXECUTOR.execute {
                 controller.get()?.onActiveDeskChanged(displayId, newActiveDesk, oldActiveDesk)
             }
         }
@@ -533,7 +694,21 @@
          *
          * @param doesAnyTaskRequireTaskbarRounding whether task requires taskbar corner roundness.
          */
-        fun onTaskbarCornerRoundingUpdate(doesAnyTaskRequireTaskbarRounding: Boolean)
+        fun onTaskbarCornerRoundingUpdate(doesAnyTaskRequireTaskbarRounding: Boolean) {}
+
+        /**
+         * Callback for when user is exiting desktop mode.
+         *
+         * @param duration for exit transition
+         */
+        fun onExitDesktopMode(duration: Int) {}
+
+        /**
+         * Callback for when user is entering desktop mode.
+         *
+         * @param duration for enter transition
+         */
+        fun onEnterDesktopMode(duration: Int) {}
     }
 
     companion object {
@@ -542,5 +717,7 @@
 
         private const val TAG = "DesktopVisController"
         private const val DEBUG = false
+
+        public const val INACTIVE_DESK_ID = -1
     }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
index 5afc5ed..8555376 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
@@ -17,6 +17,7 @@
 
 import static com.android.launcher3.Flags.enableAltTabKqsOnConnectedDisplays;
 
+import android.app.ActivityManager;
 import android.content.ComponentName;
 import android.content.pm.ActivityInfo;
 import android.view.MotionEvent;
@@ -354,6 +355,27 @@
         }
     }
 
+    @VisibleForTesting
+    boolean isShownFromTaskbar() {
+        return isShown() && mQuickSwitchViewController.wasOpenedFromTaskbar();
+    }
+
+    @VisibleForTesting
+    boolean isShown() {
+        return mQuickSwitchViewController != null
+                && !mQuickSwitchViewController.isCloseAnimationRunning();
+    }
+
+    @VisibleForTesting
+    List<Integer> shownTaskIds() {
+        if (!isShown()) {
+            return Collections.emptyList();
+        }
+
+        return mTasks.stream().flatMap(
+                groupTask -> groupTask.getTasks().stream().map(task -> task.key.id)).toList();
+    }
+
     @Override
     public void dumpLogs(String prefix, PrintWriter pw) {
         pw.println(prefix + "KeyboardQuickSwitchController:");
@@ -423,7 +445,13 @@
             if (task == null) {
                 return false;
             }
-            int runningTaskId = ActivityManagerWrapper.getInstance().getRunningTask().taskId;
+            ActivityManager.RunningTaskInfo runningTaskInfo =
+                    ActivityManagerWrapper.getInstance().getRunningTask();
+            if (runningTaskInfo == null) {
+                return false;
+            }
+
+            int runningTaskId = runningTaskInfo.taskId;
             return task.containsTask(runningTaskId);
         }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java
index ce96556..f80dc90 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
@@ -188,8 +197,7 @@
 
 
         final boolean isLeftRightSplit = !splitBounds.appsStackedVertically;
-        final float leftOrTopTaskPercent = isLeftRightSplit
-                ? splitBounds.leftTaskPercent : splitBounds.topTaskPercent;
+        final float leftOrTopTaskPercent = splitBounds.getLeftTopTaskPercent();
 
         ConstraintLayout.LayoutParams leftTopParams = (ConstraintLayout.LayoutParams)
                 mThumbnailView1.getLayoutParams();
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/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
index 4143157..3f3700b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
@@ -246,7 +246,7 @@
 
         if (!ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue()
                 && mControllers.taskbarDesktopModeController
-                    .getAreDesktopTasksVisibleAndNotInOverview()) {
+                    .isInDesktopModeAndNotInOverview(mLauncher.getDisplayId())) {
             // TODO: b/333533253 - Remove after flag rollout
             isVisible = false;
         }
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 8880abd..57bcc14 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -18,6 +18,7 @@
 import static android.os.Trace.TRACE_TAG_APP;
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+import static android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
 import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
 import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR;
@@ -26,6 +27,7 @@
 
 import static androidx.annotation.VisibleForTesting.PACKAGE_PRIVATE;
 
+import static com.android.app.animation.Interpolators.LINEAR;
 import static com.android.launcher3.AbstractFloatingView.TYPE_ALL;
 import static com.android.launcher3.AbstractFloatingView.TYPE_ON_BOARD_POPUP;
 import static com.android.launcher3.AbstractFloatingView.TYPE_REBIND_SAFE;
@@ -46,8 +48,8 @@
 import static com.android.quickstep.util.AnimUtils.completeRunnableListCallback;
 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 +79,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 +301,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 +438,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);
 
     }
 
@@ -445,14 +450,25 @@
         mControllers.taskbarViewController.adjustTaskbarForBubbleBar();
     }
 
-    public void init(@NonNull TaskbarSharedState sharedState) {
+    /**
+     * Init of taskbar activity context.
+     * @param duration If duration is greater than 0, it will be used to create an animation
+ *                     for the taskbar create/recreate process.
+     */
+    public void init(@NonNull TaskbarSharedState sharedState, int duration) {
         mImeDrawsImeNavBar = getBoolByName(IME_DRAWS_IME_NAV_BAR_RES_NAME, getResources(), false);
         mLastRequestedNonFullscreenSize = getDefaultTaskbarWindowSize();
         mWindowLayoutParams = createAllWindowParams();
         mLastUpdatedLayoutParams = new WindowManager.LayoutParams();
 
+
+        AnimatorSet recreateAnim = null;
+        if (duration > 0) {
+            recreateAnim = onRecreateAnimation(duration);
+        }
+
         // Initialize controllers after all are constructed.
-        mControllers.init(sharedState);
+        mControllers.init(sharedState, recreateAnim);
         // This may not be necessary and can be reverted once we move towards recreating all
         // controllers without re-creating the window
         mControllers.rotationButtonController.onNavigationModeChanged(mNavMode.resValue);
@@ -480,6 +496,33 @@
         } else {
             notifyUpdateLayoutParams();
         }
+
+
+        if (recreateAnim != null) {
+            recreateAnim.start();
+        }
+    }
+
+    /**
+     * Create AnimatorSet for taskbar create/recreate animation. Further used in init
+     */
+    public AnimatorSet onRecreateAnimation(int duration) {
+        AnimatorSet animatorSet = new AnimatorSet();
+        animatorSet.setDuration(duration);
+        return animatorSet;
+    }
+
+    /**
+     * Called when we want destroy current taskbar with animation as part of recreate process.
+     */
+    public AnimatorSet onDestroyAnimation(int duration) {
+        mIsDestroyed = true;
+        AnimatorSet animatorSet = new AnimatorSet();
+        mControllers.taskbarViewController.onDestroyAnimation(animatorSet);
+        mControllers.taskbarDragLayerController.onDestroyAnimation(animatorSet);
+        animatorSet.setInterpolator(LINEAR);
+        animatorSet.setDuration(duration);
+        return animatorSet;
     }
 
     /**
@@ -511,6 +554,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).
@@ -868,7 +915,7 @@
 
     @Override
     public void onPopupVisibilityChanged(boolean isVisible) {
-        setTaskbarWindowFocusable(isVisible);
+        setTaskbarWindowFocusable(isVisible /* focusable */, false /* imeFocusable */);
     }
 
     @Override
@@ -1227,17 +1274,29 @@
     }
 
     /**
-     * Either adds or removes {@link WindowManager.LayoutParams#FLAG_NOT_FOCUSABLE} on the taskbar
-     * window.
+     * Sets whether the taskbar window should be focusable and IME focusable. This won't be IME
+     * focusable unless it is also focusable.
+     *
+     * @param focusable    whether it should be focusable.
+     * @param imeFocusable whether it should be IME focusable.
+     *
+     * @see WindowManager.LayoutParams#FLAG_NOT_FOCUSABLE
+     * @see WindowManager.LayoutParams#FLAG_ALT_FOCUSABLE_IM
      */
-    public void setTaskbarWindowFocusable(boolean focusable) {
+    public void setTaskbarWindowFocusable(boolean focusable, boolean imeFocusable) {
         if (isPhoneMode()) {
             return;
         }
         if (focusable) {
             mWindowLayoutParams.flags &= ~FLAG_NOT_FOCUSABLE;
+            if (imeFocusable) {
+                mWindowLayoutParams.flags &= ~FLAG_ALT_FOCUSABLE_IM;
+            } else {
+                mWindowLayoutParams.flags |= FLAG_ALT_FOCUSABLE_IM;
+            }
         } else {
             mWindowLayoutParams.flags |= FLAG_NOT_FOCUSABLE;
+            mWindowLayoutParams.flags &= ~FLAG_ALT_FOCUSABLE_IM;
         }
         notifyUpdateLayoutParams();
     }
@@ -1258,8 +1317,12 @@
     }
 
     /**
-     * Either adds or removes {@link WindowManager.LayoutParams#FLAG_NOT_FOCUSABLE} on the taskbar
-     * window. If we're now focusable, also move nav buttons to a separate window above IME.
+     * Sets whether the taskbar window should be focusable, as well as IME focusable. If we're now
+     * focusable, also move nav buttons to a separate window above IME.
+     *
+     * @param focusable whether it should be focusable.
+     *
+     * @see WindowManager.LayoutParams#FLAG_NOT_FOCUSABLE
      */
     public void setTaskbarWindowFocusableForIme(boolean focusable) {
         if (focusable) {
@@ -1267,7 +1330,7 @@
         } else {
             mControllers.navbarButtonsViewController.moveNavButtonsBackToTaskbarWindow();
         }
-        setTaskbarWindowFocusable(focusable);
+        setTaskbarWindowFocusable(focusable, true /* imeFocusable */);
     }
 
     /** Adds the given view to WindowManager with the provided LayoutParams (creates new window). */
@@ -1291,7 +1354,7 @@
 
     boolean areDesktopTasksVisible() {
         return mControllers != null
-                && mControllers.taskbarDesktopModeController.getAreDesktopTasksVisible();
+                && mControllers.taskbarDesktopModeController.isInDesktopMode(getDisplayId());
     }
 
     protected void onTaskbarIconClicked(View view) {
@@ -1533,9 +1596,9 @@
         return new RemoteTransition(
                 new DesktopAppLaunchTransition(
                         this,
-                        getMainExecutor(),
                         appLaunchType,
-                        cujType
+                        cujType,
+                        getMainExecutor()
                 ),
                 "TaskbarDesktopAppLaunch");
     }
@@ -1558,7 +1621,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;
         }
 
@@ -1699,7 +1765,7 @@
         }
         // There is no task associated with this launch - launch a new task through an intent
         ActivityOptionsWrapper opts = getActivityLaunchDesktopOptions();
-        if (enableStartLaunchTransitionFromTaskbarBugfix()) {
+        if (DesktopModeFlags.ENABLE_START_LAUNCH_TRANSITION_FROM_TASKBAR_BUGFIX.isTrue()) {
             mSysUiProxy.startLaunchIntentTransition(intent, opts.options.toBundle(), displayId);
         } else {
             startActivity(intent, opts.options.toBundle());
@@ -1956,6 +2022,10 @@
         return mControllers.taskbarStashController.isInApp();
     }
 
+    public boolean isInOverview() {
+        return mControllers.taskbarStashController.isInOverview();
+    }
+
     public boolean isInStashedLauncherState() {
         return mControllers.taskbarStashController.isInStashedLauncherState();
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt
index e44bce1..89cc991 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt
@@ -50,6 +50,8 @@
         }
 
     var isAnimatingPinning = false
+    var isAnimatingPersistentTaskbar = false
+    var isAnimatingTransientTaskbar = false
 
     val paint = Paint()
     private val strokePaint = Paint()
@@ -108,7 +110,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
@@ -144,7 +146,7 @@
     /** Draws the background with the given paint and height, on the provided canvas. */
     fun draw(canvas: Canvas) {
         if (isInSetup) return
-        val isTransientTaskbar = backgroundProgress == 0f
+        val isTransientTaskbar = DisplayController.isTransientTaskbar(context)
         canvas.save()
         if (!isTransientTaskbar || transientBackgroundBounds.isEmpty || isAnimatingPinning) {
             drawPersistentBackground(canvas)
@@ -158,7 +160,7 @@
     }
 
     private fun drawPersistentBackground(canvas: Canvas) {
-        if (isAnimatingPinning) {
+        if (isAnimatingPinning || isAnimatingPersistentTaskbar) {
             val persistentTaskbarHeight = maxPersistentTaskbarHeight * backgroundProgress
             canvas.translate(0f, canvas.height - persistentTaskbarHeight)
             // Draw the background behind taskbar content.
@@ -181,12 +183,13 @@
     private fun drawTransientBackground(canvas: Canvas) {
         val res = context.resources
         val transientTaskbarHeight = maxTransientTaskbarHeight * (1f - backgroundProgress)
+        val isAnimating = isAnimatingPinning || isAnimatingTransientTaskbar
         val heightProgressWhileAnimating =
-            if (isAnimatingPinning) transientTaskbarHeight else backgroundHeight
+            if (isAnimating) transientTaskbarHeight else backgroundHeight
 
         var progress = heightProgressWhileAnimating / maxTransientTaskbarHeight
         progress = Math.round(progress * 100f) / 100f
-        if (isAnimatingPinning) {
+        if (isAnimating) {
             var scale = transientTaskbarHeight / maxTransientTaskbarHeight
             scale = Math.round(scale * 100f) / 100f
             bottomMargin =
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
index af60f10..6ca9385 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
@@ -15,6 +15,7 @@
  */
 package com.android.launcher3.taskbar;
 
+import android.animation.AnimatorSet;
 import android.content.pm.ActivityInfo.Config;
 
 import androidx.annotation.NonNull;
@@ -149,15 +150,15 @@
      * TaskbarControllers instance, but should be careful to only access things that were created
      * in constructors for now, as some controllers may still be waiting for init().
      */
-    public void init(@NonNull TaskbarSharedState sharedState) {
+    public void init(@NonNull TaskbarSharedState sharedState, AnimatorSet startAnimation) {
         mAreAllControllersInitialized = false;
         mSharedState = sharedState;
 
         taskbarDragController.init(this);
         navbarButtonsViewController.init(this);
         rotationButtonController.init();
-        taskbarDragLayerController.init(this);
-        taskbarViewController.init(this);
+        taskbarDragLayerController.init(this, startAnimation);
+        taskbarViewController.init(this, startAnimation);
         taskbarScrimViewController.init(this);
         taskbarUnfoldAnimationController.init(this);
         taskbarKeyguardController.init(navbarButtonsViewController);
@@ -194,7 +195,8 @@
                 voiceInteractionWindowController
         };
 
-        if (taskbarDesktopModeController.getAreDesktopTasksVisibleAndNotInOverview()) {
+        if (taskbarDesktopModeController.isInDesktopModeAndNotInOverview(
+                taskbarActivityContext.getDisplayId())) {
             mCornerRoundness.value = taskbarDesktopModeController.getTaskbarCornerRoundness(
                     mSharedState.showCornerRadiusInDesktopMode);
         } else {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDesktopModeController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarDesktopModeController.kt
index cb399e8..ca8e4ca 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDesktopModeController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDesktopModeController.kt
@@ -30,11 +30,8 @@
     private lateinit var taskbarControllers: TaskbarControllers
     private lateinit var taskbarSharedState: TaskbarSharedState
 
-    val areDesktopTasksVisibleAndNotInOverview: Boolean
-        get() = desktopVisibilityController.areDesktopTasksVisibleAndNotInOverview()
-
-    val areDesktopTasksVisible: Boolean
-        get() = desktopVisibilityController.areDesktopTasksVisible()
+    val isInDesktopMode: Boolean
+        get() = desktopVisibilityController.isInDesktopMode
 
     fun init(controllers: TaskbarControllers, sharedState: TaskbarSharedState) {
         taskbarControllers = controllers
@@ -42,14 +39,20 @@
         desktopVisibilityController.registerTaskbarDesktopModeListener(this)
     }
 
+    fun isInDesktopMode(displayId: Int) = desktopVisibilityController.isInDesktopMode(displayId)
+
+    fun isInDesktopModeAndNotInOverview(displayId: Int) =
+        desktopVisibilityController.isInDesktopModeAndNotInOverview(displayId)
+
     override fun onTaskbarCornerRoundingUpdate(doesAnyTaskRequireTaskbarRounding: Boolean) {
+        if (taskbarControllers.taskbarActivityContext.isDestroyed) return
         taskbarSharedState.showCornerRadiusInDesktopMode = doesAnyTaskRequireTaskbarRounding
         val cornerRadius = getTaskbarCornerRoundness(doesAnyTaskRequireTaskbarRounding)
         taskbarControllers.taskbarCornerRoundness.animateToValue(cornerRadius).start()
     }
 
     fun shouldShowDesktopTasksInTaskbar(): Boolean {
-        return desktopVisibilityController.areDesktopTasksVisible() ||
+        return isInDesktopMode(context.displayId) ||
             DisplayController.showDesktopTaskbarForFreeformDisplay(context) ||
             (DisplayController.showLockedTaskbarOnHome(context) &&
                 taskbarControllers.taskbarStashController.isOnHome)
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
index f36c481..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;
@@ -347,7 +349,7 @@
         // Pre-drag has ended, start the global system drag.
         if (mDisallowGlobalDrag
                 || mControllers.taskbarDesktopModeController
-                    .getAreDesktopTasksVisibleAndNotInOverview()) {
+                    .isInDesktopModeAndNotInOverview(mActivity.getDisplayId())) {
             AbstractFloatingView.closeAllOpenViewsExcept(mActivity, TYPE_TASKBAR_ALL_APPS);
             return;
         }
@@ -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/TaskbarDragLayer.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
index 59ef577..4dbad8c 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
@@ -186,6 +186,7 @@
 
     @Override
     protected void dispatchDraw(Canvas canvas) {
+        if (mContainer.isDestroyed()) return;
         float backgroundHeight = mControllerCallbacks.getTaskbarBackgroundHeight()
                 * (1f - mTaskbarBackgroundOffset);
         mBackgroundRenderer.setBackgroundHeight(backgroundHeight);
@@ -286,6 +287,21 @@
     }
 
     /**
+     * Sets animation boolean when only animating persistent taskbar.
+     */
+    public void setIsAnimatingPersistentTaskbarBackground(boolean animatingPersistentTaskbarBg) {
+        mBackgroundRenderer.setAnimatingPersistentTaskbar(animatingPersistentTaskbarBg);
+    }
+
+    /**
+     * Sets animation boolean when only animating transient taskbar.
+     */
+    public void setIsAnimatingTransientTaskbarBackground(boolean animatingTransientTaskbarBg) {
+        mBackgroundRenderer.setAnimatingTransientTaskbar(animatingTransientTaskbarBg);
+    }
+
+
+    /**
      * Sets the width percentage to inset the transient taskbar's background from the left and from
      * the right.
      */
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java
index 68c252a..55ecc37 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java
@@ -15,9 +15,12 @@
  */
 package com.android.launcher3.taskbar;
 
+import static com.android.app.animation.Interpolators.EMPHASIZED;
 import static com.android.launcher3.taskbar.TaskbarPinningController.PINNING_PERSISTENT;
 import static com.android.launcher3.taskbar.TaskbarPinningController.PINNING_TRANSIENT;
 
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
 import android.content.res.Resources;
 import android.graphics.Canvas;
 import android.graphics.Point;
@@ -29,6 +32,7 @@
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.R;
 import com.android.launcher3.anim.AnimatedFloat;
+import com.android.launcher3.anim.AnimatorListeners;
 import com.android.launcher3.util.DimensionUtils;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.MultiPropertyFactory.MultiProperty;
@@ -58,6 +62,8 @@
     private final AnimatedFloat mImeBgTaskbar = new AnimatedFloat(this::updateBackgroundAlpha);
     private final AnimatedFloat mAssistantBgTaskbar = new AnimatedFloat(
             this::updateBackgroundAlpha);
+    private final AnimatedFloat mBgTaskbarRecreate = new AnimatedFloat(
+            this::updateBackgroundAlpha);
     // Used to hide our background color when someone else (e.g. ScrimView) is handling it.
     private final AnimatedFloat mBgOverride = new AnimatedFloat(this::updateBackgroundAlpha);
 
@@ -88,7 +94,10 @@
         mFolderMargin = resources.getDimensionPixelSize(R.dimen.taskbar_folder_margin);
     }
 
-    public void init(TaskbarControllers controllers) {
+    /**
+     * Init of taskbar drag layer controller
+     */
+    public void init(TaskbarControllers controllers, AnimatorSet startAnimation) {
         mControllers = controllers;
         mTaskbarStashViaTouchController = new TaskbarStashViaTouchController(mControllers);
         mTaskbarDragLayer.init(new TaskbarDragLayerCallbacks());
@@ -96,15 +105,45 @@
         mOnBackgroundNavButtonColorIntensity = mControllers.navbarButtonsViewController
                 .getOnTaskbarBackgroundNavButtonColorOverride();
 
-        mTaskbarBackgroundProgress.updateValue(DisplayController.isTransientTaskbar(mActivity)
-                ? PINNING_TRANSIENT
-                : PINNING_PERSISTENT);
+
+        if (startAnimation != null) {
+            // set taskbar background render animation boolean
+            if (DisplayController.isTransientTaskbar(mActivity)) {
+                mTaskbarDragLayer.setIsAnimatingTransientTaskbarBackground(true);
+            } else {
+                mTaskbarDragLayer.setIsAnimatingPersistentTaskbarBackground(true);
+            }
+
+            float desiredValue = DisplayController.isTransientTaskbar(mActivity)
+                    ? PINNING_TRANSIENT
+                    : PINNING_PERSISTENT;
+
+            float nonDesiredvalue = !DisplayController.isTransientTaskbar(mActivity)
+                    ? PINNING_TRANSIENT
+                    : PINNING_PERSISTENT;
+
+            ObjectAnimator objectAnimator = mTaskbarBackgroundProgress.animateToValue(
+                    nonDesiredvalue, desiredValue);
+            objectAnimator.setInterpolator(EMPHASIZED);
+            startAnimation.play(objectAnimator);
+            startAnimation.addListener(AnimatorListeners.forEndCallback(()-> {
+                // reset taskbar background render animation boolean
+                mTaskbarDragLayer.setIsAnimatingPersistentTaskbarBackground(false);
+                mTaskbarDragLayer.setIsAnimatingTransientTaskbarBackground(false);
+            }));
+
+        } else {
+            mTaskbarBackgroundProgress.updateValue(DisplayController.isTransientTaskbar(mActivity)
+                    ? PINNING_TRANSIENT
+                    : PINNING_PERSISTENT);
+        }
 
         mBgTaskbar.value = 1;
         mKeyguardBgTaskbar.value = 1;
         mNotificationShadeBgTaskbar.value = 1;
         mImeBgTaskbar.value = 1;
         mAssistantBgTaskbar.value = 1;
+        mBgTaskbarRecreate.value = 1;
         mBgOverride.value = 1;
         updateBackgroundAlpha();
 
@@ -112,6 +151,13 @@
         updateTaskbarAlpha();
     }
 
+    /**
+     * Called when destroying Taskbar with animation.
+     */
+    public void onDestroyAnimation(AnimatorSet animatorSet) {
+        animatorSet.play(mBgTaskbarRecreate.animateToValue(0f));
+    }
+
     public void onDestroy() {
         mTaskbarDragLayer.onDestroy();
     }
@@ -172,14 +218,14 @@
     }
 
     private void updateBackgroundAlpha() {
-        if (mActivity.isPhoneMode()) {
+        if (mActivity.isPhoneMode() || mActivity.isDestroyed()) {
             return;
         }
 
         final float bgNavbar = mBgNavbar.value;
         final float bgTaskbar = mBgTaskbar.value * mKeyguardBgTaskbar.value
                 * mNotificationShadeBgTaskbar.value * mImeBgTaskbar.value
-                * mAssistantBgTaskbar.value;
+                * mAssistantBgTaskbar.value * mBgTaskbarRecreate.value;
         mLastSetBackgroundAlpha = mBgOverride.value * Math.max(bgNavbar, bgTaskbar);
         mBackgroundRendererAlpha.setValue(mLastSetBackgroundAlpha);
 
@@ -266,6 +312,7 @@
         pw.println(prefix + "\t\tmNotificationShadeBgTaskbar=" + mNotificationShadeBgTaskbar.value);
         pw.println(prefix + "\t\tmImeBgTaskbar=" + mImeBgTaskbar.value);
         pw.println(prefix + "\t\tmAssistantBgTaskbar=" + mAssistantBgTaskbar.value);
+        pw.println(prefix + "\t\tmBgTaskbarRecreate=" + mBgTaskbarRecreate.value);
     }
 
     /**
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt
index 26a552e..5d1288c 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt
@@ -39,6 +39,7 @@
 import com.airbnb.lottie.LottieAnimationView
 import com.android.launcher3.LauncherPrefs
 import com.android.launcher3.R
+import com.android.launcher3.RemoveAnimationSettingsTracker
 import com.android.launcher3.Utilities
 import com.android.launcher3.config.FeatureFlags.enableTaskbarPinning
 import com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_EDU_OPEN
@@ -128,6 +129,26 @@
         activityContext.dragLayer.post { maybeShowSearchEdu() }
     }
 
+    /**
+     * Turns off auto play of lottie animations if user has opted to remove animation else attaches
+     * click listener to allow user to play or pause animations.
+     */
+    fun handleEduAnimations(animationViews: List<LottieAnimationView>) {
+        for (animationView in animationViews) {
+            if (
+                RemoveAnimationSettingsTracker.INSTANCE.get(animationView.context)
+                    .isRemoveAnimationEnabled()
+            ) {
+                animationView.pauseAnimation()
+            } else {
+                animationView.setOnClickListener {
+                    if (animationView.isAnimating) animationView.pauseAnimation()
+                    else animationView.playAnimation()
+                }
+            }
+        }
+    }
+
     /** Shows swipe EDU tooltip if it is the current [tooltipStep]. */
     fun maybeShowSwipeEdu() {
         if (
@@ -141,7 +162,13 @@
         tooltipStep = TOOLTIP_STEP_FEATURES
         inflateTooltip(R.layout.taskbar_edu_swipe)
         tooltip?.run {
-            requireViewById<LottieAnimationView>(R.id.swipe_animation).supportLightTheme()
+            TypefaceUtils.setTypeface(
+                requireViewById(R.id.taskbar_edu_title),
+                TypefaceUtils.FONT_FAMILY_HEADLINE_SMALL_EMPHASIZED,
+            )
+            val swipeAnimation = requireViewById<LottieAnimationView>(R.id.swipe_animation)
+            swipeAnimation.supportLightTheme()
+            handleEduAnimations(listOf(swipeAnimation))
             show()
         }
     }
@@ -170,6 +197,7 @@
             splitscreenAnim.supportLightTheme()
             suggestionsAnim.supportLightTheme()
             pinningAnim.supportLightTheme()
+            handleEduAnimations(listOf(splitscreenAnim, suggestionsAnim, pinningAnim))
             if (DisplayController.isTransientTaskbar(activityContext)) {
                 splitscreenAnim.setAnimation(R.raw.taskbar_edu_splitscreen_transient)
                 suggestionsAnim.setAnimation(R.raw.taskbar_edu_suggestions_transient)
@@ -180,6 +208,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> {
@@ -228,9 +273,19 @@
 
         tooltip?.run {
             allowTouchDismissal = true
-            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,
+            )
 
+            val pinningAnim =
+                requireViewById<LottieAnimationView>(R.id.standalone_pinning_animation)
+            pinningAnim.supportLightTheme()
+            handleEduAnimations(listOf(pinningAnim))
             updateLayoutParams<BaseDragLayer.LayoutParams> {
                 if (DisplayController.isTransientTaskbar(activityContext)) {
                     bottomMargin += activityContext.deviceProfile.taskbarHeight
@@ -274,8 +329,17 @@
         inflateTooltip(R.layout.taskbar_edu_search)
         tooltip?.run {
             allowTouchDismissal = true
-            requireViewById<LottieAnimationView>(R.id.search_edu_animation).supportLightTheme()
+            val searchEdu = requireViewById<LottieAnimationView>(R.id.search_edu_animation)
+            searchEdu.supportLightTheme()
+            handleEduAnimations(listOf(searchEdu))
             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/TaskbarLauncherStateController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
index cada5a3..10eb64a 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
@@ -223,9 +223,13 @@
                     updateStateForFlag(FLAG_LAUNCHER_IN_STATE_TRANSITION, true);
                     if (!mShouldDelayLauncherStateAnim) {
                         if (toState == LauncherState.NORMAL) {
-                            applyState(QuickstepTransitionManager.getTaskbarToHomeDuration(
+                            boolean isPinnedTaskbarAndNotInDesktopMode =
                                     DisplayController.isPinnedTaskbar(
-                                            mControllers.taskbarActivityContext)));
+                                            mControllers.taskbarActivityContext)
+                                            && !DisplayController.isInDesktopMode(
+                                            mControllers.taskbarActivityContext);
+                            applyState(QuickstepTransitionManager.getTaskbarToHomeDuration(
+                                    isPinnedTaskbarAndNotInDesktopMode));
                         } else {
                             applyState();
                         }
@@ -631,7 +635,8 @@
 
         float cornerRoundness = isInLauncher ? 0 : 1;
 
-        if (mControllers.taskbarDesktopModeController.getAreDesktopTasksVisibleAndNotInOverview()
+        if (mControllers.taskbarDesktopModeController.isInDesktopModeAndNotInOverview(
+                mControllers.taskbarActivityContext.getDisplayId())
                 && mControllers.getSharedState() != null) {
             cornerRoundness =
                     mControllers.taskbarDesktopModeController.getTaskbarCornerRoundness(
@@ -679,8 +684,11 @@
         } else if (mIconAlignment.isAnimatingToValue(toAlignment)
                 || mIconAlignment.isSettledOnValue(toAlignment)) {
             // Already at desired value, but make sure we run the callback at the end.
-            animatorSet.addListener(AnimatorListeners.forEndCallback(
-                    this::onIconAlignmentRatioChanged));
+            animatorSet.addListener(AnimatorListeners.forEndCallback(() -> {
+                if (!mIconAlignment.isAnimating()) {
+                    onIconAlignmentRatioChanged();
+                }
+            }));
         } else {
             mIconAlignment.cancelAnimation();
             ObjectAnimator iconAlignAnim = mIconAlignment
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
index f704254..943c44e 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
@@ -16,24 +16,27 @@
 package com.android.launcher3.taskbar;
 
 import static android.content.Context.RECEIVER_NOT_EXPORTED;
+import static android.content.Context.RECEIVER_EXPORTED;
 import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR;
 import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL;
 
 import static com.android.launcher3.BaseActivity.EVENT_DESTROYED;
+import static com.android.launcher3.Flags.enableGrowthNudge;
 import static com.android.launcher3.Flags.enableUnfoldStateAnimation;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_TASKBAR_NAVBAR_UNIFICATION;
 import static com.android.launcher3.config.FeatureFlags.enableTaskbarNoRecreate;
 import static com.android.launcher3.util.DisplayController.CHANGE_DENSITY;
 import static com.android.launcher3.util.DisplayController.CHANGE_DESKTOP_MODE;
 import static com.android.launcher3.util.DisplayController.CHANGE_NAVIGATION_MODE;
+import static com.android.launcher3.util.DisplayController.CHANGE_SHOW_LOCKED_TASKBAR;
 import static com.android.launcher3.util.DisplayController.CHANGE_TASKBAR_PINNING;
-import static com.android.launcher3.util.DisplayController.TASKBAR_NOT_DESTROYED_TAG;
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 import static com.android.launcher3.util.FlagDebugUtils.formatFlagChange;
+import static com.android.launcher3.taskbar.growth.GrowthConstants.BROADCAST_SHOW_NUDGE;
 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.animation.AnimatorSet;
 import android.annotation.SuppressLint;
 import android.app.PendingIntent;
 import android.content.ComponentCallbacks;
@@ -46,6 +49,7 @@
 import android.os.Handler;
 import android.os.Trace;
 import android.provider.Settings;
+import android.util.ArraySet;
 import android.util.Log;
 import android.util.SparseArray;
 import android.util.SparseBooleanArray;
@@ -53,15 +57,19 @@
 import android.view.MotionEvent;
 import android.view.WindowManager;
 import android.widget.FrameLayout;
+import android.window.DesktopExperienceFlags;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.anim.AnimatorListeners;
 import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.statehandlers.DesktopVisibilityController;
 import com.android.launcher3.statemanager.StatefulActivity;
 import com.android.launcher3.taskbar.TaskbarNavButtonController.TaskbarNavButtonCallbacks;
 import com.android.launcher3.taskbar.unfold.NonDestroyableScopedUnfoldTransitionProgressProvider;
@@ -75,14 +83,20 @@
 import com.android.quickstep.fallback.window.RecentsDisplayModel;
 import com.android.quickstep.fallback.window.RecentsWindowManager;
 import com.android.quickstep.util.ContextualSearchInvoker;
+import com.android.quickstep.util.GroupTask;
 import com.android.quickstep.views.RecentsViewContainer;
+import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.statusbar.phone.BarTransitions;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
+import com.android.systemui.shared.system.TaskStackChangeListener;
+import com.android.systemui.shared.system.TaskStackChangeListeners;
 import com.android.systemui.unfold.UnfoldTransitionProgressProvider;
 import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider;
 
 import java.io.PrintWriter;
+import java.util.Set;
 import java.util.StringJoiner;
 
 /**
@@ -91,9 +105,10 @@
 public class TaskbarManager {
     private static final String TAG = "TaskbarManager";
     private static final boolean DEBUG = false;
-    // TODO(b/382378283) remove all logs with this tag
-    public static final String NULL_TASKBAR_ROOT_LAYOUT_TAG = "b/382378283";
-    public static final String ILLEGAL_ARGUMENT_WM_ADD_VIEW = "b/391653300";
+    private static final int TASKBAR_DESTROY_DURATION = 100;
+
+    // TODO: b/397738606  - Remove all logs with this tag after the growth framework is integrated.
+    public static final String GROWTH_FRAMEWORK_TAG = "Growth Framework";
 
     /**
      * All the configurations which do not initiate taskbar recreation.
@@ -118,14 +133,14 @@
             Settings.Secure.NAV_BAR_KIDS_MODE);
 
     private final Context mBaseContext;
+    private final 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 final WindowManager mPrimaryWindowManager;
+    private TaskbarNavButtonController mPrimaryNavButtonController;
+    private ComponentCallbacks mPrimaryComponentCallbacks;
 
-    private final SimpleBroadcastReceiver mShutdownReceiver =
-            new SimpleBroadcastReceiver(UI_HELPER_EXECUTOR, i -> destroyAllTaskbars());
+    private final SimpleBroadcastReceiver mShutdownReceiver;
 
     // The source for this provider is set when Launcher is available
     // We use 'non-destroyable' version here so the original provider won't be destroyed
@@ -141,6 +156,13 @@
     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<>();
+    /** DisplayId - {@link DeviceProfile} map for Connected Display. */
+    private final SparseArray<DeviceProfile> mExternalDeviceProfiles = new SparseArray<>();
     private StatefulActivity mActivity;
     private RecentsViewContainer mRecentsViewContainer;
 
@@ -159,32 +181,157 @@
     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) {
-                recreateTaskbar();
+                    | CHANGE_TASKBAR_PINNING | CHANGE_SHOW_LOCKED_TASKBAR)) != 0) {
+                debugTaskbarManager("onDisplayInfoChanged: Recreating Taskbar!",
+                        context.getDisplayId());
+                TaskbarActivityContext taskbarActivityContext = getCurrentActivityContext();
+                if ((flags & CHANGE_SHOW_LOCKED_TASKBAR) != 0) {
+                    recreateTaskbars();
+                } else if ((flags & CHANGE_DESKTOP_MODE) != 0) {
+                    // Only Handles Special Exit Cases for Desktop Mode Taskbar Recreation.
+                    if (taskbarActivityContext != null
+                            && !DesktopVisibilityController.INSTANCE.get(taskbarActivityContext)
+                            .isInDesktopMode()
+                            && !DisplayController.showLockedTaskbarOnHome(context)) {
+                        recreateTaskbars();
+                    }
+                } else {
+                    recreateTaskbars();
+                }
             }
         }
     }
-    private final SettingsCache.OnChangeListener mOnSettingsChangeListener = c -> recreateTaskbar();
+
+    private final SettingsCache.OnChangeListener mOnSettingsChangeListener = c -> {
+        debugPrimaryTaskbar("Settings changed! Recreating Taskbar!");
+        recreateTaskbars();
+    };
+
+    private final PerceptibleTaskListener mTaskStackListener;
+
+    private class PerceptibleTaskListener implements TaskStackChangeListener {
+        private ArraySet<Integer> mPerceptibleTasks = new ArraySet<Integer>();
+
+        @Override
+        public void onTaskMovedToFront(int taskId) {
+            if (mPerceptibleTasks.contains(taskId)) {
+                return;
+            }
+
+            // This listens to any Task, so we filter them by the ones shown in the launcher.
+            // For Tasks restored after startup, they will by default not be Perceptible, and no
+            // need to until user interacts with it by bringing it to the foreground.
+            for (int i = 0; i < mTaskbars.size(); i++) {
+                // get pinned tasks
+                Set<Integer> taskbarPinnedTasks =
+                        mTaskbars.valueAt(i).getControllers().taskbarViewController
+                                .getTaskIdsForPinnedApps();
+
+                // mark as perceptible if the foregrounded task is in the list of apps shown in
+                // the launcher.
+                if (taskbarPinnedTasks.contains(taskId)
+                        && ActivityManagerWrapper.getInstance()
+                        .setTaskIsPerceptible(taskId, true)
+                ) {
+                    mPerceptibleTasks.add(taskId);
+                }
+            }
+        }
+
+        /**
+         * Launcher also can display recently launched tasks that are not pinned. Also add
+         * these as perceptible
+         */
+        @Override
+        public void onRecentTaskListUpdated() {
+            for (int i = 0; i < mTaskbars.size(); i++) {
+                for (GroupTask gTask : mTaskbars.valueAt(i).getControllers()
+                        .taskbarRecentAppsController.getShownTasks()) {
+                    for (Task task : gTask.getTasks()) {
+                        int taskId = task.key.id;
+
+                        if (!mPerceptibleTasks.contains(taskId)) {
+                            ActivityManagerWrapper.getInstance()
+                                    .setTaskIsPerceptible(taskId, true);
+                            mPerceptibleTasks.add(taskId);
+                        }
+                    }
+                }
+            }
+        }
+
+        @Override
+        public void onTaskRemoved(int taskId) {
+            mPerceptibleTasks.remove(taskId);
+        }
+    }
+
+    private final DesktopVisibilityController.TaskbarDesktopModeListener
+            mTaskbarDesktopModeListener =
+            new DesktopVisibilityController.TaskbarDesktopModeListener() {
+                @Override
+                public void onExitDesktopMode(int duration) {
+                    for (int taskbarIndex = 0; taskbarIndex < mTaskbars.size(); taskbarIndex++) {
+                        int displayId = mTaskbars.keyAt(taskbarIndex);
+                        TaskbarActivityContext taskbarActivityContext = getTaskbarForDisplay(
+                                displayId);
+                        if (taskbarActivityContext != null
+                                && !taskbarActivityContext.isInOverview()) {
+                            AnimatorSet animatorSet = taskbarActivityContext.onDestroyAnimation(
+                                    TASKBAR_DESTROY_DURATION);
+                            animatorSet.addListener(AnimatorListeners.forEndCallback(
+                                    () -> recreateTaskbarForDisplay(getDefaultDisplayId(),
+                                            duration)));
+                            animatorSet.start();
+                        }
+                    }
+                }
+
+                @Override
+                public void onEnterDesktopMode(int duration) {
+                    for (int taskbarIndex = 0; taskbarIndex < mTaskbars.size(); taskbarIndex++) {
+                        int displayId = mTaskbars.keyAt(taskbarIndex);
+                        TaskbarActivityContext taskbarActivityContext = getTaskbarForDisplay(
+                                displayId);
+                        AnimatorSet animatorSet = taskbarActivityContext.onDestroyAnimation(
+                                TASKBAR_DESTROY_DURATION);
+                        animatorSet.addListener(AnimatorListeners.forEndCallback(
+                                () -> recreateTaskbarForDisplay(getDefaultDisplayId(), duration)));
+                        animatorSet.start();
+                    }
+                }
+
+                @Override
+                public void onTaskbarCornerRoundingUpdate(
+                        boolean doesAnyTaskRequireTaskbarRounding) {
+                    //NO-OP
+                }
+            };
+
 
     private boolean mUserUnlocked = false;
 
-    private final SimpleBroadcastReceiver mTaskbarBroadcastReceiver =
-            new SimpleBroadcastReceiver(UI_HELPER_EXECUTOR, this::showTaskbarFromBroadcast);
+    private final SimpleBroadcastReceiver mTaskbarBroadcastReceiver;
+
+    private final SimpleBroadcastReceiver mGrowthBroadcastReceiver;
 
     private final AllAppsActionManager mAllAppsActionManager;
     private final RecentsDisplayModel mRecentsDisplayModel;
@@ -193,23 +340,24 @@
         @Override
         public void run() {
             int displayId = getDefaultDisplayId();
+            debugTaskbarManager("onActivityDestroyed:", displayId);
             if (mActivity != null) {
                 displayId = mActivity.getDisplayId();
                 mActivity.removeOnDeviceProfileChangeListener(
                         mDebugActivityDeviceProfileChanged);
-                Log.d(TASKBAR_NOT_DESTROYED_TAG,
-                        "unregistering activity lifecycle callbacks from "
-                                + "onActivityDestroyed.");
+                debugTaskbarManager("onActivityDestroyed: unregistering callbacks", displayId);
                 mActivity.removeEventCallback(EVENT_DESTROYED, this);
             }
             if (mActivity == mRecentsViewContainer) {
                 mRecentsViewContainer = null;
             }
             mActivity = null;
-            debugWhyTaskbarNotDestroyed("clearActivity");
             TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId);
             if (taskbar != null) {
+                debugTaskbarManager("onActivityDestroyed: setting taskbarUIController", displayId);
                 taskbar.setUIController(TaskbarUIController.DEFAULT);
+            } else {
+                debugTaskbarManager("onActivityDestroyed: taskbar is null!", displayId);
             }
             mUnfoldProgressProvider.setSourceProvider(null);
         }
@@ -219,28 +367,27 @@
             new UnfoldTransitionProgressProvider.TransitionProgressListener() {
                 @Override
                 public void onTransitionStarted() {
-                    Log.d(TASKBAR_NOT_DESTROYED_TAG,
-                            "fold/unfold transition started getting called.");
+                    debugPrimaryTaskbar("fold/unfold transition started getting called.");
                 }
 
                 @Override
                 public void onTransitionProgress(float progress) {
-                    Log.d(TASKBAR_NOT_DESTROYED_TAG,
-                            "fold/unfold transition progress : " + progress);
+                    debugPrimaryTaskbar(
+                            "fold/unfold transition progress getting called. | progress="
+                                    + progress);
                 }
 
                 @Override
                 public void onTransitionFinishing() {
-                    Log.d(TASKBAR_NOT_DESTROYED_TAG,
+                    debugPrimaryTaskbar(
                             "fold/unfold transition finishing getting called.");
 
                 }
 
                 @Override
                 public void onTransitionFinished() {
-                    Log.d(TASKBAR_NOT_DESTROYED_TAG,
+                    debugPrimaryTaskbar(
                             "fold/unfold transition finished getting called.");
-
                 }
             };
 
@@ -252,21 +399,43 @@
             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();
+        debugPrimaryTaskbar("TaskbarManager constructor");
+        mPrimaryWindowContext = createWindowContext(primaryDisplayId);
+        mPrimaryWindowManager = mPrimaryWindowContext.getSystemService(WindowManager.class);
+        DesktopVisibilityController.INSTANCE.get(
+                mPrimaryWindowContext).registerTaskbarDesktopModeListener(
+                mTaskbarDesktopModeListener);
+        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.register(mPrimaryWindowContext, Intent.ACTION_SHUTDOWN);
+        mShutdownReceiver =
+                new SimpleBroadcastReceiver(
+                        mPrimaryWindowContext, UI_HELPER_EXECUTOR, i -> destroyAllTaskbars());
+        mTaskbarBroadcastReceiver =
+                new SimpleBroadcastReceiver(mPrimaryWindowContext,
+                        UI_HELPER_EXECUTOR, this::showTaskbarFromBroadcast);
+
+        mShutdownReceiver.register(Intent.ACTION_SHUTDOWN);
+        if (enableGrowthNudge()) {
+            // TODO: b/397739323 - Add permission to limit access to Growth Framework.
+            mGrowthBroadcastReceiver =
+                    new SimpleBroadcastReceiver(
+                            mPrimaryWindowContext, UI_HELPER_EXECUTOR, this::showGrowthNudge);
+            mGrowthBroadcastReceiver.register(RECEIVER_EXPORTED,
+                    BROADCAST_SHOW_NUDGE);
+        } else {
+            mGrowthBroadcastReceiver = null;
+        }
         UI_HELPER_EXECUTOR.execute(() -> {
             mSharedState.taskbarSystemActionPendingIntent = PendingIntent.getBroadcast(
                     mPrimaryWindowContext,
@@ -274,111 +443,44 @@
                     new Intent(ACTION_SHOW_TASKBAR).setPackage(
                             mPrimaryWindowContext.getPackageName()),
                     PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
-            mTaskbarBroadcastReceiver.register(
-                    mPrimaryWindowContext, RECEIVER_NOT_EXPORTED, ACTION_SHOW_TASKBAR);
+            mTaskbarBroadcastReceiver.register(RECEIVER_NOT_EXPORTED, ACTION_SHOW_TASKBAR);
         });
 
-        debugWhyTaskbarNotDestroyed("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() { }
-        };
+        if (ActivityManagerWrapper.usePerceptibleTasks(getPrimaryWindowContext())) {
+            mTaskStackListener = new PerceptibleTaskListener();
+            TaskStackChangeListeners.getInstance().registerTaskStackListener(mTaskStackListener);
+        } else {
+            mTaskStackListener = null;
+        }
+        recreateTaskbars();
+        debugPrimaryTaskbar("TaskbarManager created");
     }
 
     private void destroyAllTaskbars() {
+        debugPrimaryTaskbar("destroyAllTaskbars");
         for (int i = 0; i < mTaskbars.size(); i++) {
             int displayId = mTaskbars.keyAt(i);
+            debugTaskbarManager("destroyAllTaskbars: call destroyTaskbarForDisplay", displayId);
             destroyTaskbarForDisplay(displayId);
+
+            debugTaskbarManager("destroyAllTaskbars: call removeTaskbarRootViewFromWindow",
+                    displayId);
             removeTaskbarRootViewFromWindow(displayId);
         }
     }
 
-    private void destroyDefaultTaskbar() {
-        destroyTaskbarForDisplay(getDefaultDisplayId());
-    }
-
     private void destroyTaskbarForDisplay(int displayId) {
-        Log.d(ILLEGAL_ARGUMENT_WM_ADD_VIEW, "destroyTaskbarForDisplay: " + displayId);
+        debugTaskbarManager("destroyTaskbarForDisplay", displayId);
         TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId);
-        debugWhyTaskbarNotDestroyed("destroyTaskbarForDisplay: " + taskbar, displayId);
         if (taskbar != null) {
             taskbar.onDestroy();
             // remove all defaults that we store
             removeTaskbarFromMap(displayId);
+        } else {
+            debugTaskbarManager("destroyTaskbarForDisplay: taskbar is NULL!", displayId);
         }
-        // make this display-specific
-        DeviceProfile dp = mUserUnlocked ?
-                LauncherAppState.getIDP(getWindowContext(displayId)).getDeviceProfile(
-                        getWindowContext(displayId)) : null;
+
+        DeviceProfile dp = getDeviceProfile(displayId);
         if (dp == null || !isTaskbarEnabled(dp)) {
             removeTaskbarRootViewFromWindow(displayId);
         }
@@ -388,13 +490,24 @@
      * Show Taskbar upon receiving broadcast
      */
     private void showTaskbarFromBroadcast(Intent intent) {
+        debugPrimaryTaskbar("destroyTaskbarForDisplay");
         // TODO: make this code displayId specific
         TaskbarActivityContext taskbar = getTaskbarForDisplay(getDefaultDisplayId());
-        if (ACTION_SHOW_TASKBAR.equals(intent.getAction()) && taskbar != null) {
+        if (ACTION_SHOW_TASKBAR.equals(intent.getAction())) {
             taskbar.showTaskbarFromBroadcast();
         }
     }
 
+    private void showGrowthNudge(Intent intent) {
+        if (!enableGrowthNudge()) {
+            return;
+        }
+        if (BROADCAST_SHOW_NUDGE.equals(intent.getAction())) {
+            // TODO: b/397738606 - extract the details and create a nudge payload.
+            Log.d(GROWTH_FRAMEWORK_TAG, "Intent received");
+        }
+    }
+
     /**
      * Toggles All Apps for Taskbar or Launcher depending on the current state.
      */
@@ -424,26 +537,32 @@
      * Called when the user is unlocked
      */
     public void onUserUnlocked() {
+        debugPrimaryTaskbar("onUserUnlocked");
         mUserUnlocked = true;
         DisplayController.INSTANCE.get(mPrimaryWindowContext).addChangeListener(
                 mRecreationListener);
-        recreateTaskbar();
-        addTaskbarRootViewToWindow(getDefaultDisplayId());
+        debugPrimaryTaskbar("onUserUnlocked: recreating all taskbars!");
+        recreateTaskbars();
+        for (int i = 0; i < mTaskbars.size(); i++) {
+            int displayId = mTaskbars.keyAt(i);
+            debugTaskbarManager("onUserUnlocked: addTaskbarRootViewToWindow()", displayId);
+            addTaskbarRootViewToWindow(displayId);
+        }
     }
 
     /**
      * Sets a {@link StatefulActivity} to act as taskbar callback
      */
     public void setActivity(@NonNull StatefulActivity activity) {
+        debugPrimaryTaskbar("setActivity: mActivity=" + mActivity);
         if (mActivity == activity) {
+            debugPrimaryTaskbar("setActivity: No need to set activity!");
             return;
         }
         removeActivityCallbacksAndListeners();
         mActivity = activity;
-        debugWhyTaskbarNotDestroyed("Set mActivity=" + mActivity);
         mActivity.addOnDeviceProfileChangeListener(mDebugActivityDeviceProfileChanged);
-        Log.d(TASKBAR_NOT_DESTROYED_TAG,
-                "registering activity lifecycle callbacks from setActivity().");
+        debugPrimaryTaskbar("setActivity: registering activity lifecycle callbacks.");
         mActivity.addEventCallback(EVENT_DESTROYED, mActivityOnDestroyCallback);
         UnfoldTransitionProgressProvider unfoldTransitionProgressProvider =
                 getUnfoldTransitionProgressProviderForActivity(activity);
@@ -461,6 +580,7 @@
      * Sets the current RecentsViewContainer, from which we create a TaskbarUIController.
      */
     public void setRecentsViewContainer(@NonNull RecentsViewContainer recentsViewContainer) {
+        debugPrimaryTaskbar("setRecentsViewContainer");
         if (mRecentsViewContainer == recentsViewContainer) {
             return;
         }
@@ -484,18 +604,20 @@
      */
     private UnfoldTransitionProgressProvider getUnfoldTransitionProgressProviderForActivity(
             StatefulActivity activity) {
+        debugPrimaryTaskbar("getUnfoldTransitionProgressProviderForActivity");
         if (!enableUnfoldStateAnimation()) {
             if (activity instanceof QuickstepLauncher ql) {
                 return ql.getUnfoldTransitionProgressProvider();
             }
         } else {
-            return SystemUiProxy.INSTANCE.get(mPrimaryWindowContext).getUnfoldTransitionProvider();
+            return SystemUiProxy.INSTANCE.get(mBaseContext).getUnfoldTransitionProvider();
         }
         return null;
     }
 
     /** Creates a {@link TaskbarUIController} to use with non default displays. */
     private TaskbarUIController createTaskbarUIControllerForNonDefaultDisplay(int displayId) {
+        debugPrimaryTaskbar("createTaskbarUIControllerForNonDefaultDisplay");
         if (RecentsDisplayModel.enableOverviewInWindow()) {
             RecentsViewContainer rvc = mRecentsDisplayModel.getRecentsWindowManager(displayId);
             if (rvc != null) {
@@ -511,6 +633,7 @@
      */
     private TaskbarUIController createTaskbarUIControllerForRecentsViewContainer(
             RecentsViewContainer container) {
+        debugPrimaryTaskbar("createTaskbarUIControllerForRecentsViewContainer");
         if (container instanceof QuickstepLauncher quickstepLauncher) {
             return new LauncherTaskbarUIController(quickstepLauncher);
         }
@@ -527,13 +650,24 @@
 
     /**
      * 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());
+    public synchronized void recreateTaskbars() {
+        debugPrimaryTaskbar("recreateTaskbars");
+        // Handles initial creation case.
+        if (mTaskbars.size() == 0) {
+            debugTaskbarManager("recreateTaskbars: create primary taskbar", getDefaultDisplayId());
+            recreateTaskbarForDisplay(getDefaultDisplayId(), 0);
+            return;
+        }
+
+        for (int i = 0; i < mTaskbars.size(); i++) {
+            int displayId = mTaskbars.keyAt(i);
+            debugTaskbarManager("recreateTaskbars: create external taskbar", displayId);
+            recreateTaskbarForDisplay(displayId, 0);
+        }
     }
 
     /**
@@ -541,48 +675,62 @@
      * we fully want to destroy an existing taskbar for a specified display and create a new one.
      * In other case (folding/unfolding) we don't need to remove and add window.
      */
-    private void recreateTaskbarForDisplay(int displayId) {
-        Trace.beginSection("recreateTaskbar");
+    private void recreateTaskbarForDisplay(int displayId, int duration) {
+        debugTaskbarManager("recreateTaskbarForDisplay: ", displayId);
+        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;
+            debugTaskbarManager("recreateTaskbarForDisplay: getting device profile", displayId);
+            // TODO (b/381113004): make this display-specific via getWindowContext()
+            DeviceProfile dp = getDeviceProfile(displayId);
 
             // All Apps action is unrelated to navbar unification, so we only need to check DP.
             final boolean isLargeScreenTaskbar = dp != null && dp.isTaskbarPresent;
             mAllAppsActionManager.setTaskbarPresent(isLargeScreenTaskbar);
-
+            debugTaskbarManager("recreateTaskbarForDisplay: destroying taskbar", displayId);
             destroyTaskbarForDisplay(displayId);
 
+            boolean displayExists = getDisplay(displayId) != null;
             boolean isTaskbarEnabled = dp != null && isTaskbarEnabled(dp);
-            debugWhyTaskbarNotDestroyed("recreateTaskbar: 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)
-                    .notifyTaskbarStatus(/* visible */ false, /* stashed */ false);
-                if (!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)
+                    + " displayExists=" + displayExists, displayId);
+            if (!isTaskbarEnabled || !isLargeScreenTaskbar || !displayExists) {
+                SystemUiProxy.INSTANCE.get(mBaseContext)
+                        .notifyTaskbarStatus(/* visible */ false, /* stashed */ false);
+                if (!isTaskbarEnabled || !displayExists) {
+                    debugTaskbarManager(
+                            "recreateTaskbarForDisplay: exiting bc (!isTaskbarEnabled || "
+                                    + "!displayExists)",
+                            displayId);
                     return;
                 }
             }
 
             TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId);
             if (enableTaskbarNoRecreate() || taskbar == null) {
+                debugTaskbarManager("recreateTaskbarForDisplay: creating taskbar", displayId);
                 taskbar = createTaskbarActivityContext(dp, displayId);
+                if (taskbar == null) {
+                    debugTaskbarManager(
+                            "recreateTaskbarForDisplay: new taskbar instance is null!", displayId);
+                    return;
+                }
             } else {
+                debugTaskbarManager("recreateTaskbarForDisplay: updating taskbar device profile",
+                        displayId);
                 taskbar.updateDeviceProfile(dp);
             }
             mSharedState.startTaskbarVariantIsTransient =
                     DisplayController.isTransientTaskbar(taskbar);
             mSharedState.allAppsVisible = mSharedState.allAppsVisible && isLargeScreenTaskbar;
-            taskbar.init(mSharedState);
+            taskbar.init(mSharedState, duration);
 
             // 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(
@@ -590,15 +738,17 @@
             }
 
             if (enableTaskbarNoRecreate()) {
+                debugTaskbarManager("recreateTaskbarForDisplay: adding rootView", displayId);
                 addTaskbarRootViewToWindow(displayId);
                 FrameLayout taskbarRootLayout = getTaskbarRootLayoutForDisplay(displayId);
                 if (taskbarRootLayout != null) {
+                    debugTaskbarManager("recreateTaskbarForDisplay: adding root layout", displayId);
                     taskbarRootLayout.removeAllViews();
                     taskbarRootLayout.addView(taskbar.getDragLayer());
                     taskbar.notifyUpdateLayoutParams();
                 } else {
-                    Log.e(NULL_TASKBAR_ROOT_LAYOUT_TAG,
-                            "taskbarRootLayout is null for displayId=" + displayId);
+                    debugTaskbarManager("recreateTaskbarForDisplay: taskbarRootLayout is null!",
+                            displayId);
                 }
             }
         } finally {
@@ -619,8 +769,8 @@
     }
 
     public void onLongPressHomeEnabled(boolean assistantLongPressEnabled) {
-        if (mDefaultNavButtonController != null) {
-            mDefaultNavButtonController.setAssistantLongPressEnabled(assistantLongPressEnabled);
+        if (mPrimaryNavButtonController != null) {
+            mPrimaryNavButtonController.setAssistantLongPressEnabled(assistantLongPressEnabled);
         }
     }
 
@@ -629,6 +779,7 @@
      */
     public void setSetupUIVisible(boolean isVisible) {
         mSharedState.setupUIVisible = isVisible;
+        mAllAppsActionManager.setSetupUiVisible(isVisible);
         TaskbarActivityContext taskbar = getTaskbarForDisplay(getDefaultDisplayId());
         if (taskbar != null) {
             taskbar.setSetupUIVisible(isVisible);
@@ -668,7 +819,7 @@
     }
 
     public void transitionTo(int displayId, @BarTransitions.TransitionMode int barMode,
-            boolean animate) {
+                             boolean animate) {
         TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId);
         if (taskbar != null) {
             taskbar.transitionTo(barMode, animate);
@@ -742,14 +893,62 @@
      * primary device or a previously mirroring display is switched to extended mode.
      */
     public void onDisplayAddSystemDecorations(int displayId) {
-        if (isDefaultDisplay(displayId) || !enableTaskbarConnectedDisplays()) {
+        debugTaskbarManager("onDisplayAddSystemDecorations: ", displayId);
+        Display display = getDisplay(displayId);
+        if (display == null) {
+            debugTaskbarManager("onDisplayAddSystemDecorations: can't find display!", displayId);
             return;
         }
 
+        if (!DesktopExperienceFlags.ENABLE_TASKBAR_CONNECTED_DISPLAYS.isTrue() || isDefaultDisplay(
+                displayId)) {
+            debugTaskbarManager(
+                    "onDisplayAddSystemDecorations: not an external display! | "
+                            + "ENABLE_TASKBAR_CONNECTED_DISPLAYS="
+                            + DesktopExperienceFlags.ENABLE_TASKBAR_CONNECTED_DISPLAYS.isTrue()
+                            + " isDefaultDisplay=" + isDefaultDisplay(displayId), displayId);
+            return;
+        }
+        debugTaskbarManager("onDisplayAddSystemDecorations: creating new windowContext!",
+                displayId);
         Context newWindowContext = createWindowContext(displayId);
         if (newWindowContext != null) {
+            debugTaskbarManager("onDisplayAddSystemDecorations: add new windowContext to map!",
+                    displayId);
             addWindowContextToMap(displayId, newWindowContext);
+            WindowManager wm = getWindowManager(displayId);
+            if (wm == null || !wm.shouldShowSystemDecors(displayId)) {
+                String wmStatus = wm == null ? "WindowManager is null!" : "WindowManager exists";
+                boolean showDecor = wm != null && wm.shouldShowSystemDecors(displayId);
+                debugTaskbarManager(
+                        "onDisplayAddSystemDecorations:\n\t" + wmStatus + "\n\tshowSystemDecors="
+                                + showDecor, displayId);
+                return;
+            }
+            debugTaskbarManager("onDisplayAddSystemDecorations: creating RootLayout!", displayId);
+
+            createExternalDeviceProfile(displayId);
+
+            debugTaskbarManager("onDisplayAddSystemDecorations: creating RootLayout!", displayId);
+            createTaskbarRootLayout(displayId);
+
+            debugTaskbarManager("onDisplayAddSystemDecorations: creating NavButtonController!",
+                    displayId);
+            createNavButtonController(displayId);
+
+            debugTaskbarManager(
+                    "onDisplayAddSystemDecorations: createAndRegisterComponentCallbacks!",
+                    displayId);
+            createAndRegisterComponentCallbacks(displayId);
+            debugTaskbarManager("onDisplayAddSystemDecorations: recreateTaskbarForDisplay!",
+                    displayId);
+            recreateTaskbarForDisplay(displayId, 0);
+        } else {
+            debugTaskbarManager("onDisplayAddSystemDecorations: newWindowContext is NULL!",
+                    displayId);
         }
+
+        debugTaskbarManager("onDisplayAddSystemDecorations: finished!", displayId);
     }
 
     /**
@@ -757,27 +956,54 @@
      * removed from the primary device.
      */
     public void onDisplayRemoved(int displayId) {
-        if (isDefaultDisplay(displayId) || !enableTaskbarConnectedDisplays()) {
+        debugTaskbarManager("onDisplayRemoved: ", displayId);
+        if (!DesktopExperienceFlags.ENABLE_TASKBAR_CONNECTED_DISPLAYS.isTrue() || isDefaultDisplay(
+                displayId)) {
+            debugTaskbarManager(
+                    "onDisplayRemoved: not an external display! | "
+                            + "ENABLE_TASKBAR_CONNECTED_DISPLAYS="
+                            + DesktopExperienceFlags.ENABLE_TASKBAR_CONNECTED_DISPLAYS.isTrue()
+                            + " isDefaultDisplay=" + isDefaultDisplay(displayId), displayId);
             return;
         }
 
         Context windowContext = getWindowContext(displayId);
         if (windowContext != null) {
+            debugTaskbarManager("onDisplayRemoved: removing NavButtonController!", displayId);
+            removeNavButtonController(displayId);
+
+            debugTaskbarManager("onDisplayRemoved: removeAndUnregisterComponentCallbacks!",
+                    displayId);
+            removeAndUnregisterComponentCallbacks(displayId);
+
+            debugTaskbarManager("onDisplayRemoved: destroying Taskbar!", displayId);
+            destroyTaskbarForDisplay(displayId);
+
+            debugTaskbarManager("onDisplayRemoved: removing DeviceProfile from map!", displayId);
+            removeDeviceProfileFromMap(displayId);
+
+            debugTaskbarManager("onDisplayRemoved: removing WindowContext from map!", displayId);
             removeWindowContextFromMap(displayId);
+
+            debugTaskbarManager("onDisplayRemoved: finished!", displayId);
+        } else {
+            debugTaskbarManager("onDisplayRemoved: removing NavButtonController!", displayId);
         }
     }
 
     /**
      * Signal from SysUI indicating that system decorations should be removed from the display.
      */
-    public void onDisplayRemoveSystemDecorations(int displayId) {}
+    public void onDisplayRemoveSystemDecorations(int displayId) {
+        // The display mirroring starts. The handling logic is the same as when removing a
+        // display.
+        onDisplayRemoved(displayId);
+    }
 
     private void removeActivityCallbacksAndListeners() {
         if (mActivity != null) {
             mActivity.removeOnDeviceProfileChangeListener(mDebugActivityDeviceProfileChanged);
-            Log.d(TASKBAR_NOT_DESTROYED_TAG,
-                    "unregistering activity lifecycle callbacks from "
-                            + "removeActivityCallbackAndListeners().");
+            debugPrimaryTaskbar("unregistering activity lifecycle callbacks");
             mActivity.removeEventCallback(EVENT_DESTROYED, mActivityOnDestroyCallback);
             UnfoldTransitionProgressProvider unfoldTransitionProgressProvider =
                     getUnfoldTransitionProgressProviderForActivity(mActivity);
@@ -791,10 +1017,17 @@
      * Called when the manager is no longer needed
      */
     public void destroy() {
+        debugPrimaryTaskbar("TaskbarManager#destroy()");
         mRecentsViewContainer = null;
-        debugWhyTaskbarNotDestroyed("TaskbarManager#destroy()");
+        debugPrimaryTaskbar("destroy: removing activity callbacks");
+        DesktopVisibilityController.INSTANCE.get(
+                mPrimaryWindowContext).unregisterTaskbarDesktopModeListener(
+                mTaskbarDesktopModeListener);
         removeActivityCallbacksAndListeners();
-        mTaskbarBroadcastReceiver.unregisterReceiverSafely(mPrimaryWindowContext);
+        mTaskbarBroadcastReceiver.unregisterReceiverSafely();
+        if (mGrowthBroadcastReceiver != null) {
+            mGrowthBroadcastReceiver.unregisterReceiverSafely();
+        }
 
         if (mUserUnlocked) {
             DisplayController.INSTANCE.get(mPrimaryWindowContext).removeChangeListener(
@@ -804,10 +1037,18 @@
                 .unregister(USER_SETUP_COMPLETE_URI, mOnSettingsChangeListener);
         SettingsCache.INSTANCE.get(mPrimaryWindowContext)
                 .unregister(NAV_BAR_KIDS_MODE, mOnSettingsChangeListener);
-        Log.d(TASKBAR_NOT_DESTROYED_TAG, "unregistering component callbacks from destroy().");
-        mPrimaryWindowContext.unregisterComponentCallbacks(mDefaultComponentCallbacks);
-        mShutdownReceiver.unregisterReceiverSafely(mPrimaryWindowContext);
+        debugPrimaryTaskbar("destroy: unregistering component callbacks");
+        removeAndUnregisterComponentCallbacks(getDefaultDisplayId());
+        mShutdownReceiver.unregisterReceiverSafely();
+        if (ActivityManagerWrapper.usePerceptibleTasks(getPrimaryWindowContext())) {
+            for (Integer taskId : mTaskStackListener.mPerceptibleTasks) {
+                ActivityManagerWrapper.getInstance().setTaskIsPerceptible(taskId, false);
+            }
+        }
+        TaskStackChangeListeners.getInstance().unregisterTaskStackListener(mTaskStackListener);
+        debugPrimaryTaskbar("destroy: destroying all taskbars!");
         destroyAllTaskbars();
+        debugPrimaryTaskbar("destroy: finished!");
     }
 
     public @Nullable TaskbarActivityContext getCurrentActivityContext() {
@@ -830,40 +1071,53 @@
     }
 
     private void addTaskbarRootViewToWindow(int displayId) {
+        debugTaskbarManager("addTaskbarRootViewToWindow:", 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,
-                        "addTaskbarRootViewToWindow - root layout null | displayId=" + displayId);
+                String rootLayoutStatus =
+                        (rootLayout == null) ? "rootLayout is NULL!" : "rootLayout exists!";
+                String wmStatus = (windowManager == null) ? "windowManager is NULL!"
+                        : "windowManager exists!";
+                debugTaskbarManager(
+                        "addTaskbarRootViewToWindow: \n\t" + rootLayoutStatus + "\n\t" + wmStatus,
+                        displayId);
             }
         } else {
-            Log.d(NULL_TASKBAR_ROOT_LAYOUT_TAG,
-                    "addTaskbarRootViewToWindow - root layout already added | displayId="
-                            + displayId);
+            debugTaskbarManager("addTaskbarRootViewToWindow: rootLayout already added!", displayId);
         }
     }
 
     private void removeTaskbarRootViewFromWindow(int displayId) {
-        Log.d(ILLEGAL_ARGUMENT_WM_ADD_VIEW, "removeTaskbarRootViewFromWindow: " + displayId);
+        debugTaskbarManager("removeTaskbarRootViewFromWindow", displayId);
         FrameLayout rootLayout = getTaskbarRootLayoutForDisplay(displayId);
         if (!enableTaskbarNoRecreate() || rootLayout == null) {
             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);
         }
     }
 
@@ -873,7 +1127,7 @@
      *
      * @param displayId The ID of the display to retrieve the taskbar for.
      * @return The {@link TaskbarUIController} for the specified display, or
-     *         {@code null} if no taskbar is associated with that display.
+     * {@code null} if no taskbar is associated with that display.
      */
     @Nullable
     public TaskbarUIController getUIControllerForDisplay(int displayId) {
@@ -900,7 +1154,7 @@
      *
      * @param displayId The ID of the display to retrieve the taskbar for.
      * @return The {@link TaskbarActivityContext} for the specified display, or
-     *         {@code null} if no taskbar is associated with that display.
+     * {@code null} if no taskbar is associated with that display.
      */
     private TaskbarActivityContext getTaskbarForDisplay(int displayId) {
         return mTaskbars.get(displayId);
@@ -909,28 +1163,236 @@
 
     /**
      * 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;
     }
 
     /**
+     * Creates a {@link DeviceProfile} for the given display and adds it to the map.
+     * @param displayId The ID of the display.
+     */
+    private void createExternalDeviceProfile(int displayId) {
+        if (!mUserUnlocked) {
+            return;
+        }
+
+        InvariantDeviceProfile idp = LauncherAppState.getIDP(mPrimaryWindowContext);
+        if (idp == null) {
+            return;
+        }
+
+        Context displayContext = getWindowContext(displayId);
+        if (displayContext == null) {
+            return;
+        }
+
+        DeviceProfile externalDeviceProfile = idp.createDeviceProfileForSecondaryDisplay(
+                displayContext);
+        mExternalDeviceProfiles.put(displayId, externalDeviceProfile);
+    }
+
+    /**
+     * Gets a {@link DeviceProfile} for the given displayId.
+     * @param displayId The ID of the display.
+     */
+    private @Nullable DeviceProfile getDeviceProfile(int displayId) {
+        if (!mUserUnlocked) {
+            return null;
+        }
+
+        InvariantDeviceProfile idp = LauncherAppState.getIDP(mPrimaryWindowContext);
+        if (idp == null) {
+            return null;
+        }
+
+        boolean isPrimary = isDefaultDisplay(displayId)
+                || !DesktopExperienceFlags.ENABLE_TASKBAR_CONNECTED_DISPLAYS.isTrue();
+        if (isPrimary) {
+            return idp.getDeviceProfile(mPrimaryWindowContext);
+        }
+
+        return mExternalDeviceProfiles.get(displayId);
+    }
+
+    /**
+     * Removes the {@link DeviceProfile} associated with the given display ID from the map.
+     * @param displayId The ID of the display for which to remove the taskbar.
+     */
+    private void removeDeviceProfileFromMap(int displayId) {
+        mExternalDeviceProfiles.delete(displayId);
+    }
+
+    /**
+     * 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) {
+        debugTaskbarManager("createAndRegisterComponentCallbacks", 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("onConfigurationChanged: " + newConfig, displayId);
+
+                DeviceProfile dp = getDeviceProfile(displayId);
+                int configDiff = mOldConfig.diff(newConfig) & ~SKIP_RECREATE_CONFIG_CHANGES;
+
+                if ((configDiff & ActivityInfo.CONFIG_UI_MODE) != 0) {
+                    debugTaskbarManager("onConfigurationChanged: theme changed", displayId);
+                    // 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("onConfigurationChanged: | configDiff="
+                        + Configuration.configurationDiffToString(configDiff), displayId);
+                if (configDiff != 0 || getCurrentActivityContext() == null) {
+                    debugTaskbarManager("onConfigurationChanged: call recreateTaskbars", displayId);
+                    recreateTaskbars();
+                } else if (dp != null) {
+                    // Config change might be handled without re-creating the taskbar
+                    if (!isTaskbarEnabled(dp)) {
+                        debugPrimaryTaskbar(
+                                "onConfigurationChanged: isTaskbarEnabled(dp)=False | "
+                                        + "destroyTaskbarForDisplay");
+                        destroyTaskbarForDisplay(getDefaultDisplayId());
+                    } else {
+                        debugPrimaryTaskbar("onConfigurationChanged: isTaskbarEnabled(dp)=True");
+                        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?
+                            debugPrimaryTaskbar("onConfigurationChanged: call recreateTaskbars");
+                            recreateTaskbars();
+                        } else {
+                            debugPrimaryTaskbar(
+                                    "onConfigurationChanged: updateDeviceProfile for current "
+                                            + "taskbar.");
+                            getCurrentActivityContext().updateDeviceProfile(dp);
+                        }
+                    }
+                } else {
+
+                    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.
      *
-     * @param displayId The ID of the display to retrieve the taskbar for.
+     * @param displayId  The ID of the display to retrieve the taskbar for.
      * @param newTaskbar The new {@link TaskbarActivityContext} to add to the map.
      */
     private void addTaskbarToMap(int displayId, TaskbarActivityContext newTaskbar) {
@@ -950,24 +1412,31 @@
 
     /**
      * Creates {@link FrameLayout} for the taskbar on the specified display and adds it to map.
+     *
      * @param displayId The ID of the display for which to create the taskbar root layout.
      */
     private void createTaskbarRootLayout(int displayId) {
-        Log.d(ILLEGAL_ARGUMENT_WM_ADD_VIEW, "createTaskbarRootLayout: " + displayId);
+        debugTaskbarManager("createTaskbarRootLayout: ", displayId);
+        if (!enableTaskbarNoRecreate()) {
+            return;
+        }
+
         FrameLayout newTaskbarRootLayout = new FrameLayout(getWindowContext(displayId)) {
             @Override
             public boolean dispatchTouchEvent(MotionEvent ev) {
+                debugTaskbarManager("dispatchTouchEvent: ", displayId);
                 // 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);
                 }
                 return super.dispatchTouchEvent(ev);
             }
         };
+
+        debugTaskbarManager("createTaskbarRootLayout: adding to map", displayId);
         addTaskbarRootLayoutToMap(displayId, newTaskbarRootLayout);
-        Log.d(NULL_TASKBAR_ROOT_LAYOUT_TAG, "created new root layout - displayId=" + displayId);
     }
 
     private boolean isDefaultDisplay(int displayId) {
@@ -981,13 +1450,12 @@
      * @return The taskbar root layout {@link FrameLayout} for a given display or {@code null}.
      */
     private FrameLayout getTaskbarRootLayoutForDisplay(int displayId) {
-        Log.d(ILLEGAL_ARGUMENT_WM_ADD_VIEW, "getTaskbarRootLayoutForDisplay: " + displayId);
+        debugTaskbarManager("getTaskbarRootLayoutForDisplay:", displayId);
         FrameLayout frameLayout = mRootLayouts.get(displayId);
         if (frameLayout != null) {
             return frameLayout;
         } else {
-            Log.d(NULL_TASKBAR_ROOT_LAYOUT_TAG,
-                    "getTaskbarRootLayoutForDisplay == null | displayId=" + displayId);
+            debugTaskbarManager("getTaskbarRootLayoutForDisplay: rootLayout is null!", displayId);
             return null;
         }
     }
@@ -995,15 +1463,18 @@
     /**
      * Adds the taskbar root layout {@link FrameLayout} to taskbar map, mapped to display ID.
      *
-     * @param displayId The ID of the display to associate with the taskbar root layout.
+     * @param displayId  The ID of the display to associate with the taskbar root layout.
      * @param rootLayout The taskbar root layout {@link FrameLayout} to add to the map.
      */
     private void addTaskbarRootLayoutToMap(int displayId, FrameLayout rootLayout) {
+        debugTaskbarManager("addTaskbarRootLayoutToMap: ", displayId);
         if (!mRootLayouts.contains(displayId) && rootLayout != null) {
             mRootLayouts.put(displayId, rootLayout);
         }
 
-        Log.d(NULL_TASKBAR_ROOT_LAYOUT_TAG, "mRootLayouts.size()=" + mRootLayouts.size());
+        debugTaskbarManager(
+                "addTaskbarRootLayoutToMap: finished! mRootLayouts.size()=" + mRootLayouts.size(),
+                displayId);
     }
 
     /**
@@ -1012,26 +1483,26 @@
      * @param displayId The ID of the display for which to remove the taskbar root layout.
      */
     private void removeTaskbarRootLayoutFromMap(int displayId) {
+        debugTaskbarManager("removeTaskbarRootLayoutFromMap:", displayId);
         if (mRootLayouts.contains(displayId)) {
             mAddedRootLayouts.delete(displayId);
             mRootLayouts.delete(displayId);
         }
 
-        Log.d(NULL_TASKBAR_ROOT_LAYOUT_TAG, "mRootLayouts.size()=" + mRootLayouts.size());
+        debugTaskbarManager("removeTaskbarRootLayoutFromMap: finished! mRootLayouts.size="
+                + mRootLayouts.size(), displayId);
     }
 
     /**
      * Creates {@link Context} for the taskbar on the specified display.
+     *
      * @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;
         }
 
@@ -1039,10 +1510,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.
      *
@@ -1050,7 +1540,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);
     }
 
@@ -1066,12 +1557,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;
         }
 
@@ -1081,7 +1575,7 @@
     /**
      * Adds the window context {@link Context} to taskbar map, mapped to display ID.
      *
-     * @param displayId The ID of the display to associate with the taskbar root layout.
+     * @param displayId     The ID of the display to associate with the taskbar root layout.
      * @param windowContext The window context {@link Context} to add to the map.
      */
     private void addWindowContextToMap(int displayId, @NonNull Context windowContext) {
@@ -1105,29 +1599,73 @@
         return mBaseContext.getDisplayId();
     }
 
-    /** Temp logs for b/254119092. */
-    public void debugWhyTaskbarNotDestroyed(String debugReason) {
-        debugWhyTaskbarNotDestroyed(debugReason, getDefaultDisplayId());
+    /**
+     * Logs debug information about the TaskbarManager for primary display.
+     * @param debugReason A string describing the reason for the debug log.
+     * @param displayId The ID of the display for which to log debug information.
+     */
+    public void debugTaskbarManager(String debugReason, int displayId) {
+        StringJoiner log = new StringJoiner("\n");
+        log.add(debugReason + " displayId=" + displayId + " isDefaultDisplay=" + isDefaultDisplay(
+                displayId));
+        Log.d(TAG, log.toString());
     }
 
-    /** Temp logs for b/254119092. */
-    public void debugWhyTaskbarNotDestroyed(String debugReason, int displayId) {
+    /**
+     * Logs verbose debug information about the TaskbarManager for primary display.
+     * @param debugReason A string describing the reason for the debug log.
+     * @param displayId The ID of the display for which to log debug information.
+     * @param verbose Indicates whether or not to debug with detail.
+     */
+    public void debugTaskbarManager(String debugReason, int displayId, boolean verbose) {
         StringJoiner log = new StringJoiner("\n");
-        log.add(debugReason  + " displayId=" + displayId);
+        log.add(debugReason + " displayId=" + displayId + " isDefaultDisplay=" + isDefaultDisplay(
+                displayId));
+        if (verbose) {
+            generateVerboseLogs(log, displayId);
+        }
+        Log.d(TAG, log.toString());
+    }
 
+    /**
+     * Logs debug information about the TaskbarManager for primary display.
+     * @param debugReason A string describing the reason for the debug log.
+     *
+     */
+    public void debugPrimaryTaskbar(String debugReason) {
+        debugTaskbarManager(debugReason, getDefaultDisplayId(), false);
+    }
+
+    /**
+     * Logs debug information about the TaskbarManager for primary display.
+     * @param debugReason A string describing the reason for the debug log.
+     *
+     */
+    public void debugPrimaryTaskbar(String debugReason, boolean verbose) {
+        debugTaskbarManager(debugReason, getDefaultDisplayId(), verbose);
+    }
+
+    /**
+     * Logs verbose debug information about the TaskbarManager for a specific display.
+     */
+    private void generateVerboseLogs(StringJoiner log, int 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);
+            log.add("windowContext is null!");
             return;
         }
 
-        boolean contextTaskbarPresent = mUserUnlocked && LauncherAppState.getIDP(windowContext)
-                .getDeviceProfile(windowContext).isTaskbarPresent;
+        boolean contextTaskbarPresent = false;
+        if (mUserUnlocked) {
+            DeviceProfile dp = getDeviceProfile(displayId);
+            contextTaskbarPresent = dp != null && dp.isTaskbarPresent;
+        }
         if (activityTaskbarPresent == contextTaskbarPresent) {
             log.add("mActivity and mWindowContext agree taskbarIsPresent=" + contextTaskbarPresent);
-            Log.d(TASKBAR_NOT_DESTROYED_TAG, log.toString());
+            Log.d(TAG, log.toString());
             return;
         }
 
@@ -1146,16 +1684,14 @@
         log.add("\t\tWindowContext.getResources().getConfiguration()="
                 + windowContext.getResources().getConfiguration());
         if (mUserUnlocked) {
-            log.add("\t\tLauncherAppState.getIDP().getDeviceProfile(mPrimaryWindowContext)"
-                    + ".isTaskbarPresent=" + contextTaskbarPresent);
+            log.add("\t\tgetDeviceProfile(mPrimaryWindowContext).isTaskbarPresent="
+                    + contextTaskbarPresent);
         } else {
             log.add("\t\tCouldn't get DeviceProfile because !mUserUnlocked");
         }
-
-        Log.d(TASKBAR_NOT_DESTROYED_TAG, log.toString());
     }
 
     private final DeviceProfile.OnDeviceProfileChangeListener mDebugActivityDeviceProfileChanged =
-            dp -> debugWhyTaskbarNotDestroyed("mActivity onDeviceProfileChanged");
+            dp -> debugPrimaryTaskbar("mActivity onDeviceProfileChanged", true);
 
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarPinningController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarPinningController.kt
index 23c5070..7141bb8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarPinningController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarPinningController.kt
@@ -58,8 +58,9 @@
                 }
                 val shouldPinTaskbar =
                     if (
-                        controllers.taskbarDesktopModeController
-                            .areDesktopTasksVisibleAndNotInOverview
+                        controllers.taskbarDesktopModeController.isInDesktopModeAndNotInOverview(
+                            context.displayId
+                        )
                     ) {
                         !launcherPrefs.get(TASKBAR_PINNING_IN_DESKTOP_MODE)
                     } else {
@@ -140,7 +141,11 @@
     @VisibleForTesting
     fun recreateTaskbarAndUpdatePinningValue() {
         updateIsAnimatingTaskbarPinningAndNotifyTaskbarDragLayer(false)
-        if (controllers.taskbarDesktopModeController.areDesktopTasksVisibleAndNotInOverview) {
+        if (
+            controllers.taskbarDesktopModeController.isInDesktopModeAndNotInOverview(
+                context.displayId
+            )
+        ) {
             launcherPrefs.put(
                 TASKBAR_PINNING_IN_DESKTOP_MODE,
                 !launcherPrefs.get(TASKBAR_PINNING_IN_DESKTOP_MODE),
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java
index feb9b33..5d8b821 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java
@@ -217,7 +217,7 @@
         }
         shortcuts.add(APP_INFO);
         if (!mControllers.taskbarDesktopModeController
-                .getAreDesktopTasksVisibleAndNotInOverview()) {
+                .isInDesktopModeAndNotInOverview(mContext.getDisplayId())) {
             shortcuts.addAll(mControllers.uiController.getSplitMenuOptions().toList());
         }
         if (BubbleAnythingFlagHelper.enableCreateAnyBubble()) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
index 2ded1bf..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;
         }
 
@@ -1189,7 +1193,7 @@
         if (mActivity.isHardwareKeyboard()
                 && mActivity.isThreeButtonNav()
                 && mControllers.taskbarDesktopModeController
-                    .getAreDesktopTasksVisibleAndNotInOverview()) {
+                    .isInDesktopModeAndNotInOverview(mActivity.getDisplayId())) {
             return false;
         }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
index e5d642d..89bcb41 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
@@ -283,7 +283,7 @@
                                     foundTask,
                                     taskContainer.getIconView().getDrawable(),
                                     taskContainer.getSnapshotView(),
-                                    taskContainer.getSplitAnimationThumbnail(),
+                                    taskContainer.getThumbnail(),
                                     null /* intent */,
                                     null /* user */,
                                     info);
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
index a59c9e3..c92f20b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
@@ -392,6 +392,7 @@
 
     /** Inflates/binds the hotseat items and recent tasks to the view. */
     protected void updateItems(ItemInfo[] hotseatItemInfos, List<GroupTask> recentTasks) {
+        if (mActivityContext.isDestroyed()) return;
         // Filter out unsupported items.
         hotseatItemInfos = Arrays.stream(hotseatItemInfos)
                 .filter(Objects::nonNull)
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
index cbc5d3d..384468c 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
@@ -121,9 +121,10 @@
     public static final int ALPHA_INDEX_NOTIFICATION_EXPANDED = 4;
     public static final int ALPHA_INDEX_ASSISTANT_INVOKED = 5;
     public static final int ALPHA_INDEX_SMALL_SCREEN = 6;
-
     public static final int ALPHA_INDEX_BUBBLE_BAR = 7;
-    private static final int NUM_ALPHA_CHANNELS = 8;
+    public static final int ALPHA_INDEX_RECREATE = 8;
+
+    private static final int NUM_ALPHA_CHANNELS = 9;
 
     /** Only used for animation purposes, to position the divider between two item indices. */
     public static final float DIVIDER_VIEW_POSITION_OFFSET = 0.5f;
@@ -238,9 +239,22 @@
                 R.dimen.transient_taskbar_padding);
     }
 
-    public void init(TaskbarControllers controllers) {
+    /**
+     * Init of taskbar view controller.
+     */
+    public void init(TaskbarControllers controllers, AnimatorSet startAnimation) {
         mControllers = controllers;
         controllers.bubbleControllers.ifPresent(bc -> mBubbleControllers = bc);
+
+        if (startAnimation != null) {
+            MultiPropertyFactory<View>.MultiProperty multiProperty =
+                    mTaskbarIconAlpha.get(ALPHA_INDEX_RECREATE);
+            multiProperty.setValue(0f);
+            Animator animator = multiProperty.animateToValue(1f);
+            animator.setInterpolator(EMPHASIZED);
+            startAnimation.play(animator);
+        }
+
         mTaskbarView.init(TaskbarViewCallbacksFactory.newInstance(mActivity).create(
                 mActivity, mControllers, mTaskbarView));
         mTaskbarView.getLayoutParams().height = mActivity.isPhoneMode()
@@ -362,6 +376,15 @@
         mTaskbarView.announceAccessibilityChanges();
     }
 
+    /**
+     * Called with destroying Taskbar with animation.
+     */
+    public void onDestroyAnimation(AnimatorSet animatorSet) {
+        animatorSet.play(
+                mTaskbarIconAlpha.get(TaskbarViewController.ALPHA_INDEX_RECREATE).animateToValue(
+                        0f));
+    }
+
     public void onDestroy() {
         if (enableTaskbarPinning()) {
             mTaskbarView.removeOnLayoutChangeListener(mTaskbarViewLayoutChangeListener);
@@ -1299,7 +1322,7 @@
         ObjectAnimator animator = mIconsTranslationXForNavbar.animateToValue(translationX);
         animator.setStartDelay(FADE_OUT_ANIM_POSITION_DURATION_MS);
         animator.setDuration(FADE_IN_ANIM_ALPHA_DURATION_MS);
-        animator.setInterpolator(Interpolators.EMPHASIZED);
+        animator.setInterpolator(EMPHASIZED);
         return animator;
     }
 }
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..e9c62d1
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/TypefaceUtils.kt
@@ -0,0 +1,53 @@
+/*
+ * 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_BODY_SMALL_BASELINE = "variable-body-small"
+        const val FONT_FAMILY_BODY_MEDIUM_BASELINE = "variable-body-medium"
+        const val FONT_FAMILY_BODY_LARGE_BASELINE = "variable-body-large"
+        const val FONT_FAMILY_LABEL_LARGE_BASELINE = "variable-label-large"
+        const val FONT_FAMILY_DISPLAY_SMALL_EMPHASIZED = "variable-display-small-emphasized"
+        const val FONT_FAMILY_DISPLAY_MEDIUM_EMPHASIZED = "variable-display-medium-emphasized"
+        const val FONT_FAMILY_HEADLINE_SMALL_EMPHASIZED = "variable-headline-small-emphasized"
+        const val FONT_FAMILY_HEADLINE_LARGE_EMPHASIZED = "variable-headline-large-emphasized"
+
+        @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/taskbar/growth/GrowthConstants.java b/quickstep/src/com/android/launcher3/taskbar/growth/GrowthConstants.java
new file mode 100644
index 0000000..78ef152
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/growth/GrowthConstants.java
@@ -0,0 +1,28 @@
+/*
+ * 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.growth;
+
+/**
+ * Constants for registering Growth framework.
+ */
+public final class GrowthConstants {
+    /**
+     * For Taskbar broadcast intent filter.
+     */
+    public static final String BROADCAST_SHOW_NUDGE =
+            "com.android.launcher3.growth.BROADCAST_SHOW_NUDGE";
+    private GrowthConstants() {}
+}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java b/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
index b25f999..15a27d1 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
@@ -17,6 +17,7 @@
 
 import static com.android.app.animation.Interpolators.ACCELERATE_DECELERATE;
 import static com.android.launcher3.icons.FastBitmapDrawable.getDisabledColorFilter;
+import static com.android.launcher3.icons.IconNormalizer.ICON_VISIBLE_AREA_FACTOR;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -51,9 +52,8 @@
 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;
 import com.android.launcher3.model.data.ItemInfoWithIcon;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
@@ -138,11 +138,11 @@
     public PredictedAppIcon(Context context, AttributeSet attrs, int defStyle) {
         super(context, attrs, defStyle);
         mDeviceProfile = ActivityContext.lookupContext(context).getDeviceProfile();
-        mNormalizedIconSize = IconNormalizer.getNormalizedCircleSize(getIconSize());
+        mNormalizedIconSize = Math.round(getIconSize() * ICON_VISIBLE_AREA_FACTOR);
         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 2111a80..aab8ad1 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -86,7 +86,6 @@
 import android.os.Trace;
 import android.os.UserHandle;
 import android.util.AttributeSet;
-import android.util.Log;
 import android.view.Display;
 import android.view.HapticFeedbackConstants;
 import android.view.KeyEvent;
@@ -247,7 +246,6 @@
     private SplitWithKeyboardShortcutController mSplitWithKeyboardShortcutController;
     private SplitToWorkspaceController mSplitToWorkspaceController;
     private BubbleBarLocation mBubbleBarLocation;
-    private static final String TRACKING_BUG = "b/395214062";
 
     /**
      * If Launcher restarted while in the middle of an Overview split select, it needs this data to
@@ -563,7 +561,6 @@
 
     @Override
     public void onDestroy() {
-        Log.d(TRACKING_BUG, "onDestroy: " + this.hashCode());
         if (mAppTransitionManager != null) {
             mAppTransitionManager.onActivityDestroyed();
         }
@@ -589,10 +586,7 @@
 
         RecentsView recentsView = getOverviewPanel();
         if (recentsView != null) {
-            Log.d(TRACKING_BUG, "onDestroy - recentsView.destroy(): " + this.hashCode());
             recentsView.destroy();
-        } else {
-            Log.d(TRACKING_BUG, "onDestroy - recentsView is null: " + this.hashCode());
         }
 
         super.onDestroy();
@@ -719,7 +713,6 @@
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-        Log.d(TRACKING_BUG, "onCreate: " + this.hashCode());
         if (savedInstanceState != null) {
             mPendingSplitSelectInfo = ObjectWrapper.unwrap(
                     savedInstanceState.getIBinder(PENDING_SPLIT_SELECT_INFO));
@@ -832,7 +825,7 @@
     @Override
     protected void onResume() {
         super.onResume();
-        Log.d(TRACKING_BUG, "onResume: " + this.hashCode());
+
         if (mLauncherUnfoldAnimationController != null) {
             mLauncherUnfoldAnimationController.onResume();
         }
@@ -867,7 +860,6 @@
     @Override
     protected void onStop() {
         super.onStop();
-        Log.d(TRACKING_BUG, "onStop: " + this.hashCode());
         if (mTaskbarUIController != null && FeatureFlags.enableHomeTransitionListener()) {
             mTaskbarUIController.onLauncherStop();
         }
@@ -1055,7 +1047,7 @@
         DesktopVisibilityController desktopVisibilityController =
                 DesktopVisibilityController.INSTANCE.get(this);
         if (!ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue()
-                && desktopVisibilityController.areDesktopTasksVisibleAndNotInOverview()
+                && desktopVisibilityController.isInDesktopModeAndNotInOverview(getDisplayId())
                 && !desktopVisibilityController.isRecentsGestureInProgress()) {
             // Return early to skip setting activity to appear as resumed
             // TODO: b/333533253 - Remove after flag rollout
@@ -1377,7 +1369,7 @@
     @Override
     public boolean areDesktopTasksVisible() {
         return DesktopVisibilityController.INSTANCE.get(this)
-                .areDesktopTasksVisibleAndNotInOverview();
+                .isInDesktopModeAndNotInOverview(getDisplayId());
     }
 
     @Override
@@ -1388,7 +1380,8 @@
         SystemUiProxy.INSTANCE.get(this).setLauncherAppIconSize(mDeviceProfile.iconSizePx);
         TaskbarManager taskbarManager = mTISBindHelper.getTaskbarManager();
         if (taskbarManager != null) {
-            taskbarManager.debugWhyTaskbarNotDestroyed("QuickstepLauncher#onDeviceProfileChanged");
+            taskbarManager.debugPrimaryTaskbar("QuickstepLauncher#onDeviceProfileChanged",
+                    true);
         }
     }
 
diff --git a/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.kt b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.kt
index c8f46a9..04e1905 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.kt
+++ b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.kt
@@ -20,23 +20,23 @@
 import com.android.app.animation.Interpolators.FINAL_FRAME
 import com.android.app.animation.Interpolators.INSTANT
 import com.android.app.animation.Interpolators.LINEAR
+import com.android.launcher3.Flags.enableDesktopExplodedView
 import com.android.launcher3.Flags.enableLargeDesktopWindowingTile
 import com.android.launcher3.LauncherState
 import com.android.launcher3.anim.AnimatedFloat
 import com.android.launcher3.anim.AnimatorListeners.forSuccessCallback
 import com.android.launcher3.anim.PendingAnimation
 import com.android.launcher3.anim.PropertySetter
-import com.android.launcher3.logging.StatsLogManager.LauncherEvent
 import com.android.launcher3.statemanager.StateManager.StateHandler
 import com.android.launcher3.states.StateAnimationConfig
 import com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_ACTIONS_FADE
 import com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_FADE
 import com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_MODAL
 import com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SCALE
-import com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SPLIT_SELECT_INSTRUCTIONS_FADE
 import com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_TRANSLATE_X
 import com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_TRANSLATE_Y
 import com.android.launcher3.states.StateAnimationConfig.SKIP_OVERVIEW
+import com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE
 import com.android.quickstep.util.AnimUtils
 import com.android.quickstep.views.ClearAllButton
 import com.android.quickstep.views.RecentsView
@@ -51,6 +51,7 @@
 import com.android.quickstep.views.RecentsView.TASK_SECONDARY_SPLIT_TRANSLATION
 import com.android.quickstep.views.RecentsView.TASK_SECONDARY_TRANSLATION
 import com.android.quickstep.views.RecentsView.TASK_THUMBNAIL_SPLASH_ALPHA
+import com.android.quickstep.views.RecentsViewUtils.Companion.DESK_EXPLODE_PROGRESS
 import com.android.quickstep.views.TaskView.Companion.FLAG_UPDATE_ALL
 
 /**
@@ -74,6 +75,13 @@
             recentsView,
             if (state.displayOverviewTasksAsGrid(launcher.deviceProfile)) 1f else 0f,
         )
+        if (enableDesktopExplodedView()) {
+            DESK_EXPLODE_PROGRESS.set(
+                recentsView,
+                if (state.displayOverviewTasksAsGrid(launcher.deviceProfile)) 1f else 0f,
+            )
+        }
+
         TASK_THUMBNAIL_SPLASH_ALPHA.set(
             recentsView,
             if (state.showTaskThumbnailSplash()) 1f else 0f,
@@ -156,6 +164,15 @@
             getOverviewInterpolator(fromState, toState),
         )
 
+        if (enableDesktopExplodedView()) {
+            builder.setFloat(
+                recentsView,
+                DESK_EXPLODE_PROGRESS,
+                if (toState.isRecentsViewVisible) 1f else 0f,
+                getOverviewInterpolator(fromState, toState),
+            )
+        }
+
         if (enableLargeDesktopWindowingTile()) {
             builder.setFloat(
                 recentsView,
@@ -208,8 +225,8 @@
         builder: PendingAnimation,
         animate: Boolean,
     ) {
-        val goingToOverviewFromWorkspaceContextual = toState == LauncherState.OVERVIEW &&
-                launcher.isSplitSelectionActive
+        val goingToOverviewFromWorkspaceContextual =
+            toState == LauncherState.OVERVIEW && launcher.isSplitSelectionActive
         if (
             toState != LauncherState.OVERVIEW_SPLIT_SELECT &&
                 !goingToOverviewFromWorkspaceContextual
@@ -284,6 +301,14 @@
             overviewButtonAlpha,
             config.getInterpolator(ANIM_OVERVIEW_ACTIONS_FADE, LINEAR),
         )
+        recentsView.addDeskButton?.let {
+            propertySetter.setFloat(
+                it.visibilityAlphaProperty,
+                MULTI_PROPERTY_VALUE,
+                if (state.areElementsVisible(launcher, LauncherState.ADD_DESK_BUTTON)) 1f else 0f,
+                LINEAR,
+            )
+        }
     }
 
     private fun getOverviewInterpolator(fromState: LauncherState, toState: LauncherState) =
diff --git a/quickstep/src/com/android/launcher3/uioverrides/SystemApiWrapper.kt b/quickstep/src/com/android/launcher3/uioverrides/SystemApiWrapper.kt
index 6e901ee..1f34969 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/SystemApiWrapper.kt
+++ b/quickstep/src/com/android/launcher3/uioverrides/SystemApiWrapper.kt
@@ -144,7 +144,7 @@
     override fun isNonResizeableActivity(lai: LauncherActivityInfo) =
         lai.activityInfo.resizeMode == ActivityInfo.RESIZE_MODE_UNRESIZEABLE
 
-    override fun supportsMultiInstance(lai: LauncherActivityInfo) : Boolean {
+    override fun supportsMultiInstance(lai: LauncherActivityInfo): Boolean {
         return try {
             super.supportsMultiInstance(lai) || lai.supportsMultiInstance()
         } catch (e: Exception) {
@@ -202,4 +202,7 @@
         (appInfo.sourceDir?.hashCode() ?: 0).toString() + " " + appInfo.longVersionCode
 
     override fun getRoundIconRes(appInfo: ApplicationInfo) = appInfo.roundIconRes
+
+    override fun isFileDrawable(shortcutInfo: ShortcutInfo) =
+        shortcutInfo.hasIconFile() || shortcutInfo.hasIconUri()
 }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt b/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt
index 7c09e9a..0d2cfbf 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt
+++ b/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt
@@ -317,6 +317,12 @@
             )
             addPreference(
                 Preference(context).apply {
+                    title = "Launch Full Gesture Tutorial"
+                    intent = Intent(launchSandboxIntent).putExtra("use_tutorial_menu", false)
+                }
+            )
+            addPreference(
+                Preference(context).apply {
                     title = "Launch Back Tutorial"
                     intent =
                         Intent(launchSandboxIntent)
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java b/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
index 10513c0..b27c6e8 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
@@ -153,7 +153,7 @@
         return new PageAlphaProvider(DECELERATE_2) {
             @Override
             public float getPageAlpha(int pageIndex) {
-                return launcher.getDeviceProfile().shouldShowAllAppsOnSheet()
+                return launcher.getDeviceProfile().isTablet
                         ? superPageAlphaProvider.getPageAlpha(pageIndex)
                         : 0;
             }
@@ -164,7 +164,7 @@
     public int getVisibleElements(Launcher launcher) {
         int elements = ALL_APPS_CONTENT | FLOATING_SEARCH_BAR;
         // When All Apps is presented on a bottom sheet, HOTSEAT_ICONS are visible.
-        if (launcher.getDeviceProfile().shouldShowAllAppsOnSheet()) {
+        if (launcher.getDeviceProfile().isTablet) {
             elements |= HOTSEAT_ICONS;
         }
         return elements;
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java b/quickstep/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
index ca388c6..b1196af 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
@@ -77,7 +77,8 @@
         return super.getVisibleElements(launcher)
                 & ~OVERVIEW_ACTIONS
                 & ~CLEAR_ALL_BUTTON
-                & ~VERTICAL_SWIPE_INDICATOR;
+                & ~VERTICAL_SWIPE_INDICATOR
+                & ~ADD_DESK_BUTTON;
     }
 
     @Override
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewModalTaskState.java b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewModalTaskState.java
index 80fc5fa..0c0b4fd 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewModalTaskState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewModalTaskState.java
@@ -45,7 +45,7 @@
 
     @Override
     public int getVisibleElements(Launcher launcher) {
-        return OVERVIEW_ACTIONS | CLEAR_ALL_BUTTON;
+        return OVERVIEW_ACTIONS;
     }
 
     @Override
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
index 15216fe..5fdedcc 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
@@ -110,7 +110,7 @@
 
     @Override
     public int getVisibleElements(Launcher launcher) {
-        int elements = CLEAR_ALL_BUTTON | OVERVIEW_ACTIONS;
+        int elements = CLEAR_ALL_BUTTON | OVERVIEW_ACTIONS | ADD_DESK_BUTTON;
         DeviceProfile dp = launcher.getDeviceProfile();
         boolean showFloatingSearch;
         if (dp.isPhone) {
@@ -124,7 +124,7 @@
             elements |= FLOATING_SEARCH_BAR;
         }
         if (launcher.isSplitSelectionActive()) {
-            elements &= ~CLEAR_ALL_BUTTON;
+            elements &= ~CLEAR_ALL_BUTTON & ~ADD_DESK_BUTTON;
         }
         return elements;
     }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java b/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java
index 1907b4e..44f8bf1 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java
@@ -109,8 +109,11 @@
 
             // We sync the scrim fade with the taskbar animation duration to avoid any flickers for
             // taskbar icons disappearing before hotseat icons show up.
+            boolean isPinnedTaskbarAndNotInDesktopMode =
+                    isPinnedTaskbar && !DisplayController.isInDesktopMode(mContainer);
             float scrimUpperBoundFromSplit =
-                    QuickstepTransitionManager.getTaskbarToHomeDuration(isPinnedTaskbar)
+                    QuickstepTransitionManager.getTaskbarToHomeDuration(
+                            isPinnedTaskbarAndNotInDesktopMode)
                             / (float) config.duration;
             scrimUpperBoundFromSplit = Math.min(scrimUpperBoundFromSplit, 1f);
             config.setInterpolator(ANIM_OVERVIEW_ACTIONS_FADE, clampToProgress(LINEAR, 0, 0.25f));
@@ -142,7 +145,8 @@
                 if (mContainer.getDeviceProfile().isTaskbarPresent) {
                     config.duration = Math.min(
                             config.duration,
-                            QuickstepTransitionManager.getTaskbarToHomeDuration(isPinnedTaskbar));
+                            QuickstepTransitionManager.getTaskbarToHomeDuration(
+                                    isPinnedTaskbarAndNotInDesktopMode));
                 }
                 overview.snapToPage(DEFAULT_PAGE, Math.toIntExact(config.duration));
             } else {
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
index 05d12c3..58b274a 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
@@ -64,6 +64,7 @@
 import android.graphics.PointF;
 import android.view.MotionEvent;
 import android.view.animation.Interpolator;
+import android.window.DesktopModeFlags;
 
 import com.android.internal.jank.Cuj;
 import com.android.launcher3.LauncherState;
@@ -86,7 +87,6 @@
 import com.android.quickstep.util.WorkspaceRevealAnim;
 import com.android.quickstep.views.RecentsView;
 import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
-import com.android.window.flags.Flags;
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
 
 /**
@@ -188,7 +188,7 @@
         }
         if (DesktopModeStatus.canEnterDesktopMode(mLauncher)
                 //TODO(b/345296916): replace with dev option once in teamfood
-                && Flags.enableQuickswitchDesktopSplitBugfix()
+                && DesktopModeFlags.ENABLE_QUICKSWITCH_DESKTOP_SPLIT_BUGFIX.isTrue()
                 && mRecentsView.getNonDesktopTaskViewCount() < 1) {
             return false;
         }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewDismissTouchController.kt b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewDismissTouchController.kt
index 77a05c1..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
@@ -26,10 +28,13 @@
 import com.android.launcher3.Utilities.isRtl
 import com.android.launcher3.Utilities.mapToRange
 import com.android.launcher3.touch.SingleAxisSwipeDetector
+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
 import kotlin.math.abs
 import kotlin.math.sign
 
@@ -48,12 +53,15 @@
             recentsView.pagedOrientationHandler.upDownSwipeDirection,
         )
     private val isRtl = isRtl(container.resources)
+    private val tempTaskThumbnailBounds = Rect()
 
     private var taskBeingDragged: TaskView? = null
     private var springAnimation: SpringAnimation? = null
     private var dismissLength: Int = 0
     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 {
@@ -95,6 +103,7 @@
 
     private fun onActionDown(ev: MotionEvent): Boolean {
         springAnimation?.cancel()
+        recentsScaleAnimation?.cancel()
         if (!canInterceptTouch(ev)) {
             return false
         }
@@ -105,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
                 }
@@ -159,9 +170,32 @@
             }
             recentsView.redrawLiveTile()
         }
+        val dismissFraction = displacement / (dismissLength * verticalFactor).toFloat()
+        RECENTS_SCALE_PROPERTY.setValue(recentsView, getRecentsScale(dismissFraction))
+        playDismissThresholdHaptic(displacement)
         return true
     }
 
+    /**
+     * Play a haptic to alert the user they have passed the dismiss threshold.
+     *
+     * <p>Check within a range of the threshold value, as the drag event does not necessarily happen
+     * at the exact threshold's displacement.
+     */
+    private fun playDismissThresholdHaptic(displacement: Float) {
+        val dismissThreshold = (DISMISS_THRESHOLD_FRACTION * dismissLength * verticalFactor)
+        val inHapticRange =
+            displacement >= (dismissThreshold - DISMISS_THRESHOLD_HAPTIC_RANGE) &&
+                displacement <= (dismissThreshold + DISMISS_THRESHOLD_HAPTIC_RANGE)
+        if (!inHapticRange) {
+            hasDismissThresholdHapticRun = false
+        } else if (!hasDismissThresholdHapticRun) {
+            MSDLPlayerWrapper.INSTANCE.get(recentsView.context)
+                .playToken(MSDLToken.SWIPE_THRESHOLD_INDICATOR)
+            hasDismissThresholdHapticRun = true
+        }
+    }
+
     override fun onDragEnd(velocity: Float) {
         val taskBeingDragged = taskBeingDragged ?: return
 
@@ -192,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).
@@ -206,7 +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 f46f9ae..c51f659 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -120,6 +120,7 @@
 import com.android.launcher3.dragndrop.DragView;
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.logging.StatsLogManager.StatsLogger;
+import com.android.launcher3.statehandlers.DesktopVisibilityController;
 import com.android.launcher3.statemanager.BaseState;
 import com.android.launcher3.statemanager.StatefulContainer;
 import com.android.launcher3.taskbar.TaskbarThresholdUtils;
@@ -223,6 +224,7 @@
         mRecentsView = null;
         mContainer = null;
         mStateCallback.clearState(STATE_LAUNCHER_PRESENT);
+        mRecentsAnimationStartCallbacks.clear();
     };
 
     private static int FLAG_COUNT = 0;
@@ -1112,6 +1114,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 +1265,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 +1280,7 @@
             endTarget = calculateEndTargetForNonFling(velocityPxPerMs);
         }
 
+        Log.d(TAG, "calculateEndTarget: endTarget(1)=" + endTarget);
         if (mDeviceState.isOverviewDisabled() && endTarget == RECENTS) {
             return LAST_TASK;
         }
@@ -1293,6 +1299,7 @@
                 return LAST_TASK;
             }
         }
+        Log.d(TAG, "calculateEndTarget: endTarget(2)=" + endTarget);
         return endTarget;
     }
 
@@ -1301,9 +1308,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 +1326,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 +1338,7 @@
         } else if (isScrollingToNewTask) {
             return NEW_TASK;
         }
+        Log.d(TAG, "calculateEndTargetForNonFling: mCanSlowSwipeGoHome=" + mCanSlowSwipeGoHome);
         return velocity.y < 0 && mCanSlowSwipeGoHome ? HOME : LAST_TASK;
     }
 
@@ -1408,8 +1422,10 @@
         }
         if (endTarget == HOME) {
             boolean isPinnedTaskbar = DisplayController.isPinnedTaskbar(mContext);
+            boolean isNotInDesktop =  !DisplayController.isInDesktopMode(mContext);
             duration = mContainer != null && mContainer.getDeviceProfile().isTaskbarPresent
-                    ? QuickstepTransitionManager.getTaskbarToHomeDuration(isPinnedTaskbar)
+                    ? QuickstepTransitionManager.getTaskbarToHomeDuration(
+                    isPinnedTaskbar && isNotInDesktop)
                     : StaggeredWorkspaceAnim.DURATION_MS;
             SystemUiProxy.INSTANCE.get(mContext).updateContextualEduStats(
                     mGestureState.isTrackpadGesture(), GestureType.HOME);
@@ -1589,9 +1605,27 @@
             if (mParallelRunningAnim != null) {
                 mParallelRunningAnim.addListener(new AnimatorListenerAdapter() {
                     @Override
+                    public void onAnimationStart(Animator animation) {
+                        if (DisplayController.isInDesktopMode(mContext)
+                                && mGestureState.getEndTarget() == HOME) {
+                            // Set launcher animation started, so we don't notify from
+                            // desktop visibility controller
+                            DesktopVisibilityController.INSTANCE.get(
+                                    mContext).setLauncherAnimationRunning(true);
+                        }
+                    }
+
+                    @Override
                     public void onAnimationEnd(Animator animation) {
                         mParallelRunningAnim = null;
                         mStateCallback.setStateOnUiThread(STATE_PARALLEL_ANIM_FINISHED);
+                        // Swipe to home animation finished, notify DesktopVisibilityController
+                        // to recreate Taskbar
+                        if (DisplayController.isInDesktopMode(mContext)
+                                && mGestureState.getEndTarget() == HOME) {
+                            DesktopVisibilityController.INSTANCE.get(
+                                    mContext).onLauncherAnimationFromDesktopEnd();
+                        }
                     }
                 });
                 mParallelRunningAnim.start();
@@ -1678,7 +1712,6 @@
                 if (mHandOffAnimationToHome) {
                     handOffAnimation(velocityPxPerMs);
                 }
-
                 windowAnim[0].addAnimatorListener(new AnimationSuccessListener() {
                     @Override
                     public void onAnimationSuccess(Animator animator) {
@@ -2453,7 +2486,8 @@
     }
 
     @Override
-    public void onTasksAppeared(@NonNull RemoteAnimationTarget[] appearedTaskTargets) {
+    public void onTasksAppeared(@NonNull RemoteAnimationTarget[] appearedTaskTargets,
+            @Nullable TransitionInfo transitionInfo) {
         if (mRecentsAnimationController == null) {
             return;
         }
diff --git a/quickstep/src/com/android/quickstep/AllAppsActionManager.kt b/quickstep/src/com/android/quickstep/AllAppsActionManager.kt
index 6fd68d5..b807a4b 100644
--- a/quickstep/src/com/android/quickstep/AllAppsActionManager.kt
+++ b/quickstep/src/com/android/quickstep/AllAppsActionManager.kt
@@ -21,10 +21,16 @@
 import android.app.RemoteAction
 import android.content.Context
 import android.graphics.drawable.Icon
+import android.provider.Settings
+import android.provider.Settings.Secure.USER_SETUP_COMPLETE
 import android.view.accessibility.AccessibilityManager
 import com.android.launcher3.R
+import com.android.launcher3.util.SettingsCache
+import com.android.launcher3.util.SettingsCache.OnChangeListener
 import java.util.concurrent.Executor
 
+private val USER_SETUP_COMPLETE_URI = Settings.Secure.getUriFor(USER_SETUP_COMPLETE)
+
 /**
  * Registers a [RemoteAction] for toggling All Apps if needed.
  *
@@ -38,6 +44,12 @@
     private val createAllAppsPendingIntent: () -> PendingIntent,
 ) {
 
+    private val onSettingsChangeListener = OnChangeListener { v -> isUserSetupComplete = v }
+
+    init {
+        SettingsCache.INSTANCE[context].register(USER_SETUP_COMPLETE_URI, onSettingsChangeListener)
+    }
+
     /** `true` if home and overview are the same Activity. */
     var isHomeAndOverviewSame = false
         set(value) {
@@ -52,12 +64,27 @@
             updateSystemAction()
         }
 
+    /** `true` if the setup UI is visible. */
+    var isSetupUiVisible = false
+        set(value) {
+            field = value
+            updateSystemAction()
+        }
+
+    private var isUserSetupComplete =
+        SettingsCache.INSTANCE[context].getValue(USER_SETUP_COMPLETE_URI, 0)
+        set(value) {
+            field = value
+            updateSystemAction()
+        }
+
     /** `true` if the action should be registered. */
     var isActionRegistered = false
         private set
 
     private fun updateSystemAction() {
-        val shouldRegisterAction = isHomeAndOverviewSame || isTaskbarPresent
+        val isInSetupFlow = isSetupUiVisible || !isUserSetupComplete
+        val shouldRegisterAction = (isHomeAndOverviewSame || isTaskbarPresent) && !isInSetupFlow
         if (isActionRegistered == shouldRegisterAction) return
         isActionRegistered = shouldRegisterAction
 
@@ -84,8 +111,10 @@
         isActionRegistered = false
         context
             .getSystemService(AccessibilityManager::class.java)
-            ?.unregisterSystemAction(
-                GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS,
-            )
+            ?.unregisterSystemAction(GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS)
+        SettingsCache.INSTANCE[context].unregister(
+            USER_SETUP_COMPLETE_URI,
+            onSettingsChangeListener,
+        )
     }
 }
diff --git a/quickstep/src/com/android/quickstep/BaseActivityInterface.java b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
index 7cab751..549c2f8 100644
--- a/quickstep/src/com/android/quickstep/BaseActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
@@ -87,7 +87,7 @@
             // We were on our way to this state when we got canceled, end there instead.
             startState = stateFromGestureEndTarget(endTarget);
             if (DesktopVisibilityController.INSTANCE.get(activity)
-                    .areDesktopTasksVisibleAndNotInOverview()
+                    .isInDesktopModeAndNotInOverview(activity.getDisplayId())
                     && endTarget == LAST_TASK) {
                 // When we are cancelling the transition and going back to last task, move to
                 // rest state instead when desktop tasks are visible.
diff --git a/quickstep/src/com/android/quickstep/BaseContainerInterface.java b/quickstep/src/com/android/quickstep/BaseContainerInterface.java
index 6d588d9..c64067a 100644
--- a/quickstep/src/com/android/quickstep/BaseContainerInterface.java
+++ b/quickstep/src/com/android/quickstep/BaseContainerInterface.java
@@ -233,8 +233,10 @@
         if (endTarget != null) {
             // We were on our way to this state when we got canceled, end there instead.
             startState = stateFromGestureEndTarget(endTarget);
-            if (DesktopVisibilityController.INSTANCE.get(recentsView.getContext())
-                    .areDesktopTasksVisibleAndNotInOverview() && endTarget == LAST_TASK) {
+            final var context = recentsView.getContext();
+            if (DesktopVisibilityController.INSTANCE.get(context)
+                    .isInDesktopModeAndNotInOverview(context.getDisplayId())
+                    && endTarget == LAST_TASK) {
                 // When we are cancelling the transition and going back to last task, move to
                 // rest state instead when desktop tasks are visible.
                 // If a fullscreen task is visible, launcher goes to normal state when the
diff --git a/quickstep/src/com/android/quickstep/DesktopFullscreenDrawParams.kt b/quickstep/src/com/android/quickstep/DesktopFullscreenDrawParams.kt
index bafb0b2..444e77d 100644
--- a/quickstep/src/com/android/quickstep/DesktopFullscreenDrawParams.kt
+++ b/quickstep/src/com/android/quickstep/DesktopFullscreenDrawParams.kt
@@ -17,7 +17,7 @@
 package com.android.quickstep
 
 import android.content.Context
-import com.android.systemui.shared.system.QuickStepContract
+import com.android.launcher3.R
 
 // DesktopTaskView thumbnail's corner radius is independent of fullscreenProgress.
 open class DesktopFullscreenDrawParams
@@ -28,6 +28,6 @@
         // computeCornerRadius is used as cornerRadiusProvider, so
         // QuickStepContract::getWindowCornerRadius can be mocked properly.
         private fun computeCornerRadius(context: Context): Float =
-            QuickStepContract.getWindowCornerRadius(context)
+            context.resources.getDimension(R.dimen.desktop_windowing_freeform_rounded_corner_radius)
     }
 }
diff --git a/quickstep/src/com/android/quickstep/FallbackSwipeHandler.java b/quickstep/src/com/android/quickstep/FallbackSwipeHandler.java
index 331580c..7d8a53d 100644
--- a/quickstep/src/com/android/quickstep/FallbackSwipeHandler.java
+++ b/quickstep/src/com/android/quickstep/FallbackSwipeHandler.java
@@ -50,6 +50,7 @@
 import android.view.Surface;
 import android.view.SurfaceControl;
 import android.view.SurfaceControl.Transaction;
+import android.window.TransitionInfo;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -172,14 +173,15 @@
     }
 
     @Override
-    public void onTasksAppeared(@NonNull RemoteAnimationTarget[] appearedTaskTargets) {
+    public void onTasksAppeared(@NonNull RemoteAnimationTarget[] appearedTaskTargets,
+            @Nullable TransitionInfo transitionInfo) {
         if (mActiveAnimationFactory != null && mActiveAnimationFactory.handleHomeTaskAppeared(
                 appearedTaskTargets)) {
             mActiveAnimationFactory = null;
             return;
         }
 
-        super.onTasksAppeared(appearedTaskTargets);
+        super.onTasksAppeared(appearedTaskTargets, transitionInfo);
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java b/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
index be0a339..783ec2c 100644
--- a/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
+++ b/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
@@ -514,7 +514,13 @@
 
     private void finishAnimation() {
         mLauncher.setPredictiveBackToHomeInProgress(false);
+        if (mBackTarget != null && mBackTarget.leash.isValid()) {
+            mBackTarget.leash.release();
+        }
         mBackTarget = null;
+        if (mLauncherTarget != null && mLauncherTarget.leash.isValid()) {
+            mLauncherTarget.leash.release();
+        }
         mLauncherTarget = null;
         mBackInProgress = false;
         mBackProgress = 0;
diff --git a/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java b/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java
index e1e9c99..0a77688 100644
--- a/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java
+++ b/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java
@@ -107,7 +107,7 @@
                 && workspaceView.isAttachedToWindow()
                 && workspaceView.getHeight() > 0
                 && !DesktopVisibilityController.INSTANCE.get(mContainer)
-                        .areDesktopTasksVisibleAndNotInOverview();
+                        .isInDesktopModeAndNotInOverview(mContainer.getDisplayId());
 
         mContainer.getRootView().setForceHideBackArrow(true);
 
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/OverviewComponentObserver.java b/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
index 1f95c41..bc3de41 100644
--- a/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
+++ b/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
@@ -72,12 +72,9 @@
             new DaggerSingletonObject<>(LauncherAppComponent::getOverviewComponentObserver);
 
     // We register broadcast receivers on main thread to avoid missing updates.
-    private final SimpleBroadcastReceiver mUserPreferenceChangeReceiver =
-            new SimpleBroadcastReceiver(MAIN_EXECUTOR, this::updateOverviewTargets);
-    private final SimpleBroadcastReceiver mOtherHomeAppUpdateReceiver =
-            new SimpleBroadcastReceiver(MAIN_EXECUTOR, this::updateOverviewTargets);
+    private final SimpleBroadcastReceiver mUserPreferenceChangeReceiver;
+    private final SimpleBroadcastReceiver mOtherHomeAppUpdateReceiver;
 
-    private final Context mContext;
     private final RecentsDisplayModel mRecentsDisplayModel;
 
     private final Intent mCurrentHomeIntent;
@@ -101,10 +98,13 @@
             @ApplicationContext Context context,
             RecentsDisplayModel recentsDisplayModel,
             DaggerSingletonTracker lifecycleTracker) {
-        mContext = context;
+        mUserPreferenceChangeReceiver =
+                new SimpleBroadcastReceiver(context, MAIN_EXECUTOR, this::updateOverviewTargets);
+        mOtherHomeAppUpdateReceiver =
+                new SimpleBroadcastReceiver(context, MAIN_EXECUTOR, this::updateOverviewTargets);
         mRecentsDisplayModel = recentsDisplayModel;
         mCurrentHomeIntent = createHomeIntent();
-        mMyHomeIntent = new Intent(mCurrentHomeIntent).setPackage(mContext.getPackageName());
+        mMyHomeIntent = new Intent(mCurrentHomeIntent).setPackage(context.getPackageName());
         ResolveInfo info = context.getPackageManager().resolveActivity(mMyHomeIntent, 0);
         ComponentName myHomeComponent =
                 new ComponentName(context.getPackageName(), info.activityInfo.name);
@@ -112,7 +112,7 @@
         mConfigChangesMap.append(myHomeComponent.hashCode(), info.activityInfo.configChanges);
         mSetupWizardPkg = context.getString(R.string.setup_wizard_pkg);
 
-        ComponentName fallbackComponent = new ComponentName(mContext, RecentsActivity.class);
+        ComponentName fallbackComponent = new ComponentName(context, RecentsActivity.class);
         mFallbackIntent = new Intent(Intent.ACTION_MAIN)
                 .addCategory(Intent.CATEGORY_DEFAULT)
                 .setComponent(fallbackComponent)
@@ -124,7 +124,7 @@
             mConfigChangesMap.append(fallbackComponent.hashCode(), fallbackInfo.configChanges);
         } catch (PackageManager.NameNotFoundException ignored) { /* Impossible */ }
 
-        mUserPreferenceChangeReceiver.register(mContext, ACTION_PREFERRED_ACTIVITY_CHANGED);
+        mUserPreferenceChangeReceiver.register(ACTION_PREFERRED_ACTIVITY_CHANGED);
         updateOverviewTargets();
 
         lifecycleTracker.addCloseable(this::onDestroy);
@@ -224,7 +224,7 @@
 
                 mUpdateRegisteredPackage = defaultHome.getPackageName();
                 mOtherHomeAppUpdateReceiver.registerPkgActions(
-                        mContext, mUpdateRegisteredPackage, ACTION_PACKAGE_ADDED,
+                        mUpdateRegisteredPackage, ACTION_PACKAGE_ADDED,
                         ACTION_PACKAGE_CHANGED, ACTION_PACKAGE_REMOVED);
             }
         }
@@ -235,13 +235,13 @@
      * Clean up any registered receivers.
      */
     private void onDestroy() {
-        mUserPreferenceChangeReceiver.unregisterReceiverSafely(mContext);
+        mUserPreferenceChangeReceiver.unregisterReceiverSafely();
         unregisterOtherHomeAppUpdateReceiver();
     }
 
     private void unregisterOtherHomeAppUpdateReceiver() {
         if (mUpdateRegisteredPackage != null) {
-            mOtherHomeAppUpdateReceiver.unregisterReceiverSafely(mContext);
+            mOtherHomeAppUpdateReceiver.unregisterReceiverSafely();
             mUpdateRegisteredPackage = null;
         }
     }
diff --git a/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java b/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
index 9b0e75c..f47937c 100644
--- a/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
+++ b/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
@@ -177,7 +177,7 @@
 
             case TestProtocol.REQUEST_RECREATE_TASKBAR:
                 // Allow null-pointer to catch illegal states.
-                runOnTISBinder(tisBinder -> tisBinder.getTaskbarManager().recreateTaskbar());
+                runOnTISBinder(tisBinder -> tisBinder.getTaskbarManager().recreateTaskbars());
                 return response;
             case TestProtocol.REQUEST_TASKBAR_IME_DOCKED:
                 return getTISBinderUIProperty(Bundle::putBoolean, tisBinder ->
diff --git a/quickstep/src/com/android/quickstep/RecentTasksList.java b/quickstep/src/com/android/quickstep/RecentTasksList.java
index bb72408..a178c3c 100644
--- a/quickstep/src/com/android/quickstep/RecentTasksList.java
+++ b/quickstep/src/com/android/quickstep/RecentTasksList.java
@@ -21,7 +21,7 @@
 import static com.android.launcher3.Flags.enableSeparateExternalDisplayTasks;
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 import static com.android.quickstep.util.SplitScreenUtils.convertShellSplitBoundsToLauncher;
-import static com.android.wm.shell.shared.GroupedTaskInfo.TYPE_FREEFORM;
+import static com.android.wm.shell.shared.GroupedTaskInfo.TYPE_DESK;
 import static com.android.wm.shell.shared.GroupedTaskInfo.TYPE_SPLIT;
 
 import android.app.ActivityManager.RunningTaskInfo;
@@ -32,10 +32,12 @@
 import android.os.Process;
 import android.os.RemoteException;
 import android.util.SparseBooleanArray;
+import android.window.DesktopExperienceFlags;
 
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
+import com.android.launcher3.statehandlers.DesktopVisibilityController;
 import com.android.launcher3.util.LooperExecutor;
 import com.android.launcher3.util.SplitConfigurationOptions;
 import com.android.quickstep.util.DesktopTask;
@@ -351,15 +353,23 @@
 
         TaskLoadResult allTasks = new TaskLoadResult(requestId, loadKeysOnly, rawTasks.size());
 
-        int numVisibleTasks = 0;
+        boolean isFirstVisibleTaskFound = false;
         for (GroupedTaskInfo rawTask : rawTasks) {
-            if (rawTask.isBaseType(TYPE_FREEFORM)) {
-                // TYPE_FREEFORM tasks is only created when desktop mode can be entered,
-                // leftover TYPE_FREEFORM tasks created when flag was on should be ignored.
+            if (rawTask.isBaseType(TYPE_DESK)) {
+                // TYPE_DESK tasks is only created when desktop mode can be entered,
+                // leftover TYPE_DESK tasks created when flag was on should be ignored.
                 if (DesktopModeStatus.canEnterDesktopMode(mContext)) {
                     List<DesktopTask> desktopTasks = createDesktopTasks(
                             rawTask.getBaseGroupedTask());
                     allTasks.addAll(desktopTasks);
+
+                    // If any task in desktop group task is visible, set isFirstVisibleTaskFound to
+                    // true. This way if there is a transparent task in the list later on, it does
+                    // not get its own tile in Overview.
+                    if (rawTask.getBaseGroupedTask().getTaskInfoList().stream().anyMatch(
+                            taskInfo -> taskInfo.isVisible)) {
+                        isFirstVisibleTaskFound = true;
+                    }
                 }
                 continue;
             }
@@ -400,7 +410,7 @@
                                     tmpLockedUsers.get(task2Key.userId) /* isLocked */);
                 } else {
                     // Is fullscreen task
-                    if (numVisibleTasks > 0) {
+                    if (!isFirstVisibleTaskFound) {
                         boolean isExcluded = (taskInfo1.baseIntent.getFlags()
                                 & FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) != 0;
                         if (taskInfo1.isTopActivityTransparent && isExcluded) {
@@ -411,7 +421,7 @@
                     }
                 }
                 if (taskInfo1.isVisible) {
-                    numVisibleTasks++;
+                    isFirstVisibleTaskFound = true;
                 }
                 if (task2 != null) {
                     Objects.requireNonNull(rawTask.getSplitBounds());
@@ -442,7 +452,11 @@
         Set<Integer> minimizedTaskIds = minimizedTaskIdArray != null
                 ? CollectionsKt.toSet(ArraysKt.asIterable(minimizedTaskIdArray))
                 : Collections.emptySet();
-        if (enableSeparateExternalDisplayTasks()) {
+        if (enableSeparateExternalDisplayTasks()
+                && !DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue()) {
+            // This code is not needed when the multiple desktop feature is enabled, since Shell
+            // will send a single `GroupedTaskInfo` for each desk with a unique `deskId` across
+            // all displays.
             Map<Integer, List<Task>> perDisplayTasks = new HashMap<>();
             for (TaskInfo taskInfo : recentTaskInfo.getTaskInfoList()) {
                 Task task = createTask(taskInfo, minimizedTaskIds);
@@ -450,11 +464,16 @@
                         k -> new ArrayList<>());
                 tasks.add(task);
             }
-            return MapsKt.map(perDisplayTasks, it -> new DesktopTask(it.getValue()));
+            // When the multiple desktop feature is disabled, there can only be up to a single desk
+            // on each display, The desk ID doesn't matter and should not be used.
+            return MapsKt.map(perDisplayTasks,
+                    it -> new DesktopTask(DesktopVisibilityController.INACTIVE_DESK_ID,
+                            it.getValue()));
         } else {
+            final int deskId = recentTaskInfo.getDeskId();
             List<Task> tasks = CollectionsKt.map(recentTaskInfo.getTaskInfoList(),
                     it -> createTask(it, minimizedTaskIds));
-            return List.of(new DesktopTask(tasks));
+            return List.of(new DesktopTask(deskId, tasks));
         }
     }
 
diff --git a/quickstep/src/com/android/quickstep/RecentsActivity.java b/quickstep/src/com/android/quickstep/RecentsActivity.java
index fca67c3..3d12fdf 100644
--- a/quickstep/src/com/android/quickstep/RecentsActivity.java
+++ b/quickstep/src/com/android/quickstep/RecentsActivity.java
@@ -319,7 +319,7 @@
     /**
      * Composes the animations for a launch from the recents list if possible.
      */
-    private AnimatorSet  composeRecentsLaunchAnimator(
+    private AnimatorSet composeRecentsLaunchAnimator(
             @NonNull RecentsView recentsView,
             @NonNull TaskView taskView,
             RemoteAnimationTarget[] appTargets,
@@ -329,7 +329,8 @@
         boolean activityClosing = taskIsATargetWithMode(appTargets, getTaskId(), MODE_CLOSING);
         PendingAnimation pa = new PendingAnimation(RECENTS_LAUNCH_DURATION);
         createRecentsWindowAnimator(recentsView, taskView, !activityClosing, appTargets,
-                wallpaperTargets, nonAppTargets, null /* depthController */, pa);
+                wallpaperTargets, nonAppTargets, /* depthController= */ null ,
+                /* transitionInfo= */ null, pa);
         target.play(pa.buildAnim());
 
         // Found a visible recents task that matches the opening app, lets launch the app from there
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java b/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java
index c6b858b..d7152b5 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java
@@ -21,6 +21,7 @@
 import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER;
 
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.wm.shell.shared.TransitionUtil.TYPE_SPLIT_SCREEN_DIM_LAYER;
 
 import android.annotation.Nullable;
 import android.graphics.Rect;
@@ -163,11 +164,12 @@
 
     @BinderThread
     @Override
-    public void onTasksAppeared(RemoteAnimationTarget[] apps) {
+    public void onTasksAppeared(
+            RemoteAnimationTarget[] apps, @Nullable TransitionInfo transitionInfo) {
         Utilities.postAsyncCallback(MAIN_EXECUTOR.getHandler(), () -> {
             ActiveGestureProtoLogProxy.logRecentsAnimationCallbacksOnTasksAppeared();
             for (RecentsAnimationListener listener : getListeners()) {
-                listener.onTasksAppeared(apps);
+                listener.onTasksAppeared(apps, transitionInfo);
             }
         });
     }
@@ -189,7 +191,8 @@
             ArrayList<RemoteAnimationTarget> apps, ArrayList<RemoteAnimationTarget> nonApps) {
         for (int i = 0; i < appTargets.length; i++) {
             RemoteAnimationTarget target = appTargets[i];
-            if (target.windowType == TYPE_DOCK_DIVIDER) {
+            if (target.windowType == TYPE_DOCK_DIVIDER
+                    || target.windowType == TYPE_SPLIT_SCREEN_DIM_LAYER) {
                 nonApps.add(target);
             } else {
                 apps.add(target);
@@ -225,6 +228,7 @@
         /**
          * Callback made when a task started from the recents is ready for an app transition.
          */
-        default void onTasksAppeared(@NonNull RemoteAnimationTarget[] appearedTaskTarget) {}
+        default void onTasksAppeared(@NonNull RemoteAnimationTarget[] appearedTaskTarget,
+                @Nullable TransitionInfo transitionInfo) {}
     }
 }
diff --git a/quickstep/src/com/android/quickstep/RotationTouchHelper.java b/quickstep/src/com/android/quickstep/RotationTouchHelper.java
index a614327..ae6cfa0 100644
--- a/quickstep/src/com/android/quickstep/RotationTouchHelper.java
+++ b/quickstep/src/com/android/quickstep/RotationTouchHelper.java
@@ -142,15 +142,17 @@
         mContext = context;
         mDisplayController = displayController;
         mSystemUiProxy = systemUiProxy;
+        // TODO (b/398195845): this needs updating so non-default displays do not rotate with the
+        //  default display.
         mDisplayId = DEFAULT_DISPLAY;
 
         Resources resources = mContext.getResources();
         mOrientationTouchTransformer = new OrientationTouchTransformer(resources, mMode,
                 () -> QuickStepContract.getWindowCornerRadius(mContext));
 
-        // Register for navigation mode changes
-        mDisplayController.addChangeListener(this);
-        DisplayController.Info info = mDisplayController.getInfo();
+        // Register for navigation mode and rotation changes
+        mDisplayController.addChangeListenerForDisplay(this, mDisplayId);
+        DisplayController.Info info = mDisplayController.getInfoForDisplay(mDisplayId);
         onDisplayInfoChanged(context, info, CHANGE_ALL);
 
         mOrientationListener = new OrientationEventListener(mContext) {
@@ -174,7 +176,7 @@
         };
 
         lifeCycle.addCloseable(() -> {
-            mDisplayController.removeChangeListener(this);
+            mDisplayController.removeChangeListenerForDisplay(this, mDisplayId);
             mOrientationListener.disable();
             TaskStackChangeListeners.getInstance()
                     .unregisterTaskStackListener(mFrozenTaskListener);
@@ -201,7 +203,8 @@
             return;
         }
 
-        mOrientationTouchTransformer.createOrAddTouchRegion(mDisplayController.getInfo(),
+        mOrientationTouchTransformer.createOrAddTouchRegion(
+                mDisplayController.getInfoForDisplay(mDisplayId),
                 "RTH.updateGestureTouchRegions");
     }
 
@@ -258,7 +261,8 @@
 
         if ((flags & CHANGE_NAVIGATION_MODE) != 0) {
             NavigationMode newMode = info.getNavigationMode();
-            mOrientationTouchTransformer.setNavigationMode(newMode, mDisplayController.getInfo(),
+            mOrientationTouchTransformer.setNavigationMode(newMode,
+                    mDisplayController.getInfoForDisplay(mDisplayId),
                     mContext.getResources());
 
             TaskStackChangeListeners.getInstance()
@@ -280,7 +284,8 @@
      */
     void setGesturalHeight(int newGesturalHeight) {
         mOrientationTouchTransformer.setGesturalHeight(
-                newGesturalHeight, mDisplayController.getInfo(), mContext.getResources());
+                newGesturalHeight, mDisplayController.getInfoForDisplay(mDisplayId),
+                mContext.getResources());
     }
 
     /**
@@ -296,7 +301,8 @@
     }
 
     private void enableMultipleRegions(boolean enable) {
-        mOrientationTouchTransformer.enableMultipleRegions(enable, mDisplayController.getInfo());
+        mOrientationTouchTransformer.enableMultipleRegions(enable,
+                mDisplayController.getInfoForDisplay(mDisplayId));
         notifySysuiOfCurrentRotation(mOrientationTouchTransformer.getQuickStepStartingRotation());
         if (enable && !mInOverview && !TestProtocol.sDisableSensorRotation) {
             // Clear any previous state from sensor manager
@@ -359,7 +365,8 @@
      * notifies system UI of the primary rotation the user is interacting with
      */
     private void toggleSecondaryNavBarsForRotation() {
-        mOrientationTouchTransformer.setSingleActiveRegion(mDisplayController.getInfo());
+        mOrientationTouchTransformer.setSingleActiveRegion(
+                mDisplayController.getInfoForDisplay(mDisplayId));
         notifySysuiOfCurrentRotation(mOrientationTouchTransformer.getCurrentActiveRotation());
     }
 
diff --git a/quickstep/src/com/android/quickstep/SimpleOrientationTouchTransformer.java b/quickstep/src/com/android/quickstep/SimpleOrientationTouchTransformer.java
index d2a491d..de7fb89 100644
--- a/quickstep/src/com/android/quickstep/SimpleOrientationTouchTransformer.java
+++ b/quickstep/src/com/android/quickstep/SimpleOrientationTouchTransformer.java
@@ -15,6 +15,8 @@
  */
 package com.android.quickstep;
 
+import static android.view.Display.DEFAULT_DISPLAY;
+
 import static com.android.launcher3.util.DisplayController.CHANGE_ACTIVE_SCREEN;
 import static com.android.launcher3.util.DisplayController.CHANGE_ALL;
 import static com.android.launcher3.util.DisplayController.CHANGE_ROTATION;
@@ -42,15 +44,20 @@
     private OrientationRectF mOrientationRectF;
     private OrientationRectF mTouchingOrientationRectF;
     private int mViewRotation;
+    private final int mDisplayId;
 
     @Inject
     public SimpleOrientationTouchTransformer(@ApplicationContext Context context,
             DisplayController displayController,
             DaggerSingletonTracker tracker) {
-        displayController.addChangeListener(this);
-        tracker.addCloseable(() -> displayController.removeChangeListener(this));
+        // TODO (b/398195845): make sure non-default displays don't get affected by default display
+        // changes.
+        mDisplayId = DEFAULT_DISPLAY;
+        displayController.addChangeListenerForDisplay(this, mDisplayId);
+        tracker.addCloseable(
+                () -> displayController.removeChangeListenerForDisplay(this, mDisplayId));
 
-        onDisplayInfoChanged(context, displayController.getInfo(), CHANGE_ALL);
+        onDisplayInfoChanged(context, displayController.getInfoForDisplay(mDisplayId), CHANGE_ALL);
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.kt b/quickstep/src/com/android/quickstep/SystemUiProxy.kt
index a1ac39e..1f3eb2a 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.kt
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.kt
@@ -1218,8 +1218,12 @@
         override fun onAnimationCanceled(taskIds: IntArray?, taskSnapshots: Array<TaskSnapshot>?) =
             listener.onAnimationCanceled(wrap(taskIds, taskSnapshots))
 
-        override fun onTasksAppeared(apps: Array<RemoteAnimationTarget>?) =
-            listener.onTasksAppeared(apps)
+        override fun onTasksAppeared(
+            apps: Array<RemoteAnimationTarget>?,
+            transitionInfo: TransitionInfo?,
+        ) {
+            listener.onTasksAppeared(apps, transitionInfo)
+        }
     }
 
     //
diff --git a/quickstep/src/com/android/quickstep/TaskAnimationManager.java b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
index 63b8aaf..cb11afa 100644
--- a/quickstep/src/com/android/quickstep/TaskAnimationManager.java
+++ b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
@@ -231,7 +231,8 @@
             }
 
             @Override
-            public void onTasksAppeared(RemoteAnimationTarget[] appearedTaskTargets) {
+            public void onTasksAppeared(RemoteAnimationTarget[] appearedTaskTargets,
+                    @Nullable TransitionInfo transitionInfo) {
                 RemoteAnimationTarget appearedTaskTarget = appearedTaskTargets[0];
                 BaseContainerInterface containerInterface =
                         mLastGestureState.getContainerInterface();
@@ -264,7 +265,8 @@
                         recentsView.launchSideTaskInLiveTileMode(appearedTaskTarget.taskId,
                                 appearedTaskTargets,
                                 new RemoteAnimationTarget[0] /* wallpaper */,
-                                nonAppTargets /* nonApps */);
+                                nonAppTargets /* nonApps */,
+                                transitionInfo);
                         return;
                     } else {
                         ActiveGestureProtoLogProxy.logLaunchingSideTaskFailed();
@@ -284,8 +286,32 @@
         mCallbacks.addListener(listener);
 
         final ActivityOptions options = ActivityOptions.makeBasic();
+        options.setPendingIntentBackgroundActivityStartMode(
+                ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS);
+        options.setTransientLaunch();
+        options.setSourceInfo(ActivityOptions.SourceInfo.TYPE_RECENTS_ANIMATION, eventTime);
 
-        // TODO:(b/365777482) if flag is enabled, but on launcher it will crash.
+        // Notify taskbar that we should skip reacting to launcher visibility change to
+        // avoid a jumping taskbar.
+        TaskbarUIController taskbarUIController = containerInterface.getTaskbarController();
+        if (enableScalingRevealHomeAnimation() && taskbarUIController != null) {
+            taskbarUIController.setSkipLauncherVisibilityChange(true);
+
+            mCallbacks.addListener(new RecentsAnimationCallbacks.RecentsAnimationListener() {
+                @Override
+                public void onRecentsAnimationCanceled(
+                        @NonNull HashMap<Integer, ThumbnailData> thumbnailDatas) {
+                    taskbarUIController.setSkipLauncherVisibilityChange(false);
+                }
+
+                @Override
+                public void onRecentsAnimationFinished(
+                        @NonNull RecentsAnimationController controller) {
+                    taskbarUIController.setSkipLauncherVisibilityChange(false);
+                }
+            });
+        }
+
         if(containerInterface.getCreatedContainer() instanceof RecentsWindowManager
                 && (Flags.enableFallbackOverviewInWindow()
                         || Flags.enableLauncherOverviewInWindow())) {
@@ -295,32 +321,6 @@
                     .getRecentsWindowManager(mDeviceState.getDisplayId())
                     .startRecentsWindow(mCallbacks);
         } else {
-            options.setPendingIntentBackgroundActivityStartMode(
-                    ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS);
-            options.setTransientLaunch();
-            options.setSourceInfo(ActivityOptions.SourceInfo.TYPE_RECENTS_ANIMATION, eventTime);
-
-            // Notify taskbar that we should skip reacting to launcher visibility change to
-            // avoid a jumping taskbar.
-            TaskbarUIController taskbarUIController = containerInterface.getTaskbarController();
-            if (enableScalingRevealHomeAnimation() && taskbarUIController != null) {
-                taskbarUIController.setSkipLauncherVisibilityChange(true);
-
-                mCallbacks.addListener(new RecentsAnimationCallbacks.RecentsAnimationListener() {
-                    @Override
-                    public void onRecentsAnimationCanceled(
-                            @NonNull HashMap<Integer, ThumbnailData> thumbnailDatas) {
-                        taskbarUIController.setSkipLauncherVisibilityChange(false);
-                    }
-
-                    @Override
-                    public void onRecentsAnimationFinished(
-                            @NonNull RecentsAnimationController controller) {
-                        taskbarUIController.setSkipLauncherVisibilityChange(false);
-                    }
-                });
-            }
-
             mRecentsAnimationStartPending = getSystemUiProxy().startRecentsActivity(intent,
                     options, mCallbacks, false /* useSyntheticRecentsTransition */);
         }
diff --git a/quickstep/src/com/android/quickstep/TaskIconCache.kt b/quickstep/src/com/android/quickstep/TaskIconCache.kt
index 6a7f1af..f0b9b7b 100644
--- a/quickstep/src/com/android/quickstep/TaskIconCache.kt
+++ b/quickstep/src/com/android/quickstep/TaskIconCache.kt
@@ -72,6 +72,8 @@
     var taskVisualsChangeListener: TaskVisualsChangeListener? = null
 
     init {
+        // TODO (b/397205964): this will need to be updated when we support caches for different
+        //  displays.
         displayController.addChangeListener(this)
     }
 
diff --git a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
index 7990aae..f92581e 100644
--- a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
+++ b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
@@ -236,15 +236,14 @@
                         position[0] + width, position[1] + height);
 
                 // Take the thumbnail of the task without a scrim and apply it back after
-                // TODO(b/348643341) add ability to get override the scrim for this Bitmap retrieval
-                float alpha = 0f;
-                if (!enableRefactorTaskThumbnail()) {
-                    alpha = mTaskContainer.getThumbnailViewDeprecated().getDimAlpha();
+                Bitmap thumbnail;
+                if (enableRefactorTaskThumbnail()) {
+                    thumbnail = mTaskContainer.getThumbnail();
+                } else {
+                    float alpha = mTaskContainer.getThumbnailViewDeprecated().getDimAlpha();
                     mTaskContainer.getThumbnailViewDeprecated().setDimAlpha(0);
-                }
-                Bitmap thumbnail = RecentsTransition.drawViewIntoHardwareBitmap(
-                        taskBounds.width(), taskBounds.height(), snapShotView, 1f, Color.BLACK);
-                if (!enableRefactorTaskThumbnail()) {
+                    thumbnail = RecentsTransition.drawViewIntoHardwareBitmap(
+                            taskBounds.width(), taskBounds.height(), snapShotView, 1f, Color.BLACK);
                     mTaskContainer.getThumbnailViewDeprecated().setDimAlpha(alpha);
                 }
 
@@ -313,7 +312,7 @@
             RecentsView<?, ?> recentsView = taskView.getRecentsView();
             if (recentsView != null) {
                 dismissTaskMenuView();
-                recentsView.dismissTask(taskView, true, true);
+                recentsView.dismissTaskView(taskView, true, true);
                 mTarget.getStatsLogManager().logger().withItemInfo(mTaskContainer.getItemInfo())
                         .log(LAUNCHER_SYSTEM_SHORTCUT_CLOSE_APP_TAP);
             }
diff --git a/quickstep/src/com/android/quickstep/TaskViewUtils.java b/quickstep/src/com/android/quickstep/TaskViewUtils.java
index e47223b..37c2d1c 100644
--- a/quickstep/src/com/android/quickstep/TaskViewUtils.java
+++ b/quickstep/src/com/android/quickstep/TaskViewUtils.java
@@ -38,7 +38,9 @@
 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;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -64,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;
@@ -101,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.
      *
@@ -163,39 +171,39 @@
     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,
             @NonNull RemoteAnimationTarget[] nonAppTargets,
             @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=*/ null);
-            } else if (v.containsMultipleTasks()) {
+                remoteTargetHandles = gluer.assignTargetsForDesktop(targets, transitionInfo);
+            } else if (taskView.containsMultipleTasks()) {
                 remoteTargetHandles = gluer.assignTargetsForSplitScreen(targets,
-                        ((GroupedTaskView) v).getSplitBoundsConfig());
+                        ((GroupedTaskView) taskView).getSplitBoundsConfig());
             } else {
                 remoteTargetHandles = gluer.assignTargets(targets);
             }
@@ -209,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();
@@ -218,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();
@@ -236,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
@@ -259,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) {
@@ -270,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(() -> {
@@ -320,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) {
@@ -339,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
@@ -462,7 +485,7 @@
         final RecentsView recentsView = launchingTaskView.getRecentsView();
         composeRecentsLaunchAnimator(animatorSet, launchingTaskView, appTargets, wallpaperTargets,
                 nonAppTargets, /* launcherClosing */ true, stateManager, recentsView,
-                depthController);
+                depthController, /* transitionInfo= */ null);
 
         t.apply();
         animatorSet.start();
@@ -501,7 +524,7 @@
             composeRecentsLaunchAnimator(animatorSet, launchingTaskView,
                     appTargets, wallpaperTargets, nonAppTargets,
                     true, stateManager,
-                    recentsView, depthController);
+                    recentsView, depthController, /* transitionInfo= */ null);
             animatorSet.start();
             return;
         }
@@ -593,7 +616,7 @@
 
         composeRecentsLaunchAnimator(animatorSet, launchingTaskView, apps, wallpaper, nonApps,
                 true /* launcherClosing */, stateManager, launchingTaskView.getRecentsView(),
-                depthController);
+                depthController, transitionInfo);
 
         return animatorSet;
     }
@@ -603,13 +626,13 @@
             @NonNull RemoteAnimationTarget[] wallpaperTargets,
             @NonNull RemoteAnimationTarget[] nonAppTargets, boolean launcherClosing,
             @NonNull StateManager stateManager, @NonNull RecentsView recentsView,
-            @Nullable DepthController depthController) {
+            @Nullable DepthController depthController, @Nullable TransitionInfo transitionInfo) {
         boolean skipLauncherChanges = !launcherClosing;
 
         TaskView taskView = findTaskViewToLaunch(recentsView, v, appTargets);
         PendingAnimation pa = new PendingAnimation(RECENTS_LAUNCH_DURATION);
         createRecentsWindowAnimator(recentsView, taskView, skipLauncherChanges, appTargets,
-                wallpaperTargets, nonAppTargets, depthController, pa);
+                wallpaperTargets, nonAppTargets, depthController, transitionInfo, pa);
         if (launcherClosing) {
             // TODO(b/182592057): differentiate between "restore split" vs "launch fullscreen app"
             TaskViewUtils.createSplitAuxiliarySurfacesAnimator(nonAppTargets, true /*shown*/,
@@ -732,7 +755,9 @@
         List<SurfaceControl> auxiliarySurfaces = new ArrayList<>();
         for (RemoteAnimationTarget target : nonApps) {
             final SurfaceControl leash = target.leash;
-            if (target.windowType == TYPE_DOCK_DIVIDER && leash != null && leash.isValid()) {
+            if ((target.windowType == TYPE_DOCK_DIVIDER
+                    || target.windowType == TYPE_SPLIT_SCREEN_DIM_LAYER)
+                    && leash != null && leash.isValid()) {
                 auxiliarySurfaces.add(leash);
             }
         }
diff --git a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java
index b4b80c5..537092f 100644
--- a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java
+++ b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java
@@ -18,6 +18,7 @@
 import static com.android.app.animation.Interpolators.FINAL_FRAME;
 import static com.android.app.animation.Interpolators.INSTANT;
 import static com.android.app.animation.Interpolators.LINEAR;
+import static com.android.launcher3.Flags.enableDesktopExplodedView;
 import static com.android.launcher3.Flags.enableLargeDesktopWindowingTile;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_MODAL;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SCALE;
@@ -25,6 +26,7 @@
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_TRANSLATE_Y;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_SCRIM_FADE;
 import static com.android.launcher3.states.StateAnimationConfig.SKIP_OVERVIEW;
+import static com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE;
 import static com.android.quickstep.fallback.RecentsState.OVERVIEW_SPLIT_SELECT;
 import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_HORIZONTAL_OFFSET;
 import static com.android.quickstep.views.RecentsView.DESKTOP_CAROUSEL_DETACH_PROGRESS;
@@ -36,6 +38,7 @@
 import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_SPLIT_TRANSLATION;
 import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_TRANSLATION;
 import static com.android.quickstep.views.RecentsView.TASK_THUMBNAIL_SPLASH_ALPHA;
+import static com.android.quickstep.views.RecentsViewUtils.DESK_EXPLODE_PROGRESS;
 import static com.android.quickstep.views.TaskView.FLAG_UPDATE_ALL;
 
 import android.util.FloatProperty;
@@ -97,6 +100,11 @@
         float clearAllButtonAlpha = state.hasClearAllButton() ? 1 : 0;
         setter.setFloat(mRecentsView.getClearAllButton(), ClearAllButton.VISIBILITY_ALPHA,
                 clearAllButtonAlpha, LINEAR);
+        if (mRecentsView.getAddDeskButton() != null) {
+            float addDeskButtonAlpha = state.hasAddDeskButton() ? 1 : 0;
+            setter.setFloat(mRecentsView.getAddDeskButton().getVisibilityAlphaProperty(),
+                    MULTI_PROPERTY_VALUE, addDeskButtonAlpha, LINEAR);
+        }
         float overviewButtonAlpha = state.hasOverviewActions() ? 1 : 0;
         setter.setFloat(mRecentsViewContainer.getActionsView().getVisibilityAlpha(),
                 AnimatedFloat.VALUE, overviewButtonAlpha, LINEAR);
@@ -123,6 +131,10 @@
                     state.detachDesktopCarousel() ? 1f : 0f,
                     getOverviewInterpolator(state));
         }
+        if (enableDesktopExplodedView()) {
+            setter.setFloat(mRecentsView, DESK_EXPLODE_PROGRESS, showAsGrid ? 1f : 0f,
+                    getOverviewInterpolator(state));
+        }
 
         setter.setViewBackgroundColor(mRecentsViewContainer.getScrimView(),
                 state.getScrimColor(mRecentsViewContainer.asContext()),
diff --git a/quickstep/src/com/android/quickstep/fallback/RecentsState.java b/quickstep/src/com/android/quickstep/fallback/RecentsState.java
index f27b60c..f722c5d 100644
--- a/quickstep/src/com/android/quickstep/fallback/RecentsState.java
+++ b/quickstep/src/com/android/quickstep/fallback/RecentsState.java
@@ -45,14 +45,16 @@
     private static final int FLAG_RECENTS_VIEW_VISIBLE = BaseState.getFlag(7);
     private static final int FLAG_TASK_THUMBNAIL_SPLASH = BaseState.getFlag(8);
     private static final int FLAG_DETACH_DESKTOP_CAROUSEL = BaseState.getFlag(9);
+    private static final int FLAG_ADD_DESK_BUTTON = BaseState.getFlag(10);
 
     private static final RecentsState[] sAllStates = new RecentsState[6];
 
     public static final RecentsState DEFAULT = new RecentsState(0,
             FLAG_DISABLE_RESTORE | FLAG_CLEAR_ALL_BUTTON | FLAG_OVERVIEW_ACTIONS | FLAG_SHOW_AS_GRID
-                    | FLAG_SCRIM | FLAG_LIVE_TILE | FLAG_RECENTS_VIEW_VISIBLE);
+                    | FLAG_SCRIM | FLAG_LIVE_TILE | FLAG_RECENTS_VIEW_VISIBLE
+                    | FLAG_ADD_DESK_BUTTON);
     public static final RecentsState MODAL_TASK = new ModalState(1,
-            FLAG_DISABLE_RESTORE | FLAG_CLEAR_ALL_BUTTON | FLAG_OVERVIEW_ACTIONS | FLAG_MODAL
+            FLAG_DISABLE_RESTORE | FLAG_OVERVIEW_ACTIONS | FLAG_MODAL
                     | FLAG_SHOW_AS_GRID | FLAG_SCRIM | FLAG_LIVE_TILE | FLAG_RECENTS_VIEW_VISIBLE);
     public static final RecentsState BACKGROUND_APP = new BackgroundAppState(2,
             FLAG_DISABLE_RESTORE | FLAG_NON_INTERACTIVE | FLAG_FULL_SCREEN
@@ -122,6 +124,13 @@
     }
 
     /**
+     * For this state, whether add desk button should be shown.
+     */
+    public boolean hasAddDeskButton() {
+        return hasFlag(FLAG_ADD_DESK_BUTTON);
+    }
+
+    /**
      * For this state, whether overview actions should be shown.
      */
     public boolean hasOverviewActions() {
diff --git a/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowContext.kt b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowContext.kt
index 047658c..cd48136 100644
--- a/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowContext.kt
+++ b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowContext.kt
@@ -61,9 +61,13 @@
         return dragLayer
     }
 
+    fun initDeviceProfile() {
+        deviceProfile = InvariantDeviceProfile.INSTANCE[this].getDeviceProfile(this)
+    }
+
     override fun getDeviceProfile(): DeviceProfile {
         if (deviceProfile == null) {
-            deviceProfile = InvariantDeviceProfile.INSTANCE[this].getDeviceProfile(this).copy(this)
+            initDeviceProfile()
         }
         return deviceProfile!!
     }
@@ -79,7 +83,10 @@
      * @param type The window type to pass to the created WindowManager.LayoutParams.
      * @param title The window title to pass to the created WindowManager.LayoutParams.
      */
-    fun createDefaultWindowLayoutParams(type: Int, title: String): WindowManager.LayoutParams {
+    private fun createDefaultWindowLayoutParams(
+        type: Int,
+        title: String,
+    ): WindowManager.LayoutParams {
         var windowFlags =
             (WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS or
                 WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS or
diff --git a/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowManager.kt b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowManager.kt
index 07288d8..1f4961a 100644
--- a/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowManager.kt
+++ b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowManager.kt
@@ -21,6 +21,7 @@
 import android.content.ComponentName
 import android.content.Context
 import android.content.LocusId
+import android.content.res.Configuration
 import android.os.Bundle
 import android.view.KeyEvent
 import android.view.LayoutInflater
@@ -32,6 +33,7 @@
 import android.view.WindowManager
 import android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
 import android.window.RemoteTransition
+import com.android.launcher3.AbstractFloatingView
 import com.android.launcher3.BaseActivity
 import com.android.launcher3.LauncherAnimationRunner
 import com.android.launcher3.LauncherAnimationRunner.RemoteAnimationFactory
@@ -135,73 +137,6 @@
         listOf(RunnableList(), RunnableList(), RunnableList(), RunnableList())
     private var onInitListener: Predicate<Boolean>? = null
 
-    private val taskStackChangeListener =
-        object : TaskStackChangeListener {
-            override fun onTaskMovedToFront(taskId: Int) {
-                if ((isShowing() && isInState(DEFAULT))) {
-                    // handling state where we end recents animation by swiping livetile away
-                    // TODO: animate this switch.
-                    cleanupRecentsWindow()
-                }
-            }
-        }
-
-    private val recentsAnimationListener =
-        object : RecentsAnimationListener {
-            override fun onRecentsAnimationCanceled(thumbnailDatas: HashMap<Int, ThumbnailData>) {
-                recentAnimationStopped()
-            }
-
-            override fun onRecentsAnimationFinished(controller: RecentsAnimationController) {
-                recentAnimationStopped()
-            }
-        }
-
-    init {
-        TaskStackChangeListeners.getInstance().registerTaskStackListener(taskStackChangeListener)
-    }
-
-    override fun destroy() {
-        super.destroy()
-        cleanupRecentsWindow()
-        TaskStackChangeListeners.getInstance().unregisterTaskStackListener(taskStackChangeListener)
-        callbacks?.removeListener(recentsAnimationListener)
-        recentsWindowTracker.onContextDestroyed(this)
-        recentsView?.destroy()
-    }
-
-    override fun startHome() {
-        startHome(/* finishRecentsAnimation= */ true)
-    }
-
-    fun startHome(finishRecentsAnimation: Boolean) {
-        val recentsView: RecentsView<*, *> = getOverviewPanel()
-
-        if (!finishRecentsAnimation) {
-            recentsView.switchToScreenshot(/* onFinishRunnable= */ null)
-            startHomeInternal()
-            return
-        }
-        recentsView.switchToScreenshot {
-            recentsView.finishRecentsAnimation(/* toRecents= */ true) { startHomeInternal() }
-        }
-    }
-
-    private fun startHomeInternal() {
-        val runner = LauncherAnimationRunner(mainThreadHandler, animationToHomeFactory, true)
-        val options =
-            ActivityOptions.makeRemoteAnimation(
-                RemoteAnimationAdapter(runner, HOME_APPEAR_DURATION, 0),
-                RemoteTransition(
-                    runner.toRemoteTransition(),
-                    iApplicationThread,
-                    "StartHomeFromRecents",
-                ),
-            )
-        OverviewComponentObserver.startHomeIntentSafely(this, options.toBundle(), TAG)
-        stateManager.moveToRestState()
-    }
-
     private val animationToHomeFactory =
         RemoteAnimationFactory {
             _: Int,
@@ -229,7 +164,7 @@
                 anim,
                 this@RecentsWindowManager,
                 {
-                    getStateManager().goToState(BG_LAUNCHER, false)
+                    getStateManager().goToState(BG_LAUNCHER, true)
                     cleanupRecentsWindow()
                 },
                 true, /* skipFirstFrame */
@@ -242,17 +177,49 @@
         TestLogging.recordEvent(SEQUENCE_MAIN, "onBackInvoked")
     }
 
-    private fun cleanupRecentsWindow() {
-        RecentsWindowProtoLogProxy.logCleanup(isShowing())
-        if (isShowing()) {
-            windowManager.removeViewImmediate(windowView)
+    private val taskStackChangeListener =
+        object : TaskStackChangeListener {
+            override fun onTaskMovedToFront(taskId: Int) {
+                if ((isShowing() && isInState(DEFAULT))) {
+                    // handling state where we end recents animation by swiping livetile away
+                    // TODO: animate this switch.
+                    cleanupRecentsWindow()
+                }
+            }
         }
-        stateManager.moveToRestState()
-        callbacks?.removeListener(recentsAnimationListener)
+
+    private val recentsAnimationListener =
+        object : RecentsAnimationListener {
+            override fun onRecentsAnimationCanceled(thumbnailDatas: HashMap<Int, ThumbnailData>) {
+                recentAnimationStopped()
+            }
+
+            override fun onRecentsAnimationFinished(controller: RecentsAnimationController) {
+                recentAnimationStopped()
+            }
+        }
+
+    init {
+        TaskStackChangeListeners.getInstance().registerTaskStackListener(taskStackChangeListener)
     }
 
-    private fun isShowing(): Boolean {
-        return windowView?.parent != null
+    override fun handleConfigurationChanged(configuration: Configuration?) {
+        initDeviceProfile()
+        AbstractFloatingView.closeOpenViews(
+            this,
+            true,
+            AbstractFloatingView.TYPE_ALL and AbstractFloatingView.TYPE_REBIND_SAFE.inv(),
+        )
+        dispatchDeviceProfileChanged()
+    }
+
+    override fun destroy() {
+        super.destroy()
+        cleanupRecentsWindow()
+        TaskStackChangeListeners.getInstance().unregisterTaskStackListener(taskStackChangeListener)
+        callbacks?.removeListener(recentsAnimationListener)
+        recentsWindowTracker.onContextDestroyed(this)
+        recentsView?.destroy()
     }
 
     fun startRecentsWindow(callbacks: RecentsAnimationCallbacks? = null) {
@@ -301,6 +268,51 @@
         callbacks?.addListener(recentsAnimationListener)
     }
 
+    override fun startHome() {
+        startHome(/* finishRecentsAnimation= */ true)
+    }
+
+    fun startHome(finishRecentsAnimation: Boolean) {
+        val recentsView: RecentsView<*, *> = getOverviewPanel()
+
+        if (!finishRecentsAnimation) {
+            recentsView.switchToScreenshot /* onFinishRunnable= */ {}
+            startHomeInternal()
+            return
+        }
+        recentsView.switchToScreenshot {
+            recentsView.finishRecentsAnimation(/* toRecents= */ true) { startHomeInternal() }
+        }
+    }
+
+    private fun startHomeInternal() {
+        val runner = LauncherAnimationRunner(mainThreadHandler, animationToHomeFactory, true)
+        val options =
+            ActivityOptions.makeRemoteAnimation(
+                RemoteAnimationAdapter(runner, HOME_APPEAR_DURATION, 0),
+                RemoteTransition(
+                    runner.toRemoteTransition(),
+                    iApplicationThread,
+                    "StartHomeFromRecents",
+                ),
+            )
+        OverviewComponentObserver.startHomeIntentSafely(this, options.toBundle(), TAG)
+        stateManager.moveToRestState()
+    }
+
+    private fun cleanupRecentsWindow() {
+        RecentsWindowProtoLogProxy.logCleanup(isShowing())
+        if (isShowing()) {
+            windowManager.removeViewImmediate(windowView)
+        }
+        stateManager.moveToRestState()
+        callbacks?.removeListener(recentsAnimationListener)
+    }
+
+    private fun isShowing(): Boolean {
+        return windowView?.parent != null
+    }
+
     private fun recentAnimationStopped() {
         if (isInState(BACKGROUND_APP)) {
             cleanupRecentsWindow()
@@ -352,7 +364,6 @@
     override fun onStateSetEnd(state: RecentsState) {
         super.onStateSetEnd(state)
         RecentsWindowProtoLogProxy.logOnStateSetEnd(getStateName(state))
-
         if (state == HOME || state == BG_LAUNCHER) {
             cleanupRecentsWindow()
         }
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java
index afe988d..a703c23 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java
@@ -19,6 +19,7 @@
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_DEEP_PRESS_STASHED_TASKBAR;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LONG_PRESS_NAVBAR;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LONG_PRESS_STASHED_TASKBAR;
+import static com.android.launcher3.logging.StatsLogManager.LauncherLatencyEvent.LAUNCHER_LATENCY_CONTEXTUAL_SEARCH_LPNH_ABANDON;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.LogConfig.NAV_HANDLE_LONG_PRESS;
 
@@ -30,6 +31,7 @@
 import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.Utilities;
+import com.android.launcher3.logging.InstanceIdSequence;
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.util.DisplayController;
 import com.android.quickstep.DeviceConfigWrapper;
@@ -49,6 +51,8 @@
     private static final String TAG = "NavHandleLongPressIC";
     private static final boolean DEBUG_NAV_HANDLE = Utilities.isPropertyEnabled(
             NAV_HANDLE_LONG_PRESS);
+    // Minimum time between touch down and abandon to log.
+    @VisibleForTesting static final long MIN_TIME_TO_LOG_ABANDON_MS = 200;
 
     private NavHandleLongPressHandler mNavHandleLongPressHandler;
     private final float mNavHandleWidth;
@@ -62,11 +66,12 @@
     private final int mOuterLongPressTimeout;
     private final boolean mDeepPressEnabled;
     private final NavHandle mNavHandle;
-    private final StatsLogManager mStatsLogManager;
+    private StatsLogManager mStatsLogManager;
     private final TopTaskTracker mTopTaskTracker;
     private final GestureState mGestureState;
 
-    private MotionEvent mCurrentDownEvent;
+    private MotionEvent mCurrentDownEvent;  // Down event that started the current gesture.
+    private MotionEvent mCurrentMotionEvent;  // Most recent motion event.
     private boolean mDeepPressLogged;  // Whether deep press has been logged for the current touch.
 
     public NavHandleLongPressInputConsumer(Context context, InputConsumer delegate,
@@ -125,6 +130,10 @@
 
     @Override
     public void onMotionEvent(MotionEvent ev) {
+        if (mCurrentMotionEvent != null) {
+            mCurrentMotionEvent.recycle();
+        }
+        mCurrentMotionEvent = MotionEvent.obtain(ev);
         if (mDelegate.allowInterceptByParent()) {
             handleMotionEvent(ev);
         } else if (MAIN_EXECUTOR.getHandler().hasCallbacks(mTriggerLongPress)) {
@@ -244,6 +253,15 @@
         if (DEBUG_NAV_HANDLE) {
             Log.d(TAG, "cancelLongPress: " + reason);
         }
+        // Log LPNH abandon latency if we didn't trigger but were still prepared to.
+        long latencyMs = mCurrentMotionEvent.getEventTime() - mCurrentDownEvent.getEventTime();
+        if (mState != STATE_ACTIVE && MAIN_EXECUTOR.getHandler().hasCallbacks(mTriggerLongPress)
+                && latencyMs >= MIN_TIME_TO_LOG_ABANDON_MS) {
+            mStatsLogManager.latencyLogger()
+                    .withInstanceId(new InstanceIdSequence().newInstanceId())
+                    .withLatency(latencyMs)
+                    .log(LAUNCHER_LATENCY_CONTEXTUAL_SEARCH_LPNH_ABANDON);
+        }
         mGestureState.setIsInExtendedSlopRegion(false);
         MAIN_EXECUTOR.getHandler().removeCallbacks(mTriggerLongPress);
         mNavHandleLongPressHandler.onTouchFinished(mNavHandle, reason);
@@ -274,4 +292,9 @@
     void setNavHandleLongPressHandler(NavHandleLongPressHandler navHandleLongPressHandler) {
         mNavHandleLongPressHandler = navHandleLongPressHandler;
     }
+
+    @VisibleForTesting
+    void setStatsLogManager(StatsLogManager statsLogManager) {
+        mStatsLogManager = statsLogManager;
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java b/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java
index c986b88..c1bb250 100644
--- a/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java
+++ b/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java
@@ -64,6 +64,7 @@
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.R;
+import com.android.launcher3.RemoveAnimationSettingsTracker;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimatedFloat;
 import com.android.launcher3.anim.AnimatorPlaybackController;
@@ -92,6 +93,8 @@
     private static final String LOG_TAG = "AllSetActivity";
     private static final String URI_SYSTEM_NAVIGATION_SETTING =
             "#Intent;action=com.android.settings.SEARCH_RESULT_TRAMPOLINE;S.:settings:fragment_args_key=gesture_system_navigation_input_summary;S.:settings:show_fragment=com.android.settings.gestures.SystemNavigationGestureSettings;end";
+    private static final String INTENT_ACTION_ACTIVITY_CLOSED =
+            "com.android.quickstep.interaction.ACTION_ALL_SET_ACTIVITY_CLOSED";
     private static final String EXTRA_ACCENT_COLOR_DARK_MODE = "suwColorAccentDark";
     private static final String EXTRA_ACCENT_COLOR_LIGHT_MODE = "suwColorAccentLight";
     private static final String EXTRA_DEVICE_NAME = "suwDeviceName";
@@ -105,6 +108,9 @@
 
     private static final float ANIMATION_PAUSE_ALPHA_THRESHOLD = 0.1f;
 
+    private static final String KEY_BACKGROUND_ANIMATION_TOGGLED_ON =
+            "background_animation_toggled_on";
+
     private final AnimatedFloat mSwipeProgress = new AnimatedFloat(this::onSwipeProgressUpdate);
 
     private final InvariantDeviceProfile.OnIDPChangeListener mOnIDPChangeListener =
@@ -122,6 +128,9 @@
 
     private AnimatorPlaybackController mLauncherStartAnim = null;
 
+    // Auto play background animation by default
+    private boolean mBackgroundAnimationToggledOn = true;
+
     private TextView mHintView;
 
     private final OverviewChangeListener mOverviewChangeListener = this::onOverviewTargetChange;
@@ -198,6 +207,15 @@
                         LOTTIE_TERTIARY_COLOR_TOKEN, R.color.all_set_bg_tertiary),
                 getTheme());
 
+        mBackgroundAnimationToggledOn = savedInstanceState == null
+                || savedInstanceState.getBoolean(KEY_BACKGROUND_ANIMATION_TOGGLED_ON, true);
+        // The animated background is behind a scroll view, which intercepts all input.
+        // However, the content view also covers the full screen
+        requireViewById(R.id.content).setOnClickListener(v -> {
+            mBackgroundAnimationToggledOn = !mBackgroundAnimationToggledOn;
+            maybeResumeOrPauseBackgroundAnimation();
+        });
+
         setUpBackgroundAnimation(getDP().isTablet);
         getIDP().addOnChangeListener(mOnIDPChangeListener);
 
@@ -206,6 +224,12 @@
         ActivityPreloadUtil.preloadOverviewForSUWAllSet(this);
     }
 
+    @Override
+    protected void onSaveInstanceState(Bundle outState) {
+        super.onSaveInstanceState(outState);
+        outState.putBoolean(KEY_BACKGROUND_ANIMATION_TOGGLED_ON, mBackgroundAnimationToggledOn);
+    }
+
     private InvariantDeviceProfile getIDP() {
         return LauncherAppState.getInstance(this).getInvariantDeviceProfile();
     }
@@ -332,6 +356,7 @@
             mLauncherStartAnim.dispatchOnEnd();
             mLauncherStartAnim = null;
         }
+        sendBroadcast(new Intent(INTENT_ACTION_ACTIVITY_CLOSED));
     }
 
     @Override
@@ -365,8 +390,10 @@
 
     private void maybeResumeOrPauseBackgroundAnimation() {
         boolean shouldPlayAnimation =
-                getContentViewAlphaForSwipeProgress() > ANIMATION_PAUSE_ALPHA_THRESHOLD
-                        && isResumed();
+                !RemoveAnimationSettingsTracker.INSTANCE.get(this).isRemoveAnimationEnabled()
+                        && getContentViewAlphaForSwipeProgress() > ANIMATION_PAUSE_ALPHA_THRESHOLD
+                        && isResumed()
+                        && mBackgroundAnimationToggledOn;
         if (mAnimatedBackground.isAnimating() && !shouldPlayAnimation) {
             mAnimatedBackground.pauseAnimation();
         } else if (!mAnimatedBackground.isAnimating() && shouldPlayAnimation) {
diff --git a/quickstep/src/com/android/quickstep/interaction/TutorialController.java b/quickstep/src/com/android/quickstep/interaction/TutorialController.java
index 0fc95e2..e73fb3b 100644
--- a/quickstep/src/com/android/quickstep/interaction/TutorialController.java
+++ b/quickstep/src/com/android/quickstep/interaction/TutorialController.java
@@ -57,6 +57,7 @@
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimatorListeners;
+import com.android.launcher3.taskbar.TypefaceUtils;
 import com.android.launcher3.views.ClipIconView;
 import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureAttemptCallback;
 import com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureAttemptCallback;
@@ -177,6 +178,7 @@
 
         mFeedbackTitleView.setText(getIntroductionTitle());
         mFeedbackSubtitleView.setText(getIntroductionSubtitle());
+        setTitleTypefaces();
 
         mExitingAppView.setClipToOutline(true);
         mExitingAppView.setOutlineProvider(new ViewOutlineProvider() {
@@ -434,6 +436,10 @@
 
         if (isGestureSuccessful) {
             if (mTutorialFragment.isAtFinalStep()) {
+                TypefaceUtils.setTypeface(
+                        mDoneButton,
+                        TypefaceUtils.FONT_FAMILY_LABEL_LARGE_BASELINE
+                );
                 showActionButton();
             }
 
@@ -458,7 +464,8 @@
         pauseAndHideLottieAnimation();
         mCheckmarkAnimation.setVisibility(View.VISIBLE);
         mCheckmarkAnimation.playAnimation();
-        mFeedbackTitleView.setTextAppearance(mContext, getSuccessTitleTextAppearance());
+        mFeedbackTitleView.setTextAppearance(getSuccessTitleTextAppearance());
+        setTitleTypefaces();
     }
 
     public boolean isGestureCompleted() {
@@ -513,8 +520,10 @@
         updateDrawables();
         updateLayout();
 
-        mFeedbackTitleView.setTextAppearance(mContext, getTitleTextAppearance());
-        mDoneButton.setTextAppearance(mContext, getDoneButtonTextAppearance());
+        mFeedbackTitleView.setTextAppearance(getTitleTextAppearance());
+        mDoneButton.setTextAppearance(getDoneButtonTextAppearance());
+
+        setTitleTypefaces();
         mDoneButton.getBackground().setTint(getDoneButtonColor());
         mCheckmarkAnimation.setAnimation(mTutorialFragment.isAtFinalStep()
                 ? R.raw.checkmark_animation_end
@@ -533,6 +542,21 @@
         }
     }
 
+    /**
+     * Apply expressive typefaces to the feedback title and subtitle views.
+     */
+    private void setTitleTypefaces() {
+        TypefaceUtils.setTypeface(
+                mFeedbackTitleView,
+                mTutorialFragment.isLargeScreen()
+                        ? TypefaceUtils.FONT_FAMILY_DISPLAY_MEDIUM_EMPHASIZED
+                        : TypefaceUtils.FONT_FAMILY_DISPLAY_SMALL_EMPHASIZED);
+        TypefaceUtils.setTypeface(
+                mFeedbackSubtitleView,
+                TypefaceUtils.FONT_FAMILY_BODY_LARGE_BASELINE
+        );
+    }
+
     protected void resetViewsForBackGesture() {
         mFakeTaskView.setVisibility(View.VISIBLE);
         mFakeTaskView.setBackgroundColor(getFakeTaskViewColor());
diff --git a/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
index 594c99a..58e54cf 100644
--- a/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
+++ b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
@@ -382,18 +382,15 @@
                 return;
             }
 
-            if (mItemInfo.container < 0 || !LauncherAppState.INSTANCE.executeIfCreated(app -> {
-                // Item is inside a collection, fetch collection info in a BG thread
-                // and then write to StatsLog.
-                app.getModel().enqueueModelUpdateTask((taskController, dataModel, apps) ->
-                        write(event, applyOverwrites(mItemInfo.buildProto(
-                                (CollectionInfo) dataModel.itemsIdMap.get(mItemInfo.container),
-                                mContext))));
-            })) {
-                // Write log on the model thread so that logs do not go out of order
-                // (for eg: drop comes after drag)
-                Executors.MODEL_EXECUTOR.execute(
-                        () -> write(event, applyOverwrites(mItemInfo.buildProto(mContext))));
+            // Item is inside a collection, fetch collection info in a BG thread
+            // and then write to StatsLog.
+            if (mItemInfo.container < 0) {
+                LauncherAppState.INSTANCE.get(mContext).getModel().enqueueModelUpdateTask(
+                        (taskController, dataModel, apps) -> write(event, applyOverwrites(
+                                mItemInfo.buildProto(
+                                        (CollectionInfo) dataModel.itemsIdMap
+                                                .get(mItemInfo.container),
+                                        mContext))));
             }
         }
 
diff --git a/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.kt b/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.kt
index e72ccbf..17f861d 100644
--- a/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.kt
+++ b/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.kt
@@ -285,11 +285,7 @@
                 translationY = snapshotParams.topMargin.toFloat()
             } else {
                 val topLeftTaskPlusDividerPercent =
-                    if (splitBounds.appsStackedVertically) {
-                        splitBounds.topTaskPercent + splitBounds.dividerHeightPercent
-                    } else {
-                        splitBounds.leftTaskPercent + splitBounds.dividerWidthPercent
-                    }
+                    splitBounds.leftTopTaskPercent + splitBounds.dividerPercent
                 translationY =
                     snapshotParams.topMargin +
                         (taskViewHeight - snapshotParams.topMargin) * topLeftTaskPlusDividerPercent
@@ -440,15 +436,8 @@
         splitInfo: SplitBounds,
         desiredStagePosition: Int
     ) {
-        val topLeftTaskPercent: Float
-        val dividerBarPercent: Float
-        if (splitInfo.appsStackedVertically) {
-            topLeftTaskPercent = splitInfo.topTaskPercent
-            dividerBarPercent = splitInfo.dividerHeightPercent
-        } else {
-            topLeftTaskPercent = splitInfo.leftTaskPercent
-            dividerBarPercent = splitInfo.dividerWidthPercent
-        }
+        val topLeftTaskPercent = splitInfo.leftTopTaskPercent
+        val dividerBarPercent = splitInfo.dividerPercent
 
         if (desiredStagePosition == STAGE_POSITION_TOP_OR_LEFT) {
             outRect.bottom = outRect.top + (outRect.height() * topLeftTaskPercent).toInt()
@@ -510,12 +499,7 @@
         val totalThumbnailHeight = parentHeight - spaceAboveSnapshot
         val dividerBar = getDividerBarSize(totalThumbnailHeight, splitBoundsConfig)
 
-        val taskPercent =
-            if (splitBoundsConfig.appsStackedVertically) {
-                splitBoundsConfig.topTaskPercent
-            } else {
-                splitBoundsConfig.leftTaskPercent
-            }
+        val taskPercent = splitBoundsConfig.leftTopTaskPercent
         val firstTaskViewSize = Point(parentWidth, (totalThumbnailHeight * taskPercent).toInt())
         val secondTaskViewSize =
             Point(parentWidth, totalThumbnailHeight - firstTaskViewSize.y - dividerBar)
@@ -593,7 +577,8 @@
         isRtl: Boolean,
         deviceProfile: DeviceProfile,
         splitConfig: SplitBounds,
-        inSplitSelection: Boolean
+        inSplitSelection: Boolean,
+        oneIconHiddenDueToSmallWidth: Boolean,
     ) {
         val spaceAboveSnapshot = deviceProfile.overviewTaskThumbnailTopMarginPx
         val totalThumbnailHeight = groupedTaskViewHeight - spaceAboveSnapshot
@@ -606,7 +591,8 @@
                 totalThumbnailHeight,
                 isRtl,
                 deviceProfile.overviewTaskMarginPx,
-                dividerBar
+                dividerBar,
+                oneIconHiddenDueToSmallWidth,
             )
 
         updateSplitIconsPosition(primaryIconView, topLeftY, isRtl)
@@ -663,6 +649,7 @@
         isRtl: Boolean,
         overviewTaskMarginPx: Int,
         dividerSize: Int,
+        oneIconHiddenDueToSmallWidth: Boolean,
     ): SplitIconPositions {
         return if (Flags.enableOverviewIconMenu()) {
             if (isRtl) {
@@ -671,11 +658,21 @@
                 SplitIconPositions(0, primarySnapshotHeight + dividerSize)
             }
         } else {
-            val topLeftY = primarySnapshotHeight + overviewTaskMarginPx
-            SplitIconPositions(
-                topLeftY = topLeftY,
-                bottomRightY = topLeftY + dividerSize + taskIconHeight
-            )
+            if (oneIconHiddenDueToSmallWidth) {
+                // Center both icons
+                val centerY = primarySnapshotHeight + overviewTaskMarginPx +
+                        ((taskIconHeight + dividerSize) / 2)
+                SplitIconPositions(
+                    topLeftY = centerY,
+                    bottomRightY = centerY,
+                )
+            } else {
+                val topLeftY = primarySnapshotHeight + overviewTaskMarginPx
+                SplitIconPositions(
+                    topLeftY = topLeftY,
+                    bottomRightY = topLeftY + dividerSize + taskIconHeight,
+                )
+            }
         }
     }
 
@@ -715,11 +712,7 @@
      * @return The divider size for the group task view.
      */
     protected fun getDividerBarSize(totalThumbnailHeight: Int, splitConfig: SplitBounds): Int {
-        return Math.round(
-            totalThumbnailHeight *
-                if (splitConfig.appsStackedVertically) splitConfig.dividerHeightPercent
-                else splitConfig.dividerWidthPercent
-        )
+        return Math.round(totalThumbnailHeight * splitConfig.dividerPercent)
     }
 
     /**
diff --git a/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java b/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java
index c1e1c2b..c4e82d6 100644
--- a/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java
+++ b/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java
@@ -271,12 +271,8 @@
         if (splitBounds != null) {
             if (deviceProfile.isLeftRightSplit) {
                 if (desiredTaskId == splitBounds.rightBottomTaskId) {
-                    float leftTopTaskPercent = splitBounds.appsStackedVertically
-                            ? splitBounds.topTaskPercent
-                            : splitBounds.leftTaskPercent;
-                    float dividerThicknessPercent = splitBounds.appsStackedVertically
-                            ? splitBounds.dividerHeightPercent
-                            : splitBounds.dividerWidthPercent;
+                    float leftTopTaskPercent = splitBounds.getLeftTopTaskPercent();
+                    float dividerThicknessPercent = splitBounds.getDividerPercent();
                     translationX = ((taskViewWidth * leftTopTaskPercent)
                             + (taskViewWidth * dividerThicknessPercent));
                 }
@@ -285,9 +281,9 @@
                     FrameLayout.LayoutParams snapshotParams =
                             (FrameLayout.LayoutParams) thumbnailViews[0]
                                     .getLayoutParams();
-                    float bottomRightTaskPlusDividerPercent = splitBounds.appsStackedVertically
-                            ? (1f - splitBounds.topTaskPercent)
-                            : (1f - splitBounds.leftTaskPercent);
+                    float bottomRightTaskPlusDividerPercent =
+                            splitBounds.getRightBottomTaskPercent()
+                                    + splitBounds.getDividerPercent();
                     translationY = -((taskViewHeight - snapshotParams.topMargin)
                             * bottomRightTaskPlusDividerPercent);
                 }
@@ -506,12 +502,8 @@
     @Override
     public void setSplitTaskSwipeRect(DeviceProfile dp, Rect outRect,
             SplitBounds splitInfo, int desiredStagePosition) {
-        float topLeftTaskPercent = splitInfo.appsStackedVertically
-                ? splitInfo.topTaskPercent
-                : splitInfo.leftTaskPercent;
-        float dividerBarPercent = splitInfo.appsStackedVertically
-                ? splitInfo.dividerHeightPercent
-                : splitInfo.dividerWidthPercent;
+        float topLeftTaskPercent = splitInfo.getLeftTopTaskPercent();
+        float dividerBarPercent = splitInfo.getDividerPercent();
 
         int taskbarHeight = dp.isTransientTaskbar ? 0 : dp.taskbarHeight;
         float scale = (float) outRect.height() / (dp.availableHeightPx - taskbarHeight);
@@ -559,9 +551,7 @@
         primaryParams.topMargin = spaceAboveSnapshot;
 
         int totalThumbnailHeight = parentHeight - spaceAboveSnapshot;
-        float dividerScale = splitBoundsConfig.appsStackedVertically
-                ? splitBoundsConfig.dividerHeightPercent
-                : splitBoundsConfig.dividerWidthPercent;
+        float dividerScale = splitBoundsConfig.getDividerPercent();
         Pair<Point, Point> taskViewSizes =
                 getGroupedTaskViewSizes(dp, splitBoundsConfig, parentWidth, parentHeight);
         if (!inSplitSelection) {
@@ -610,12 +600,8 @@
             int parentHeight) {
         int spaceAboveSnapshot = dp.overviewTaskThumbnailTopMarginPx;
         int totalThumbnailHeight = parentHeight - spaceAboveSnapshot;
-        float dividerScale = splitBoundsConfig.appsStackedVertically
-                ? splitBoundsConfig.dividerHeightPercent
-                : splitBoundsConfig.dividerWidthPercent;
-        float taskPercent = splitBoundsConfig.appsStackedVertically
-                ? splitBoundsConfig.topTaskPercent
-                : splitBoundsConfig.leftTaskPercent;
+        float dividerScale = splitBoundsConfig.getDividerPercent();
+        float taskPercent = splitBoundsConfig.getLeftTopTaskPercent();
 
         Point firstTaskViewSize = new Point();
         Point secondTaskViewSize = new Point();
@@ -685,7 +671,8 @@
     public void setSplitIconParams(View primaryIconView, View secondaryIconView,
             int taskIconHeight, int primarySnapshotWidth, int primarySnapshotHeight,
             int groupedTaskViewHeight, int groupedTaskViewWidth, boolean isRtl,
-            DeviceProfile deviceProfile, SplitBounds splitConfig, boolean inSplitSelection) {
+            DeviceProfile deviceProfile, SplitBounds splitConfig, boolean inSplitSelection,
+            boolean oneIconHiddenDueToSmallWidth) {
         FrameLayout.LayoutParams primaryIconParams =
                 (FrameLayout.LayoutParams) primaryIconView.getLayoutParams();
         FrameLayout.LayoutParams secondaryIconParams = enableOverviewIconMenu()
@@ -740,16 +727,30 @@
                 secondaryIconParams.gravity = TOP | (isRtl ? END : START);
                 if (!inSplitSelection) {
                     if (splitConfig.initiatedFromSeascape) {
-                        // if the split was initiated from seascape,
-                        // the task on the right (secondary) is slightly larger
-                        primaryIconView.setTranslationX(bottomToMidpointOffset - taskIconHeight);
-                        secondaryIconView.setTranslationX(bottomToMidpointOffset);
+                        if (oneIconHiddenDueToSmallWidth) {
+                            // Center both icons
+                            float centerX = bottomToMidpointOffset - (taskIconHeight / 2f);
+                            primaryIconView.setTranslationX(centerX);
+                            secondaryIconView.setTranslationX(centerX);
+                        } else {
+                            // the task on the right (secondary) is slightly larger
+                            primaryIconView.setTranslationX(
+                                    bottomToMidpointOffset - taskIconHeight);
+                            secondaryIconView.setTranslationX(bottomToMidpointOffset);
+                        }
                     } else {
-                        // if not,
-                        // the task on the left (primary) is slightly larger
-                        primaryIconView.setTranslationX(bottomToMidpointOffset + insetOffset
-                                - taskIconHeight);
-                        secondaryIconView.setTranslationX(bottomToMidpointOffset + insetOffset);
+                        if (oneIconHiddenDueToSmallWidth) {
+                            // Center both icons
+                            float centerX =
+                                    bottomToMidpointOffset + insetOffset - (taskIconHeight / 2f);
+                            primaryIconView.setTranslationX(centerX);
+                            secondaryIconView.setTranslationX(centerX);
+                        } else {
+                            // the task on the left (primary) is slightly larger
+                            primaryIconView.setTranslationX(bottomToMidpointOffset + insetOffset
+                                    - taskIconHeight);
+                            secondaryIconView.setTranslationX(bottomToMidpointOffset + insetOffset);
+                        }
                     }
                 }
             } else {
@@ -757,16 +758,30 @@
                 secondaryIconParams.gravity = TOP | (isRtl ? START : END);
                 if (!inSplitSelection) {
                     if (!splitConfig.initiatedFromSeascape) {
-                        // if the split was initiated from landscape,
-                        // the task on the left (primary) is slightly larger
-                        primaryIconView.setTranslationX(-bottomToMidpointOffset);
-                        secondaryIconView.setTranslationX(-bottomToMidpointOffset + taskIconHeight);
+                        if (oneIconHiddenDueToSmallWidth) {
+                            // Center both icons
+                            float centerX = -bottomToMidpointOffset + (taskIconHeight / 2f);
+                            primaryIconView.setTranslationX(centerX);
+                            secondaryIconView.setTranslationX(centerX);
+                        } else {
+                            // the task on the left (primary) is slightly larger
+                            primaryIconView.setTranslationX(-bottomToMidpointOffset);
+                            secondaryIconView.setTranslationX(
+                                    -bottomToMidpointOffset + taskIconHeight);
+                        }
                     } else {
-                        // if not,
-                        // the task on the right (secondary) is slightly larger
-                        primaryIconView.setTranslationX(-bottomToMidpointOffset - insetOffset);
-                        secondaryIconView.setTranslationX(-bottomToMidpointOffset - insetOffset
-                                + taskIconHeight);
+                        if (oneIconHiddenDueToSmallWidth) {
+                            // Center both icons
+                            float centerX =
+                                    -bottomToMidpointOffset - insetOffset + (taskIconHeight / 2f);
+                            primaryIconView.setTranslationX(centerX);
+                            secondaryIconView.setTranslationX(centerX);
+                        } else {
+                            // the task on the right (secondary) is slightly larger
+                            primaryIconView.setTranslationX(-bottomToMidpointOffset - insetOffset);
+                            secondaryIconView.setTranslationX(-bottomToMidpointOffset - insetOffset
+                                    + taskIconHeight);
+                        }
                     }
                 }
             }
@@ -774,9 +789,15 @@
             primaryIconParams.gravity = TOP | CENTER_HORIZONTAL;
             secondaryIconParams.gravity = TOP | CENTER_HORIZONTAL;
             if (!inSplitSelection) {
-                // shifts icon half a width left (height is used here since icons are square)
-                primaryIconView.setTranslationX(-(taskIconHeight / 2f));
-                secondaryIconView.setTranslationX(taskIconHeight / 2f);
+                if (oneIconHiddenDueToSmallWidth) {
+                    // Center both icons
+                    primaryIconView.setTranslationX(0);
+                    secondaryIconView.setTranslationX(0);
+                } else {
+                    // shifts icon half a width left (height is used here since icons are square)
+                    primaryIconView.setTranslationX(-(taskIconHeight / 2f));
+                    secondaryIconView.setTranslationX(taskIconHeight / 2f);
+                }
             }
         }
         if (!enableOverviewIconMenu() && !inSplitSelection) {
diff --git a/quickstep/src/com/android/quickstep/orientation/RecentsPagedOrientationHandler.kt b/quickstep/src/com/android/quickstep/orientation/RecentsPagedOrientationHandler.kt
index 78f9a0a..9b3c467 100644
--- a/quickstep/src/com/android/quickstep/orientation/RecentsPagedOrientationHandler.kt
+++ b/quickstep/src/com/android/quickstep/orientation/RecentsPagedOrientationHandler.kt
@@ -237,7 +237,8 @@
         isRtl: Boolean,
         deviceProfile: DeviceProfile,
         splitConfig: SplitConfigurationOptions.SplitBounds,
-        inSplitSelection: Boolean
+        inSplitSelection: Boolean,
+        oneIconHiddenDueToSmallWidth: Boolean,
     )
 
     /*
diff --git a/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.kt b/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.kt
index 3fb4f54..0cb983d 100644
--- a/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.kt
+++ b/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.kt
@@ -106,15 +106,8 @@
         splitInfo: SplitBounds,
         desiredStagePosition: Int
     ) {
-        val topLeftTaskPercent: Float
-        val dividerBarPercent: Float
-        if (splitInfo.appsStackedVertically) {
-            topLeftTaskPercent = splitInfo.topTaskPercent
-            dividerBarPercent = splitInfo.dividerHeightPercent
-        } else {
-            topLeftTaskPercent = splitInfo.leftTaskPercent
-            dividerBarPercent = splitInfo.dividerWidthPercent
-        }
+        val topLeftTaskPercent = splitInfo.leftTopTaskPercent
+        val dividerBarPercent = splitInfo.dividerPercent
 
         // In seascape, the primary thumbnail is counterintuitively placed at the physical bottom of
         // the screen. This is to preserve consistency when the user rotates: From the user's POV,
@@ -166,11 +159,7 @@
         } else {
             if (desiredTaskId == splitBounds.leftTopTaskId) {
                 val bottomRightTaskPlusDividerPercent =
-                    if (splitBounds.appsStackedVertically) {
-                        1f - splitBounds.topTaskPercent
-                    } else {
-                        1f - splitBounds.leftTaskPercent
-                    }
+                    splitBounds.rightBottomTaskPercent + splitBounds.dividerPercent
                 translationY =
                     banner.height -
                         (taskViewHeight - snapshotParams.topMargin) *
@@ -331,12 +320,7 @@
         val totalThumbnailHeight = parentHeight - spaceAboveSnapshot
         val dividerBar = getDividerBarSize(totalThumbnailHeight, splitBoundsConfig)
 
-        val taskPercent =
-            if (splitBoundsConfig.appsStackedVertically) {
-                splitBoundsConfig.topTaskPercent
-            } else {
-                splitBoundsConfig.leftTaskPercent
-            }
+        val taskPercent = splitBoundsConfig.leftTopTaskPercent
         val firstTaskViewSize = Point(parentWidth, (totalThumbnailHeight * taskPercent).toInt())
         val secondTaskViewSize =
             Point(parentWidth, totalThumbnailHeight - firstTaskViewSize.y - dividerBar)
@@ -369,17 +353,18 @@
         isRtl: Boolean,
         overviewTaskMarginPx: Int,
         dividerSize: Int,
+        oneIconHiddenDueToSmallWidth: Boolean,
     ): SplitIconPositions {
         return if (Flags.enableOverviewIconMenu()) {
             if (isRtl) {
                 SplitIconPositions(
                     topLeftY = totalThumbnailHeight - primarySnapshotHeight,
-                    bottomRightY = 0
+                    bottomRightY = 0,
                 )
             } else {
                 SplitIconPositions(
                     topLeftY = 0,
-                    bottomRightY = -(primarySnapshotHeight + dividerSize)
+                    bottomRightY = -(primarySnapshotHeight + dividerSize),
                 )
             }
         } else {
@@ -388,10 +373,19 @@
             // from the bottom to the almost-center of the screen using the bottom margin.
             // The primary snapshot is placed at the bottom, thus we translate the icons using
             // the size of the primary snapshot minus the icon size for the top-left icon.
-            SplitIconPositions(
-                topLeftY = primarySnapshotHeight - taskIconHeight,
-                bottomRightY = primarySnapshotHeight + dividerSize
-            )
+            if (oneIconHiddenDueToSmallWidth) {
+                // Center both icons
+                val centerY = primarySnapshotHeight + ((dividerSize - taskIconHeight) / 2)
+                SplitIconPositions(
+                    topLeftY = centerY,
+                    bottomRightY = centerY,
+                )
+            } else {
+                SplitIconPositions(
+                    topLeftY = primarySnapshotHeight - taskIconHeight,
+                    bottomRightY = primarySnapshotHeight + dividerSize,
+                )
+            }
         }
     }
 
diff --git a/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegate.kt b/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegate.kt
index 608fafd..12616a8 100644
--- a/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegate.kt
+++ b/quickstep/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegate.kt
@@ -17,6 +17,7 @@
 package com.android.quickstep.recents.data
 
 import android.os.UserHandle
+import android.util.Log
 import com.android.quickstep.HighResLoadingState.HighResLoadingStateChangedCallback
 import com.android.quickstep.recents.data.TaskVisualsChangedDelegate.TaskIconChangedCallback
 import com.android.quickstep.recents.data.TaskVisualsChangedDelegate.TaskThumbnailChangedCallback
@@ -58,7 +59,7 @@
         fun onTaskThumbnailChanged(thumbnailData: ThumbnailData?)
 
         /** Informs the listener that the default resolution for loading thumbnails has changed */
-        fun onHighResLoadingStateChanged()
+        fun onHighResLoadingStateChanged(highResEnabled: Boolean)
     }
 }
 
@@ -91,8 +92,9 @@
     }
 
     override fun onHighResLoadingStateChanged(enabled: Boolean) {
+        Log.d(TAG, "onHighResLoadingStateChanged(enabled = $enabled)")
         taskThumbnailChangedCallbacks.values.forEach { (_, callback) ->
-            callback.onHighResLoadingStateChanged()
+            callback.onHighResLoadingStateChanged(enabled)
         }
     }
 
@@ -142,4 +144,8 @@
             }
         }
     }
+
+    companion object {
+        const val TAG = "TaskVisualsChangedDelegateImpl"
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
index b1a5920..5274ef3 100644
--- a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
+++ b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
@@ -160,7 +160,17 @@
                     updateThumbnail(task.key.id, thumbnailData)
                 }
 
-                override fun onHighResLoadingStateChanged() {
+                override fun onHighResLoadingStateChanged(highResEnabled: Boolean) {
+                    val isTaskVisible = taskRequests.containsKey(task.key.id)
+                    if (!isTaskVisible) return
+
+                    val isCurrentThumbnailLowRes =
+                        tasks.value[task.key.id]?.thumbnail?.reducedResolution
+                    val isRequestedResHigherThanCurrent =
+                        isCurrentThumbnailLowRes == null ||
+                            (isCurrentThumbnailLowRes && highResEnabled)
+                    if (!isRequestedResHigherThanCurrent) return
+
                     recentsCoroutineScope.launch(dispatcherProvider.background) {
                         updateThumbnail(task.key.id, getThumbnailFromDataSource(task))
                     }
diff --git a/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
index 977629f..c6b3d6a 100644
--- a/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
+++ b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
@@ -28,22 +28,20 @@
 import com.android.quickstep.recents.data.TasksRepository
 import com.android.quickstep.recents.domain.usecase.GetSysUiStatusNavFlagsUseCase
 import com.android.quickstep.recents.domain.usecase.GetTaskUseCase
+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.GetThumbnailPositionUseCase
-import com.android.quickstep.recents.usecase.GetThumbnailUseCase
 import com.android.quickstep.recents.viewmodel.RecentsViewData
 import com.android.quickstep.task.viewmodel.TaskOverlayViewModel
-import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel
-import com.android.quickstep.task.viewmodel.TaskThumbnailViewModelImpl
 import com.android.systemui.shared.recents.model.Task
+import com.android.systemui.shared.recents.utilities.PreviewPositionHelper
 import kotlinx.coroutines.CoroutineName
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.SupervisorJob
 
 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 {
@@ -173,8 +171,6 @@
         val instance: Any =
             when (modelClass) {
                 RecentsViewData::class.java -> RecentsViewData()
-                TaskThumbnailViewModel::class.java ->
-                    TaskThumbnailViewModelImpl(getThumbnailPositionUseCase = inject())
                 TaskOverlayViewModel::class.java -> {
                     val task = extras["Task"] as Task
                     TaskOverlayViewModel(
@@ -188,13 +184,12 @@
                 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(
                         deviceProfileRepository = inject(),
                         rotationStateRepository = inject(),
-                        tasksRepository = inject(),
+                        previewPositionHelper = PreviewPositionHelper(),
                     )
                 OrganizeDesktopTasksUseCase::class.java -> OrganizeDesktopTasksUseCase()
                 else -> {
@@ -237,7 +232,7 @@
         fun initialize(view: View): RecentsDependencies = initialize(view.context)
 
         fun initialize(context: Context): RecentsDependencies {
-            Log.d(TAG, "initializing: $activeRecentsCount + 1 more")
+            Log.d(TAG, "initializing")
             synchronized(this) {
                 activeRecentsCount++
                 instance = RecentsDependencies(context.applicationContext)
@@ -277,7 +272,7 @@
                 Log.d(
                     TAG,
                     "RecentsDependencies was not destroyed. " +
-                        "There is still an active RecentsView instance: $activeRecentsCount",
+                        "There is still an active RecentsView instance.",
                 )
             }
         }
diff --git a/quickstep/src/com/android/quickstep/recents/usecase/GetThumbnailPositionUseCase.kt b/quickstep/src/com/android/quickstep/recents/domain/usecase/GetThumbnailPositionUseCase.kt
similarity index 62%
rename from quickstep/src/com/android/quickstep/recents/usecase/GetThumbnailPositionUseCase.kt
rename to quickstep/src/com/android/quickstep/recents/domain/usecase/GetThumbnailPositionUseCase.kt
index bea1d07..8501382 100644
--- a/quickstep/src/com/android/quickstep/recents/usecase/GetThumbnailPositionUseCase.kt
+++ b/quickstep/src/com/android/quickstep/recents/domain/usecase/GetThumbnailPositionUseCase.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2024 The Android Open Source Project
+ * 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.
@@ -14,28 +14,30 @@
  * limitations under the License.
  */
 
-package com.android.quickstep.recents.usecase
+package com.android.quickstep.recents.domain.usecase
 
 import android.graphics.Matrix
 import android.graphics.Rect
-import com.android.quickstep.recents.data.RecentTasksRepository
 import com.android.quickstep.recents.data.RecentsDeviceProfileRepository
 import com.android.quickstep.recents.data.RecentsRotationStateRepository
-import com.android.quickstep.recents.usecase.ThumbnailPositionState.MatrixScaling
-import com.android.quickstep.recents.usecase.ThumbnailPositionState.MissingThumbnail
+import com.android.systemui.shared.recents.model.ThumbnailData
 import com.android.systemui.shared.recents.utilities.PreviewPositionHelper
 
 /** Use case for retrieving [Matrix] for positioning Thumbnail in a View */
 class GetThumbnailPositionUseCase(
     private val deviceProfileRepository: RecentsDeviceProfileRepository,
     private val rotationStateRepository: RecentsRotationStateRepository,
-    private val tasksRepository: RecentTasksRepository,
-    private val previewPositionHelper: PreviewPositionHelper = PreviewPositionHelper(),
+    private val previewPositionHelper: PreviewPositionHelper,
 ) {
-    fun run(taskId: Int, width: Int, height: Int, isRtl: Boolean): ThumbnailPositionState {
-        val thumbnailData =
-            tasksRepository.getCurrentThumbnailById(taskId) ?: return MissingThumbnail
-        val thumbnail = thumbnailData.thumbnail ?: return MissingThumbnail
+    operator fun invoke(
+        thumbnailData: ThumbnailData?,
+        width: Int,
+        height: Int,
+        isRtl: Boolean,
+    ): ThumbnailPosition {
+        val thumbnail =
+            thumbnailData?.thumbnail ?: return ThumbnailPosition(Matrix.IDENTITY_MATRIX, false)
+
         previewPositionHelper.updateThumbnailMatrix(
             Rect(0, 0, thumbnail.width, thumbnail.height),
             thumbnailData,
@@ -45,9 +47,11 @@
             rotationStateRepository.getRecentsRotationState().activityRotation,
             isRtl,
         )
-        return MatrixScaling(
-            previewPositionHelper.matrix,
-            previewPositionHelper.isOrientationChanged,
+        return ThumbnailPosition(
+            matrix = previewPositionHelper.matrix,
+            isRotated = previewPositionHelper.isOrientationChanged,
         )
     }
 }
+
+data class ThumbnailPosition(val matrix: Matrix, val isRotated: Boolean)
diff --git a/quickstep/src/com/android/quickstep/recents/ui/mapper/TaskUiStateMapper.kt b/quickstep/src/com/android/quickstep/recents/ui/mapper/TaskUiStateMapper.kt
index fb62268..619075f 100644
--- a/quickstep/src/com/android/quickstep/recents/ui/mapper/TaskUiStateMapper.kt
+++ b/quickstep/src/com/android/quickstep/recents/ui/mapper/TaskUiStateMapper.kt
@@ -16,6 +16,7 @@
 
 package com.android.quickstep.recents.ui.mapper
 
+import android.view.View.OnClickListener
 import com.android.quickstep.recents.ui.viewmodel.TaskData
 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState
 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.BackgroundOnly
@@ -36,29 +37,38 @@
      * @param taskData The [TaskData] to convert. Can be null or a specific subclass.
      * @param isLiveTile A flag indicating whether the task data represents live tile.
      * @param hasHeader A flag indicating whether the UI should display a header.
+     * @param clickCloseListener A callback when the close button in the UI is clicked.
      * @return A [TaskThumbnailUiState] representing the UI state for the given task data.
      */
     fun toTaskThumbnailUiState(
         taskData: TaskData?,
         isLiveTile: Boolean,
         hasHeader: Boolean,
+        clickCloseListener: OnClickListener?,
     ): TaskThumbnailUiState =
         when {
             taskData !is TaskData.Data -> Uninitialized
-            isLiveTile -> createLiveTileState(taskData, hasHeader)
+            isLiveTile -> createLiveTileState(taskData, hasHeader, clickCloseListener)
             isBackgroundOnly(taskData) -> BackgroundOnly(taskData.backgroundColor)
             isSnapshotSplash(taskData) ->
-                SnapshotSplash(createSnapshotState(taskData, hasHeader), taskData.icon)
+                SnapshotSplash(
+                    createSnapshotState(taskData, hasHeader, clickCloseListener),
+                    taskData.icon,
+                )
             else -> Uninitialized
         }
 
-    private fun createSnapshotState(taskData: TaskData.Data, hasHeader: Boolean): Snapshot =
-        if (canHeaderBeCreated(taskData, hasHeader)) {
+    private fun createSnapshotState(
+        taskData: TaskData.Data,
+        hasHeader: Boolean,
+        clickCloseListener: OnClickListener?,
+    ): Snapshot =
+        if (canHeaderBeCreated(taskData, hasHeader, clickCloseListener)) {
             Snapshot.WithHeader(
                 taskData.thumbnailData?.thumbnail!!,
                 taskData.thumbnailData.rotation,
                 taskData.backgroundColor,
-                ThumbnailHeader(taskData.icon!!, taskData.titleDescription!!),
+                ThumbnailHeader(taskData.icon!!, taskData.titleDescription!!, clickCloseListener!!),
             )
         } else {
             Snapshot.WithoutHeader(
@@ -74,13 +84,26 @@
     private fun isSnapshotSplash(taskData: TaskData.Data) =
         taskData.thumbnailData?.thumbnail != null && !taskData.isLocked
 
-    private fun canHeaderBeCreated(taskData: TaskData.Data, hasHeader: Boolean) =
-        hasHeader && taskData.icon != null && taskData.titleDescription != null
+    private fun canHeaderBeCreated(
+        taskData: TaskData.Data,
+        hasHeader: Boolean,
+        clickCloseListener: OnClickListener?,
+    ) =
+        hasHeader &&
+            taskData.icon != null &&
+            taskData.titleDescription != null &&
+            clickCloseListener != null
 
-    private fun createLiveTileState(taskData: TaskData.Data, hasHeader: Boolean) =
-        if (canHeaderBeCreated(taskData, hasHeader)) {
+    private fun createLiveTileState(
+        taskData: TaskData.Data,
+        hasHeader: Boolean,
+        clickCloseListener: OnClickListener?,
+    ) =
+        if (canHeaderBeCreated(taskData, hasHeader, clickCloseListener)) {
             // TODO(http://b/353965691): figure out what to do when `icon` or `titleDescription` is
             //  null.
-            LiveTile.WithHeader(ThumbnailHeader(taskData.icon!!, taskData.titleDescription!!))
+            LiveTile.WithHeader(
+                ThumbnailHeader(taskData.icon!!, taskData.titleDescription!!, clickCloseListener!!)
+            )
         } else LiveTile.WithoutHeader
 }
diff --git a/quickstep/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModel.kt b/quickstep/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModel.kt
index 0b299ee..3c4a384 100644
--- a/quickstep/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModel.kt
+++ b/quickstep/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModel.kt
@@ -24,7 +24,9 @@
 import com.android.quickstep.recents.domain.model.TaskModel
 import com.android.quickstep.recents.domain.usecase.GetSysUiStatusNavFlagsUseCase
 import com.android.quickstep.recents.domain.usecase.GetTaskUseCase
+import com.android.quickstep.recents.domain.usecase.GetThumbnailPositionUseCase
 import com.android.quickstep.recents.domain.usecase.IsThumbnailValidUseCase
+import com.android.quickstep.recents.domain.usecase.ThumbnailPosition
 import com.android.quickstep.recents.viewmodel.RecentsViewData
 import com.android.quickstep.views.TaskViewType
 import com.android.systemui.shared.recents.model.ThumbnailData
@@ -48,6 +50,7 @@
     private val getTaskUseCase: GetTaskUseCase,
     private val getSysUiStatusNavFlagsUseCase: GetSysUiStatusNavFlagsUseCase,
     private val isThumbnailValidUseCase: IsThumbnailValidUseCase,
+    private val getThumbnailPositionUseCase: GetThumbnailPositionUseCase,
     dispatcherProvider: DispatcherProvider,
 ) {
     private var taskIds = MutableStateFlow(emptySet<Int>())
@@ -83,6 +86,19 @@
     fun isThumbnailValid(thumbnail: ThumbnailData?, width: Int, height: Int): Boolean =
         isThumbnailValidUseCase(thumbnail, width, height)
 
+    fun getThumbnailPosition(
+        thumbnail: ThumbnailData?,
+        width: Int,
+        height: Int,
+        isRtl: Boolean,
+    ): ThumbnailPosition =
+        getThumbnailPositionUseCase(
+            thumbnailData = thumbnail,
+            width = width,
+            height = height,
+            isRtl = isRtl,
+        )
+
     private fun mapToTaskTile(tasks: List<TaskData>, isLiveTile: Boolean): TaskTileUiState {
         val firstThumbnailData = (tasks.firstOrNull() as? TaskData.Data)?.thumbnailData
         return TaskTileUiState(
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/recents/usecase/ThumbnailPositionState.kt b/quickstep/src/com/android/quickstep/recents/usecase/ThumbnailPositionState.kt
deleted file mode 100644
index 1a1bef7..0000000
--- a/quickstep/src/com/android/quickstep/recents/usecase/ThumbnailPositionState.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.Matrix
-
-/** State on how a task Thumbnail can fit on given canvas */
-sealed class ThumbnailPositionState {
-    data object MissingThumbnail : ThumbnailPositionState()
-
-    data class MatrixScaling(val matrix: Matrix, val isRotated: Boolean) : ThumbnailPositionState()
-}
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailUiState.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailUiState.kt
index 6118544..db593d3 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailUiState.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailUiState.kt
@@ -19,6 +19,7 @@
 import android.graphics.Bitmap
 import android.graphics.drawable.Drawable
 import android.view.Surface
+import android.view.View.OnClickListener
 import androidx.annotation.ColorInt
 
 sealed class TaskThumbnailUiState {
@@ -54,5 +55,9 @@
         ) : Snapshot()
     }
 
-    data class ThumbnailHeader(val icon: Drawable, val title: String)
+    data class ThumbnailHeader(
+        val icon: Drawable,
+        val title: String,
+        val clickCloseListener: OnClickListener,
+    )
 }
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
index 63e93ba..0edbacc 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
@@ -18,7 +18,9 @@
 
 import android.content.Context
 import android.graphics.Color
+import android.graphics.Matrix
 import android.graphics.Outline
+import android.graphics.Path
 import android.graphics.Rect
 import android.graphics.drawable.ShapeDrawable
 import android.util.AttributeSet
@@ -34,34 +36,15 @@
 import com.android.launcher3.R
 import com.android.launcher3.util.MultiPropertyFactory
 import com.android.launcher3.util.ViewPool
-import com.android.launcher3.util.coroutines.DispatcherProvider
-import com.android.quickstep.recents.di.RecentsDependencies
-import com.android.quickstep.recents.di.get
 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.BackgroundOnly
 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.LiveTile
 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot
 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.SnapshotSplash
 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
-import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel
 import com.android.quickstep.views.FixedSizeImageView
 import com.android.quickstep.views.TaskThumbnailViewHeader
-import kotlinx.coroutines.CoroutineName
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.SupervisorJob
-import kotlinx.coroutines.cancel
-import kotlinx.coroutines.launch
 
 class TaskThumbnailView : FrameLayout, ViewPool.Reusable {
-    private val recentsCoroutineScope: CoroutineScope = RecentsDependencies.get()
-    private val dispatcherProvider: DispatcherProvider = RecentsDependencies.get()
-
-    // This is initialised here and set in onAttachedToWindow because onLayout can be called before
-    // onAttachedToWindow so this property needs to be initialised as it is used below.
-    private lateinit var viewModel: TaskThumbnailViewModel
-
-    private lateinit var viewAttachedScope: CoroutineScope
-
     private val scrimView: View by lazy { findViewById(R.id.task_thumbnail_scrim) }
     private val liveTileView: LiveTileView by lazy { findViewById(R.id.task_thumbnail_live_tile) }
     private val thumbnailView: FixedSizeImageView by lazy { findViewById(R.id.task_thumbnail) }
@@ -70,10 +53,22 @@
     private val dimAlpha: MultiPropertyFactory<View> by lazy {
         MultiPropertyFactory(scrimView, VIEW_ALPHA, ScrimViewAlpha.entries.size, ::maxOf)
     }
+    private val outlinePath = Path()
+    private var onSizeChanged: ((width: Int, height: Int) -> Unit)? = null
 
     private var taskThumbnailViewHeader: TaskThumbnailViewHeader? = null
 
     private var uiState: TaskThumbnailUiState = Uninitialized
+
+    /**
+     * Sets the outline bounds of the view. Default to use view's bound as outline when set to null.
+     */
+    var outlineBounds: Rect? = null
+        set(value) {
+            field = value
+            invalidateOutline()
+        }
+
     private val bounds = Rect()
 
     var cornerRadius: Float = 0f
@@ -94,36 +89,37 @@
 
     override fun onFinishInflate() {
         super.onFinishInflate()
-
         maybeCreateHeader()
     }
 
     override fun onAttachedToWindow() {
         super.onAttachedToWindow()
-        viewAttachedScope =
-            CoroutineScope(
-                SupervisorJob() + Dispatchers.Main.immediate + CoroutineName("TaskThumbnailView")
-            )
-        viewModel = RecentsDependencies.get(this)
         clipToOutline = true
         outlineProvider =
             object : ViewOutlineProvider() {
                 override fun getOutline(view: View, outline: Outline) {
-                    outline.setRoundRect(bounds, cornerRadius)
+                    val outlineRect = outlineBounds ?: bounds
+                    outlinePath.apply {
+                        rewind()
+                        addRoundRect(
+                            outlineRect.left.toFloat(),
+                            outlineRect.top.toFloat(),
+                            outlineRect.right.toFloat(),
+                            outlineRect.bottom.toFloat(),
+                            cornerRadius / scaleX,
+                            cornerRadius / scaleY,
+                            Path.Direction.CW,
+                        )
+                    }
+                    outline.setPath(outlinePath)
                 }
             }
     }
 
-    // TODO(b/391842220): Cancel scope in onDetach instead of having a specific method for this.
-    fun destroyScopes() {
-        val scopeToCancel = viewAttachedScope
-        recentsCoroutineScope.launch(dispatcherProvider.background) {
-            scopeToCancel.cancel("TaskThumbnailView detaching from window")
-        }
-    }
-
     override fun onRecycle() {
         uiState = Uninitialized
+        onSizeChanged = null
+        outlineBounds = null
         resetViews()
     }
 
@@ -159,11 +155,13 @@
         splashIcon.alpha = value
     }
 
+    fun doOnSizeChange(action: (width: Int, height: Int) -> Unit) {
+        onSizeChanged = action
+    }
+
     override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
         super.onSizeChanged(w, h, oldw, oldh)
-        if (uiState is SnapshotSplash) {
-            setImageMatrix()
-        }
+        onSizeChanged?.invoke(width, height)
         bounds.set(0, 0, w, h)
         invalidateOutline()
     }
@@ -183,8 +181,10 @@
     private fun resetViews() {
         liveTileView.isInvisible = true
         thumbnailView.isInvisible = true
+        thumbnailView.setImageBitmap(null)
         splashBackground.alpha = 0f
         splashIcon.alpha = 0f
+        splashIcon.setImageDrawable(null)
         scrimView.alpha = 0f
         setBackgroundColor(Color.BLACK)
         taskThumbnailViewHeader?.isInvisible = true
@@ -220,11 +220,12 @@
         drawBackground(snapshot.backgroundColor)
         thumbnailView.setImageBitmap(snapshot.bitmap)
         thumbnailView.isInvisible = false
-        setImageMatrix()
     }
 
-    private fun setImageMatrix() {
-        thumbnailView.imageMatrix = viewModel.getThumbnailPositionState(width, height, isLayoutRtl)
+    fun setImageMatrix(matrix: Matrix) {
+        if (uiState is SnapshotSplash) {
+            thumbnailView.imageMatrix = matrix
+        }
     }
 
     private fun logDebug(message: String) {
diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModel.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModel.kt
index 81a904b..9bff3ac 100644
--- a/quickstep/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModel.kt
+++ b/quickstep/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModel.kt
@@ -19,9 +19,7 @@
 import android.graphics.Matrix
 import com.android.launcher3.util.coroutines.DispatcherProvider
 import com.android.quickstep.recents.data.RecentTasksRepository
-import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
-import com.android.quickstep.recents.usecase.ThumbnailPositionState.MatrixScaling
-import com.android.quickstep.recents.usecase.ThumbnailPositionState.MissingThumbnail
+import com.android.quickstep.recents.domain.usecase.GetThumbnailPositionUseCase
 import com.android.quickstep.recents.viewmodel.RecentsViewData
 import com.android.quickstep.task.thumbnail.TaskOverlayUiState.Disabled
 import com.android.quickstep.task.thumbnail.TaskOverlayUiState.Enabled
@@ -36,7 +34,7 @@
     private val task: Task,
     recentsViewData: RecentsViewData,
     private val getThumbnailPositionUseCase: GetThumbnailPositionUseCase,
-    recentTasksRepository: RecentTasksRepository,
+    private val recentTasksRepository: RecentTasksRepository,
     dispatcherProvider: DispatcherProvider,
 ) {
     val overlayState =
@@ -60,22 +58,17 @@
             .flowOn(dispatcherProvider.background)
 
     fun getThumbnailPositionState(width: Int, height: Int, isRtl: Boolean): ThumbnailPositionState {
-        val matrix: Matrix
-        val isRotated: Boolean
-        when (
-            val thumbnailPositionState =
-                getThumbnailPositionUseCase.run(task.key.id, width, height, isRtl)
-        ) {
-            is MatrixScaling -> {
-                matrix = thumbnailPositionState.matrix
-                isRotated = thumbnailPositionState.isRotated
-            }
-            is MissingThumbnail -> {
-                matrix = Matrix.IDENTITY_MATRIX
-                isRotated = false
-            }
-        }
-        return ThumbnailPositionState(matrix, isRotated)
+        val thumbnailPositionState =
+            getThumbnailPositionUseCase(
+                thumbnailData = recentTasksRepository.getCurrentThumbnailById(task.key.id),
+                width = width,
+                height = height,
+                isRtl = isRtl,
+            )
+        return ThumbnailPositionState(
+            thumbnailPositionState.matrix,
+            thumbnailPositionState.isRotated,
+        )
     }
 
     data class ThumbnailPositionState(val matrix: Matrix, val isRotated: Boolean)
diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt
deleted file mode 100644
index e641737..0000000
--- a/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt
+++ /dev/null
@@ -1,28 +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.task.viewmodel
-
-import android.graphics.Matrix
-
-/** ViewModel for representing TaskThumbnails */
-interface TaskThumbnailViewModel {
-    /** Attaches this ViewModel to a specific task id for it to provide data from. */
-    fun bind(taskId: Int)
-
-    /** Returns a Matrix which can be applied to the snapshot */
-    fun getThumbnailPositionState(width: Int, height: Int, isRtl: Boolean): Matrix
-}
diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModelImpl.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModelImpl.kt
deleted file mode 100644
index 94c40d1..0000000
--- a/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModelImpl.kt
+++ /dev/null
@@ -1,47 +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 goveryning permissions and
- * limitations under the License.
- */
-
-package com.android.quickstep.task.viewmodel
-
-import android.app.ActivityTaskManager.INVALID_TASK_ID
-import android.graphics.Matrix
-import android.util.Log
-import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
-import com.android.quickstep.recents.usecase.ThumbnailPositionState
-
-class TaskThumbnailViewModelImpl(
-    private val getThumbnailPositionUseCase: GetThumbnailPositionUseCase
-) : TaskThumbnailViewModel {
-    private var taskId: Int = INVALID_TASK_ID
-
-    override fun bind(taskId: Int) {
-        Log.d(TAG, "bind taskId: $taskId")
-        this.taskId = taskId
-    }
-
-    override fun getThumbnailPositionState(width: Int, height: Int, isRtl: Boolean): Matrix =
-        when (
-            val thumbnailPositionState =
-                getThumbnailPositionUseCase.run(taskId, width, height, isRtl)
-        ) {
-            is ThumbnailPositionState.MatrixScaling -> thumbnailPositionState.matrix
-            is ThumbnailPositionState.MissingThumbnail -> Matrix.IDENTITY_MATRIX
-        }
-
-    private companion object {
-        const val TAG = "TaskThumbnailViewModel"
-    }
-}
diff --git a/quickstep/src/com/android/quickstep/util/AppPairsController.java b/quickstep/src/com/android/quickstep/util/AppPairsController.java
index f20d7a5..8385485 100644
--- a/quickstep/src/com/android/quickstep/util/AppPairsController.java
+++ b/quickstep/src/com/android/quickstep/util/AppPairsController.java
@@ -26,6 +26,7 @@
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT;
+import static com.android.systemui.shared.recents.utilities.Utilities.isFreeformTask;
 import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50;
 import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_NONE;
 import static com.android.wm.shell.shared.split.SplitScreenConstants.getIndex;
@@ -69,6 +70,7 @@
 import com.android.quickstep.views.TaskView;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
 import com.android.wm.shell.shared.split.SplitScreenConstants.PersistentSnapPosition;
 
 import java.util.Arrays;
@@ -337,10 +339,12 @@
      *   c) App B is on-screen, but App A isn't.
      *   d) Neither is on-screen.
      *
-     * If the user tapped an app pair while inside a single app, there are 3 cases:
-     *   a) The on-screen app is App A of the app pair.
-     *   b) The on-screen app is App B of the app pair.
-     *   c) It is neither.
+     * If the user tapped an app pair while a fullscreen or freeform app is visible on screen,
+     * there are 4 cases:
+     *   a) At least one of the apps in the app pair is in freeform windowing mode.
+     *   b) The on-screen app is App A of the app pair.
+     *   c) The on-screen app is App B of the app pair.
+     *   d) It is neither.
      *
      * For each case, we call the appropriate animation and split launch type.
      */
@@ -422,6 +426,14 @@
                     foundTasks -> {
                         Task foundTask1 = foundTasks[0];
                         Task foundTask2 = foundTasks[1];
+
+                        if (DesktopModeStatus.canEnterDesktopMode(context) && (isFreeformTask(
+                                foundTask1) || isFreeformTask(foundTask2))) {
+                            launchAppPair(launchingIconView,
+                                    CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_TASKBAR);
+                            return;
+                        }
+
                         boolean task1IsOnScreen;
                         boolean task2IsOnScreen;
                         if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
diff --git a/quickstep/src/com/android/quickstep/util/AsyncClockEventDelegate.java b/quickstep/src/com/android/quickstep/util/AsyncClockEventDelegate.java
index 54f6443..207e482 100644
--- a/quickstep/src/com/android/quickstep/util/AsyncClockEventDelegate.java
+++ b/quickstep/src/com/android/quickstep/util/AsyncClockEventDelegate.java
@@ -59,8 +59,7 @@
 
     private final Context mContext;
     private final SettingsCache mSettingsCache;
-    private final SimpleBroadcastReceiver mReceiver =
-            new SimpleBroadcastReceiver(UI_HELPER_EXECUTOR, this::onClockEventReceived);
+    private final SimpleBroadcastReceiver mReceiver;
 
     private final ArrayMap<BroadcastReceiver, Handler> mTimeEventReceivers = new ArrayMap<>();
     private final List<ContentObserver> mFormatObservers = new ArrayList<>();
@@ -76,7 +75,9 @@
         super(context);
         mContext = context;
         mSettingsCache = settingsCache;
-        mReceiver.register(mContext, ACTION_TIME_CHANGED, ACTION_TIMEZONE_CHANGED);
+        mReceiver = new SimpleBroadcastReceiver(
+                context, UI_HELPER_EXECUTOR, this::onClockEventReceived);
+        mReceiver.register(ACTION_TIME_CHANGED, ACTION_TIMEZONE_CHANGED);
         tracker.addCloseable(this);
     }
 
@@ -138,6 +139,6 @@
     public void close() {
         mDestroyed = true;
         mSettingsCache.unregister(mFormatUri, this);
-        mReceiver.unregisterReceiverSafely(mContext);
+        mReceiver.unregisterReceiverSafely();
     }
 }
diff --git a/quickstep/src/com/android/quickstep/util/ContextualSearchStateManager.java b/quickstep/src/com/android/quickstep/util/ContextualSearchStateManager.java
index ed96399..a8d3c6d 100644
--- a/quickstep/src/com/android/quickstep/util/ContextualSearchStateManager.java
+++ b/quickstep/src/com/android/quickstep/util/ContextualSearchStateManager.java
@@ -74,8 +74,7 @@
             Settings.Secure.getUriFor(Settings.Secure.SEARCH_ALL_ENTRYPOINTS_ENABLED);
 
     private final Runnable mSysUiStateChangeListener = this::updateOverridesToSysUi;
-    private final SimpleBroadcastReceiver mContextualSearchPackageReceiver =
-            new SimpleBroadcastReceiver(UI_HELPER_EXECUTOR, (unused) -> requestUpdateProperties());
+    private final SimpleBroadcastReceiver mContextualSearchPackageReceiver;
     protected final EventLogArray mEventLogArray = new EventLogArray(TAG, MAX_DEBUG_EVENT_SIZE);
 
     // Cached value whether the ContextualSearch intent filter matched any enabled components.
@@ -95,6 +94,9 @@
             TopTaskTracker topTaskTracker,
             DaggerSingletonTracker lifeCycle) {
         mContext = context;
+        mContextualSearchPackageReceiver =
+                new SimpleBroadcastReceiver(context, UI_HELPER_EXECUTOR,
+                        (unused) -> requestUpdateProperties());
         mContextualSearchPackage = mContext.getResources().getString(
                 com.android.internal.R.string.config_defaultContextualSearchPackageName);
         mSystemUiProxy = systemUiProxy;
@@ -112,7 +114,7 @@
         requestUpdateProperties();
         registerSearchScreenSystemAction();
         mContextualSearchPackageReceiver.registerPkgActions(
-                context, mContextualSearchPackage, Intent.ACTION_PACKAGE_ADDED,
+                mContextualSearchPackage, Intent.ACTION_PACKAGE_ADDED,
                 Intent.ACTION_PACKAGE_CHANGED, Intent.ACTION_PACKAGE_REMOVED);
 
         SettingsCache.OnChangeListener settingChangedListener =
@@ -124,7 +126,7 @@
         systemUiProxy.addOnStateChangeListener(mSysUiStateChangeListener);
 
         lifeCycle.addCloseable(() -> {
-            mContextualSearchPackageReceiver.unregisterReceiverSafely(mContext);
+            mContextualSearchPackageReceiver.unregisterReceiverSafely();
             unregisterSearchScreenSystemAction();
             settingsCache.unregister(SEARCH_ALL_ENTRYPOINTS_ENABLED_URI, settingChangedListener);
             systemUiProxy.removeOnStateChangeListener(mSysUiStateChangeListener);
diff --git a/quickstep/src/com/android/quickstep/util/DesktopTask.kt b/quickstep/src/com/android/quickstep/util/DesktopTask.kt
index 53ea022..fbe3bc6 100644
--- a/quickstep/src/com/android/quickstep/util/DesktopTask.kt
+++ b/quickstep/src/com/android/quickstep/util/DesktopTask.kt
@@ -20,17 +20,19 @@
 
 /**
  * A [Task] container that can contain N number of tasks that are part of the desktop in recent
- * tasks list. Note that desktops can be empty with no tasks in them.
+ * tasks list. Note that desktops can be empty with no tasks in them. The [deskId] makes sense only
+ * when the multiple desks feature is enabled.
  */
-class DesktopTask(tasks: List<Task>) : GroupTask(tasks, TaskViewType.DESKTOP) {
+class DesktopTask(val deskId: Int, tasks: List<Task>) : GroupTask(tasks, TaskViewType.DESKTOP) {
 
-    override fun copy() = DesktopTask(tasks)
+    override fun copy() = DesktopTask(deskId, tasks)
 
-    override fun toString() = "type=$taskViewType tasks=$tasks"
+    override fun toString() = "type=$taskViewType deskId=$deskId tasks=$tasks"
 
     override fun equals(o: Any?): Boolean {
         if (this === o) return true
         if (o !is DesktopTask) return false
+        if (deskId != o.deskId) return false
         return super.equals(o)
     }
 }
diff --git a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
index 88d14b7..a9dbbf2 100644
--- a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
+++ b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
@@ -127,7 +127,7 @@
                     val drawable = getDrawable(container.iconView, splitSelectSource)
                     return SplitAnimInitProps(
                         container.snapshotView,
-                        container.splitAnimationThumbnail,
+                        container.thumbnail,
                         drawable,
                         fadeWithThumbnail = true,
                         isStagedTask = true,
@@ -147,7 +147,7 @@
                 val drawable = getDrawable(it.iconView, splitSelectSource)
                 return SplitAnimInitProps(
                     it.snapshotView,
-                    it.splitAnimationThumbnail,
+                    it.thumbnail,
                     drawable,
                     fadeWithThumbnail = true,
                     isStagedTask = true,
@@ -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/StaggeredWorkspaceAnim.java b/quickstep/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java
index 12ca257..a2856a6 100644
--- a/quickstep/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java
+++ b/quickstep/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java
@@ -81,8 +81,10 @@
 
     public StaggeredWorkspaceAnim(QuickstepLauncher launcher, float velocity,
             boolean animateOverviewScrim, @Nullable View ignoredView, boolean staggerWorkspace) {
+        boolean isPinnedTaskbarAndNotInDesktopMode = DisplayController.isPinnedTaskbar(launcher)
+                && !DisplayController.isInDesktopMode(launcher);
         mTaskbarDurationInMs = QuickstepTransitionManager.getTaskbarToHomeDuration(
-                DisplayController.isPinnedTaskbar(launcher));
+                isPinnedTaskbarAndNotInDesktopMode);
         prepareToAnimate(launcher, animateOverviewScrim);
 
         mIgnoredView = ignoredView;
diff --git a/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java b/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
index 7d5b471..d2f9652 100644
--- a/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
+++ b/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
@@ -18,6 +18,7 @@
 
 import android.animation.Animator;
 import android.animation.RectEvaluator;
+import android.app.PictureInPictureParams;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.pm.ActivityInfo;
@@ -25,6 +26,7 @@
 import android.graphics.Rect;
 import android.graphics.RectF;
 import android.util.Log;
+import android.util.Rational;
 import android.view.Surface;
 import android.view.SurfaceControl;
 import android.view.View;
@@ -50,8 +52,6 @@
 
     private static final float END_PROGRESS = 1.0f;
 
-    private static final float PIP_ASPECT_RATIO_MISMATCH_THRESHOLD = 0.01f;
-
     private final int mTaskId;
     private final ActivityInfo mActivityInfo;
     private final SurfaceControl mLeash;
@@ -158,9 +158,8 @@
             // not a valid rectangle to use for cropping app surface
             reasonForCreateOverlay = "Source rect hint exceeds display bounds " + sourceRectHint;
             sourceRectHint.setEmpty();
-        } else if (Math.abs(
-                aspectRatio - (sourceRectHint.width() / (float) sourceRectHint.height()))
-                > PIP_ASPECT_RATIO_MISMATCH_THRESHOLD) {
+        } else if (!PictureInPictureParams.isSameAspectRatio(sourceRectHint,
+                new Rational(destinationBounds.width(), destinationBounds.height()))) {
             // The source rect hint does not aspect ratio
             reasonForCreateOverlay = "Source rect hint does not match aspect ratio "
                     + sourceRectHint + " aspect ratio " + aspectRatio;
diff --git a/quickstep/src/com/android/quickstep/util/SystemWindowManagerProxy.java b/quickstep/src/com/android/quickstep/util/SystemWindowManagerProxy.java
index 10ae7a3..d92cc86 100644
--- a/quickstep/src/com/android/quickstep/util/SystemWindowManagerProxy.java
+++ b/quickstep/src/com/android/quickstep/util/SystemWindowManagerProxy.java
@@ -73,8 +73,8 @@
     }
 
     @Override
-    public boolean isInDesktopMode() {
-        return mDesktopVisibilityController.areDesktopTasksVisible();
+    public boolean isInDesktopMode(int displayId) {
+        return mDesktopVisibilityController.isInDesktopMode(displayId);
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.java b/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.java
deleted file mode 100644
index da26622..0000000
--- a/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.java
+++ /dev/null
@@ -1,140 +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.util;
-
-import static java.lang.annotation.RetentionPolicy.SOURCE;
-
-import androidx.annotation.IntDef;
-
-import com.android.launcher3.util.IntArray;
-
-import java.lang.annotation.Retention;
-import java.util.List;
-
-/**
- * Helper class for navigating RecentsView grid tasks via arrow keys and tab.
- */
-public class TaskGridNavHelper {
-    public static final int CLEAR_ALL_PLACEHOLDER_ID = -1;
-    public static final int ADD_DESK_PLACEHOLDER_ID = -2;
-
-    public static final int DIRECTION_UP = 0;
-    public static final int DIRECTION_DOWN = 1;
-    public static final int DIRECTION_LEFT = 2;
-    public static final int DIRECTION_RIGHT = 3;
-    public static final int DIRECTION_TAB = 4;
-
-    @Retention(SOURCE)
-    @IntDef({DIRECTION_UP, DIRECTION_DOWN, DIRECTION_LEFT, DIRECTION_RIGHT, DIRECTION_TAB})
-    public @interface TASK_NAV_DIRECTION {}
-
-    private final IntArray mOriginalTopRowIds;
-    private final IntArray mTopRowIds = new IntArray();
-    private final IntArray mBottomRowIds = new IntArray();
-
-    public TaskGridNavHelper(IntArray topIds, IntArray bottomIds,
-            List<Integer> largeTileIds, boolean hasAddDesktopButton) {
-        mOriginalTopRowIds = topIds.clone();
-        generateTaskViewIdGrid(topIds, bottomIds, largeTileIds, hasAddDesktopButton);
-    }
-
-    private void generateTaskViewIdGrid(IntArray topRowIdArray, IntArray bottomRowIdArray,
-            List<Integer> largeTileIds, boolean hasAddDesktopButton) {
-        // Add AddDesktopButton and lage tiles to both rows.
-        if (hasAddDesktopButton) {
-            mTopRowIds.add(ADD_DESK_PLACEHOLDER_ID);
-            mBottomRowIds.add(ADD_DESK_PLACEHOLDER_ID);
-        }
-        for (Integer tileId : largeTileIds) {
-            mTopRowIds.add(tileId);
-            mBottomRowIds.add(tileId);
-        }
-
-        // Add row ids to their respective rows.
-        mTopRowIds.addAll(topRowIdArray);
-        mBottomRowIds.addAll(bottomRowIdArray);
-
-        // Fill in the shorter array with the ids from the longer one.
-        while (mTopRowIds.size() > mBottomRowIds.size()) {
-            mBottomRowIds.add(mTopRowIds.get(mBottomRowIds.size()));
-        }
-        while (mBottomRowIds.size() > mTopRowIds.size()) {
-            mTopRowIds.add(mBottomRowIds.get(mTopRowIds.size()));
-        }
-
-        // Add the clear all button to the end of both arrays.
-        mTopRowIds.add(CLEAR_ALL_PLACEHOLDER_ID);
-        mBottomRowIds.add(CLEAR_ALL_PLACEHOLDER_ID);
-    }
-
-    /**
-     * Returns the id of the next page in the grid or -1 for the clear all button.
-     */
-    public int getNextGridPage(int currentPageTaskViewId, int delta,
-            @TASK_NAV_DIRECTION int direction, boolean cycle) {
-        boolean inTop = mTopRowIds.contains(currentPageTaskViewId);
-        int index = inTop ? mTopRowIds.indexOf(currentPageTaskViewId)
-                : mBottomRowIds.indexOf(currentPageTaskViewId);
-        int maxSize = Math.max(mTopRowIds.size(), mBottomRowIds.size());
-        int nextIndex = index + delta;
-
-        switch (direction) {
-            case DIRECTION_UP:
-            case DIRECTION_DOWN: {
-                return inTop ? mBottomRowIds.get(index) : mTopRowIds.get(index);
-            }
-            case DIRECTION_LEFT: {
-                int boundedIndex = cycle ? nextIndex % maxSize : Math.min(nextIndex, maxSize - 1);
-                return inTop ? mTopRowIds.get(boundedIndex)
-                        : mBottomRowIds.get(boundedIndex);
-            }
-            case DIRECTION_RIGHT: {
-                int boundedIndex =
-                        cycle ? (nextIndex < 0 ? maxSize - 1 : nextIndex)
-                                : Math.max(nextIndex, 0);
-                boolean inOriginalTop = mOriginalTopRowIds.contains(currentPageTaskViewId);
-                return inOriginalTop ? mTopRowIds.get(boundedIndex)
-                        : mBottomRowIds.get(boundedIndex);
-            }
-            case DIRECTION_TAB: {
-                int boundedIndex =
-                        cycle ? (nextIndex < 0 ? maxSize - 1 : nextIndex % maxSize)
-                                : Math.min(nextIndex, maxSize - 1);
-                if (delta >= 0) {
-                    return inTop && mTopRowIds.get(index) != mBottomRowIds.get(index)
-                            ? mBottomRowIds.get(index)
-                            : mTopRowIds.get(boundedIndex);
-                } else {
-                    if (mTopRowIds.contains(currentPageTaskViewId)) {
-                        if (boundedIndex < 0) {
-                            // If no cycling, always return the first task.
-                            return mTopRowIds.get(0);
-                        } else {
-                            return mBottomRowIds.get(boundedIndex);
-                        }
-                    } else {
-                        // Go up to top if there is task above
-                        return mTopRowIds.get(index) != mBottomRowIds.get(index)
-                                ? mTopRowIds.get(index)
-                                : mBottomRowIds.get(boundedIndex);
-                    }
-                }
-            }
-            default:
-                return currentPageTaskViewId;
-        }
-    }
-}
diff --git a/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.kt b/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.kt
new file mode 100644
index 0000000..0e78801
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.kt
@@ -0,0 +1,159 @@
+/*
+ * 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.util
+
+import com.android.launcher3.util.IntArray
+import kotlin.math.abs
+import kotlin.math.max
+
+/** Helper class for navigating RecentsView grid tasks via arrow keys and tab. */
+class TaskGridNavHelper(
+    private val topIds: IntArray,
+    bottomIds: IntArray,
+    largeTileIds: List<Int>,
+    hasAddDesktopButton: Boolean,
+) {
+    private val topRowIds = mutableListOf<Int>()
+    private val bottomRowIds = mutableListOf<Int>()
+
+    init {
+        // Add AddDesktopButton and lage tiles to both rows.
+        if (hasAddDesktopButton) {
+            topRowIds += ADD_DESK_PLACEHOLDER_ID
+            bottomRowIds += ADD_DESK_PLACEHOLDER_ID
+        }
+        topRowIds += largeTileIds
+        bottomRowIds += largeTileIds
+
+        // Add row ids to their respective rows.
+        topRowIds += topIds
+        bottomRowIds += bottomIds
+
+        // Fill in the shorter array with the ids from the longer one.
+        topRowIds += bottomRowIds.takeLast(max(bottomRowIds.size - topRowIds.size, 0))
+        bottomRowIds += topRowIds.takeLast(max(topRowIds.size - bottomRowIds.size, 0))
+
+        // Add the clear all button to the end of both arrays.
+        topRowIds += CLEAR_ALL_PLACEHOLDER_ID
+        bottomRowIds += CLEAR_ALL_PLACEHOLDER_ID
+    }
+
+    /** Returns the id of the next page in the grid or -1 for the clear all button. */
+    fun getNextGridPage(
+        currentPageTaskViewId: Int,
+        delta: Int,
+        direction: TaskNavDirection,
+        cycle: Boolean,
+    ): Int {
+        val inTop = topRowIds.contains(currentPageTaskViewId)
+        val index =
+            if (inTop) topRowIds.indexOf(currentPageTaskViewId)
+            else bottomRowIds.indexOf(currentPageTaskViewId)
+        val maxSize = max(topRowIds.size, bottomRowIds.size)
+        val nextIndex = index + delta
+
+        return when (direction) {
+            TaskNavDirection.UP,
+            TaskNavDirection.DOWN -> {
+                if (inTop) bottomRowIds[index] else topRowIds[index]
+            }
+            TaskNavDirection.LEFT -> {
+                val boundedIndex =
+                    if (cycle) nextIndex % maxSize else nextIndex.coerceAtMost(maxSize - 1)
+                if (inTop) topRowIds[boundedIndex] else bottomRowIds[boundedIndex]
+            }
+            TaskNavDirection.RIGHT -> {
+                val boundedIndex =
+                    if (cycle) (if (nextIndex < 0) maxSize - 1 else nextIndex)
+                    else nextIndex.coerceAtLeast(0)
+                val inOriginalTop = topIds.contains(currentPageTaskViewId)
+                if (inOriginalTop) topRowIds[boundedIndex] else bottomRowIds[boundedIndex]
+            }
+            TaskNavDirection.TAB -> {
+                val boundedIndex =
+                    if (cycle) (if (nextIndex < 0) maxSize - 1 else nextIndex % maxSize)
+                    else nextIndex.coerceAtMost(maxSize - 1)
+                if (delta >= 0) {
+                    if (inTop && topRowIds[index] != bottomRowIds[index]) bottomRowIds[index]
+                    else topRowIds[boundedIndex]
+                } else {
+                    if (topRowIds.contains(currentPageTaskViewId)) {
+                        if (boundedIndex < 0) {
+                            // If no cycling, always return the first task.
+                            topRowIds[0]
+                        } else {
+                            bottomRowIds[boundedIndex]
+                        }
+                    } else {
+                        // Go up to top if there is task above
+                        if (topRowIds[index] != bottomRowIds[index]) topRowIds[index]
+                        else bottomRowIds[boundedIndex]
+                    }
+                }
+            }
+            else -> currentPageTaskViewId
+        }
+    }
+
+    /**
+     * Returns a sequence of pairs of (TaskView ID, offset) in the grid, ordered according to tab
+     * navigation, starting from the initial TaskView ID, towards the start or end of the grid.
+     *
+     * <p>A positive delta moves forward in the tab order towards the end of the grid, while a
+     * negative value moves backward towards the beginning. The offset is the distance between
+     * columns the tasks are in.
+     */
+    fun gridTaskViewIdOffsetPairInTabOrderSequence(
+        initialTaskViewId: Int,
+        towardsStart: Boolean,
+    ): Sequence<Pair<Int, Int>> = sequence {
+        val draggedTaskViewColumn = getColumn(initialTaskViewId)
+        var nextTaskViewId: Int = initialTaskViewId
+        var previousTaskViewId: Int = Int.MIN_VALUE
+        while (nextTaskViewId != previousTaskViewId && nextTaskViewId >= 0) {
+            previousTaskViewId = nextTaskViewId
+            nextTaskViewId =
+                getNextGridPage(
+                    nextTaskViewId,
+                    if (towardsStart) -1 else 1,
+                    TaskNavDirection.TAB,
+                    cycle = false,
+                )
+            if (nextTaskViewId >= 0 && nextTaskViewId != previousTaskViewId) {
+                val columnOffset = abs(getColumn(nextTaskViewId) - draggedTaskViewColumn)
+                yield(Pair(nextTaskViewId, columnOffset))
+            }
+        }
+    }
+
+    /** Returns the column of a task's id in the grid. */
+    private fun getColumn(taskViewId: Int): Int =
+        if (topRowIds.contains(taskViewId)) topRowIds.indexOf(taskViewId)
+        else bottomRowIds.indexOf(taskViewId)
+
+    enum class TaskNavDirection {
+        UP,
+        DOWN,
+        LEFT,
+        RIGHT,
+        TAB,
+    }
+
+    companion object {
+        const val CLEAR_ALL_PLACEHOLDER_ID: Int = -1
+        const val ADD_DESK_PLACEHOLDER_ID: Int = -2
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/util/TaskRestartedDuringLaunchListener.java b/quickstep/src/com/android/quickstep/util/TaskRestartedDuringLaunchListener.java
index 91e8376..6e2d469 100644
--- a/quickstep/src/com/android/quickstep/util/TaskRestartedDuringLaunchListener.java
+++ b/quickstep/src/com/android/quickstep/util/TaskRestartedDuringLaunchListener.java
@@ -16,16 +16,11 @@
 
 package com.android.quickstep.util;
 
-import static android.app.ActivityTaskManager.INVALID_TASK_ID;
-
-import android.app.Activity;
 import android.app.ActivityManager;
 import android.util.Log;
 
 import androidx.annotation.NonNull;
 
-import com.android.launcher3.util.ActivityLifecycleCallbacksAdapter;
-import com.android.quickstep.RecentsModel;
 import com.android.systemui.shared.system.TaskStackChangeListener;
 import com.android.systemui.shared.system.TaskStackChangeListeners;
 
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/util/TransformParams.java b/quickstep/src/com/android/quickstep/util/TransformParams.java
index 1c1fbd8..716803a 100644
--- a/quickstep/src/com/android/quickstep/util/TransformParams.java
+++ b/quickstep/src/com/android/quickstep/util/TransformParams.java
@@ -222,7 +222,7 @@
         return null;
     }
 
-    // Pubic getters so outside packages can read the values.
+    // Public getters so outside packages can read the values.
 
     public float getProgress() {
         return mProgress;
diff --git a/quickstep/src/com/android/quickstep/views/AddDesktopButton.kt b/quickstep/src/com/android/quickstep/views/AddDesktopButton.kt
index 9f3c017..244c3b2 100644
--- a/quickstep/src/com/android/quickstep/views/AddDesktopButton.kt
+++ b/quickstep/src/com/android/quickstep/views/AddDesktopButton.kt
@@ -20,10 +20,12 @@
 import android.graphics.Canvas
 import android.graphics.Rect
 import android.util.AttributeSet
+import android.view.View
 import android.widget.ImageButton
 import com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X
 import com.android.launcher3.R
 import com.android.launcher3.util.MultiPropertyFactory
+import com.android.launcher3.util.MultiValueAlpha
 import com.android.quickstep.util.BorderAnimator
 import com.android.quickstep.util.BorderAnimator.Companion.createSimpleBorderAnimator
 
@@ -34,6 +36,22 @@
 class AddDesktopButton @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
     ImageButton(context, attrs) {
 
+    private enum class Alpha {
+        CONTENT,
+        VISIBILITY,
+    }
+
+    private val addDeskButtonAlpha = MultiValueAlpha(this, Alpha.entries.size)
+
+    var contentAlpha
+        set(value) {
+            addDeskButtonAlpha.get(Alpha.CONTENT.ordinal).value = value
+        }
+        get() = addDeskButtonAlpha.get(Alpha.CONTENT.ordinal).value
+
+    val visibilityAlphaProperty: MultiPropertyFactory<View>.MultiProperty
+        get() = addDeskButtonAlpha.get(Alpha.VISIBILITY.ordinal)
+
     private enum class TranslationX {
         GRID,
         OFFSET,
@@ -91,6 +109,9 @@
         }
     }
 
+    protected fun getScrollAdjustment(showAsGrid: Boolean): Int =
+        if (showAsGrid) gridTranslationX.toInt() else 0
+
     private fun getBorderBounds(bounds: Rect) {
         bounds.set(0, 0, width, height)
         val outlinePadding =
diff --git a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
index da160f1..75f3b69 100644
--- a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
@@ -15,12 +15,12 @@
  */
 package com.android.quickstep.views
 
-import android.animation.Animator
 import android.annotation.SuppressLint
 import android.content.Context
 import android.graphics.Matrix
 import android.graphics.PointF
 import android.graphics.Rect
+import android.graphics.Rect.intersects
 import android.graphics.RectF
 import android.util.AttributeSet
 import android.util.Log
@@ -30,11 +30,11 @@
 import android.view.ViewStub
 import androidx.core.content.res.ResourcesCompat
 import androidx.core.view.updateLayoutParams
+import com.android.internal.hidden_from_bootclasspath.com.android.window.flags.Flags.enableDesktopRecentsTransitionsCornersBugfix
 import com.android.launcher3.Flags.enableDesktopExplodedView
 import com.android.launcher3.Flags.enableOverviewIconMenu
 import com.android.launcher3.Flags.enableRefactorTaskThumbnail
 import com.android.launcher3.R
-import com.android.launcher3.anim.AnimatedFloat
 import com.android.launcher3.testing.TestLogging
 import com.android.launcher3.testing.shared.TestProtocol
 import com.android.launcher3.util.RunnableList
@@ -57,6 +57,7 @@
 import com.android.quickstep.task.thumbnail.TaskThumbnailView
 import com.android.quickstep.util.DesktopTask
 import com.android.quickstep.util.RecentsOrientedState
+import kotlin.math.roundToInt
 
 /** TaskView that contains all tasks that are part of the desktop. */
 class DesktopTaskView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
@@ -102,7 +103,7 @@
      * Holds the default (user placed) positions of task windows. This can be moved into the
      * viewModel once RefactorTaskThumbnail has been launched.
      */
-    private var defaultTaskPositions: List<DesktopTaskBoundsData> = emptyList()
+    private var fullscreenTaskPositions: List<DesktopTaskBoundsData> = emptyList()
 
     /**
      * When enableDesktopExplodedView is enabled, this controls the gradual transition from the
@@ -162,9 +163,6 @@
             ?.inflate()
     }
 
-    fun startWindowExplodeAnimation(): Animator =
-        AnimatedFloat { progress -> explodeProgress = progress }.animateToValue(0.0f, 1.0f)
-
     private fun positionTaskWindows() {
         if (taskContainers.isEmpty()) {
             return
@@ -172,42 +170,44 @@
 
         val thumbnailTopMarginPx = container.deviceProfile.overviewTaskThumbnailTopMarginPx
 
-        val containerWidth = layoutParams.width
-        val containerHeight = layoutParams.height - thumbnailTopMarginPx
+        val taskViewWidth = layoutParams.width
+        val taskViewHeight = layoutParams.height - thumbnailTopMarginPx
 
         BaseContainerInterface.getTaskDimension(mContext, container.deviceProfile, tempPointF)
 
-        val windowWidth = tempPointF.x.toInt()
-        val windowHeight = tempPointF.y.toInt()
-        val scaleWidth = containerWidth / windowWidth.toFloat()
-        val scaleHeight = containerHeight / windowHeight.toFloat()
+        val screenWidth = tempPointF.x.toInt()
+        val screenHeight = tempPointF.y.toInt()
+        val screenRect = Rect(0, 0, screenWidth, screenHeight)
+        val scaleWidth = taskViewWidth / screenWidth.toFloat()
+        val scaleHeight = taskViewHeight / screenHeight.toFloat()
 
         taskContainers.forEach {
             val taskId = it.task.key.id
-            val defaultPosition = defaultTaskPositions.firstOrNull { it.taskId == taskId } ?: return
-            val position =
+            val fullscreenTaskPosition =
+                fullscreenTaskPositions.firstOrNull { it.taskId == taskId } ?: return
+            val overviewTaskPosition =
                 if (enableDesktopExplodedView()) {
                     viewModel!!
                         .organizedDesktopTaskPositions
                         .firstOrNull { it.taskId == taskId }
                         ?.let { organizedPosition ->
-                            TEMP_RECT.apply {
+                            TEMP_OVERVIEW_TASK_POSITION.apply {
                                 lerpRect(
-                                    defaultPosition.bounds,
+                                    fullscreenTaskPosition.bounds,
                                     organizedPosition.bounds,
                                     explodeProgress,
                                 )
                             }
-                        } ?: defaultPosition.bounds
+                        } ?: fullscreenTaskPosition.bounds
                 } else {
-                    defaultPosition.bounds
+                    fullscreenTaskPosition.bounds
                 }
 
             if (enableDesktopExplodedView()) {
                 getRemoteTargetHandle(taskId)?.let { remoteTargetHandle ->
                     val fromRect =
-                        TEMP_RECTF1.apply {
-                            set(defaultPosition.bounds)
+                        TEMP_FROM_RECTF.apply {
+                            set(fullscreenTaskPosition.bounds)
                             scale(scaleWidth)
                             offset(
                                 lastComputedTaskSize.left.toFloat(),
@@ -215,8 +215,8 @@
                             )
                         }
                     val toRect =
-                        TEMP_RECTF2.apply {
-                            set(position)
+                        TEMP_TO_RECTF.apply {
+                            set(overviewTaskPosition)
                             scale(scaleWidth)
                             offset(
                                 lastComputedTaskSize.left.toFloat(),
@@ -230,10 +230,10 @@
                 }
             }
 
-            val taskLeft = position.left * scaleWidth
-            val taskTop = position.top * scaleHeight
-            val taskWidth = position.width() * scaleWidth
-            val taskHeight = position.height() * scaleHeight
+            val taskLeft = overviewTaskPosition.left * scaleWidth
+            val taskTop = overviewTaskPosition.top * scaleHeight
+            val taskWidth = overviewTaskPosition.width() * scaleWidth
+            val taskHeight = overviewTaskPosition.height() * scaleHeight
             // TODO(b/394660950): Revisit the choice to update the layout when explodeProgress == 1.
             // To run the explode animation in reverse, it may be simpler to use translation/scale
             // for all cases where the progress is non-zero.
@@ -254,6 +254,24 @@
                     leftMargin = taskLeft.toInt()
                     topMargin = taskTop.toInt()
                 }
+
+                if (
+                    enableDesktopRecentsTransitionsCornersBugfix() && enableRefactorTaskThumbnail()
+                ) {
+                    it.thumbnailView.outlineBounds =
+                        if (intersects(overviewTaskPosition, screenRect))
+                            Rect(overviewTaskPosition).apply {
+                                intersectUnchecked(screenRect)
+                                // Offset to 0,0 to transform into TaskThumbnailView's coordinate
+                                // system.
+                                offset(-overviewTaskPosition.left, -overviewTaskPosition.top)
+                                left = (left * scaleWidth).roundToInt()
+                                top = (top * scaleHeight).roundToInt()
+                                right = (right * scaleWidth).roundToInt()
+                                bottom = (bottom * scaleHeight).roundToInt()
+                            }
+                        else null
+                }
             } else {
                 // During the animation, apply translation and scale such that the view is
                 // transformed to where we want, without triggering layout.
@@ -327,14 +345,7 @@
         explodeProgress = 0.0f
         viewModel = null
         visibility = VISIBLE
-        taskContainers.forEach {
-            contentView.removeView(it.snapshotView)
-            if (enableRefactorTaskThumbnail()) {
-                taskThumbnailViewPool!!.recycle(it.thumbnailView)
-            } else {
-                taskThumbnailViewDeprecatedPool!!.recycle(it.thumbnailViewDeprecated)
-            }
-        }
+        taskContainers.forEach { removeAndRecycleThumbnailView(it) }
     }
 
     @SuppressLint("RtlHardcoded")
@@ -342,19 +353,7 @@
         super.updateTaskSize(lastComputedTaskSize, lastComputedGridTaskSize)
         this.lastComputedTaskSize.set(lastComputedTaskSize)
 
-        BaseContainerInterface.getTaskDimension(mContext, container.deviceProfile, tempPointF)
-        val desktopSize = Size(tempPointF.x.toInt(), tempPointF.y.toInt())
-        DEFAULT_BOUNDS.set(0, 0, desktopSize.width / 4, desktopSize.height / 4)
-
-        defaultTaskPositions =
-            taskContainers.map {
-                DesktopTaskBoundsData(it.task.key.id, it.task.appBounds ?: DEFAULT_BOUNDS)
-            }
-
-        if (enableDesktopExplodedView()) {
-            viewModel?.organizeDesktopTasks(desktopSize, defaultTaskPositions)
-        }
-        positionTaskWindows()
+        updateTaskPositions()
     }
 
     override fun onTaskListVisibilityChanged(visible: Boolean, changes: Int) {
@@ -440,6 +439,56 @@
         ViewUtils.addAccessibleChildToList(backgroundView, outChildren)
     }
 
+    fun removeTaskFromExplodedView(taskId: Int, animate: Boolean) {
+        if (!enableDesktopExplodedView()) {
+            Log.e(
+                TAG,
+                "removeTaskFromExplodedView called when enableDesktopExplodedView flag is false",
+            )
+            return
+        }
+
+        // Remove the task's [taskContainer] and its associated Views.
+        val taskContainer = getTaskContainerById(taskId) ?: return
+        removeAndRecycleThumbnailView(taskContainer)
+        taskContainer.destroy()
+        taskContainers = taskContainers.filterNot { it == taskContainer }
+
+        // Dismiss the current DesktopTaskView if all its windows are closed.
+        if (taskContainers.isEmpty()) {
+            recentsView?.dismissTaskView(this, animate, /* removeTask= */ true)
+        } else {
+            // Otherwise, re-position the remaining task windows.
+            // TODO(b/353949276): Implement the re-layout animations.
+            updateTaskPositions()
+        }
+    }
+
+    private fun removeAndRecycleThumbnailView(taskContainer: TaskContainer) {
+        contentView.removeView(taskContainer.snapshotView)
+        if (enableRefactorTaskThumbnail()) {
+            taskThumbnailViewPool!!.recycle(taskContainer.thumbnailView)
+        } else {
+            taskThumbnailViewDeprecatedPool!!.recycle(taskContainer.thumbnailViewDeprecated)
+        }
+    }
+
+    private fun updateTaskPositions() {
+        BaseContainerInterface.getTaskDimension(mContext, container.deviceProfile, tempPointF)
+        val desktopSize = Size(tempPointF.x.toInt(), tempPointF.y.toInt())
+        DEFAULT_BOUNDS.set(0, 0, desktopSize.width / 4, desktopSize.height / 4)
+
+        fullscreenTaskPositions =
+            taskContainers.map {
+                DesktopTaskBoundsData(it.task.key.id, it.task.appBounds ?: DEFAULT_BOUNDS)
+            }
+
+        if (enableDesktopExplodedView()) {
+            viewModel?.organizeDesktopTasks(desktopSize, fullscreenTaskPositions)
+        }
+        positionTaskWindows()
+    }
+
     companion object {
         private const val TAG = "DesktopTaskView"
         private const val DEBUG = false
@@ -449,8 +498,8 @@
         private const val VIEW_POOL_INITIAL_SIZE = 0
         private val DEFAULT_BOUNDS = Rect()
         // Temporaries used for various purposes to avoid allocations.
-        private val TEMP_RECT = Rect()
-        private val TEMP_RECTF1 = RectF()
-        private val TEMP_RECTF2 = RectF()
+        private val TEMP_OVERVIEW_TASK_POSITION = Rect()
+        private val TEMP_FROM_RECTF = RectF()
+        private val TEMP_TO_RECTF = RectF()
     }
 }
diff --git a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.kt b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.kt
index c07b7fb..5c4a35d 100644
--- a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.kt
+++ b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.kt
@@ -189,11 +189,11 @@
                 SplitBannerConfig.SPLIT_GRID_BANNER_LARGE
             // For landscape grid, for 30% width we only show icon, otherwise show icon and time
             task.key.id == splitBounds.leftTopTaskId ->
-                if (splitBounds.leftTaskPercent < THRESHOLD_LEFT_ICON_ONLY)
+                if (splitBounds.leftTopTaskPercent < THRESHOLD_LEFT_ICON_ONLY)
                     SplitBannerConfig.SPLIT_GRID_BANNER_SMALL
                 else SplitBannerConfig.SPLIT_GRID_BANNER_LARGE
             else ->
-                if (splitBounds.leftTaskPercent > THRESHOLD_RIGHT_ICON_ONLY)
+                if (splitBounds.leftTopTaskPercent > THRESHOLD_RIGHT_ICON_ONLY)
                     SplitBannerConfig.SPLIT_GRID_BANNER_SMALL
                 else SplitBannerConfig.SPLIT_GRID_BANNER_LARGE
         }
diff --git a/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt b/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
index 229c8f5..a8eee0a 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)
@@ -189,18 +187,30 @@
         val taskIconHeight = deviceProfile.overviewTaskIconSizePx
         val isRtl = layoutDirection == LAYOUT_DIRECTION_RTL
         val inSplitSelection = getThisTaskCurrentlyInSplitSelection() != INVALID_TASK_ID
+        var oneIconHiddenDueToSmallWidth = false
 
         if (enableFlexibleTwoAppSplit()) {
-            val topLeftTaskPercent =
-                if (deviceProfile.isLeftRightSplit) splitBoundsConfig.leftTaskPercent
-                else splitBoundsConfig.topTaskPercent
-            val bottomRightTaskPercent = 1 - topLeftTaskPercent
-            leftTopTaskContainer.iconView.setFlexSplitAlpha(
-                if (topLeftTaskPercent < MINIMUM_RATIO_TO_SHOW_ICON) 0f else 1f
-            )
-            rightBottomTaskContainer.iconView.setFlexSplitAlpha(
-                if (bottomRightTaskPercent < MINIMUM_RATIO_TO_SHOW_ICON) 0f else 1f
-            )
+            // Update values for both icons' setFlexSplitAlpha. Mainly, we want to hide an icon if
+            // its app tile is too small. But we also have to set the alphas back if we go to
+            // split selection.
+            val hideLeftTopIcon: Boolean
+            val hideRightBottomIcon: Boolean
+            if (inSplitSelection) {
+                hideLeftTopIcon =
+                    getThisTaskCurrentlyInSplitSelection() == splitBoundsConfig.leftTopTaskId
+                hideRightBottomIcon =
+                    getThisTaskCurrentlyInSplitSelection() == splitBoundsConfig.rightBottomTaskId
+            } else {
+                hideLeftTopIcon = splitBoundsConfig.leftTopTaskPercent < MINIMUM_RATIO_TO_SHOW_ICON
+                hideRightBottomIcon =
+                    splitBoundsConfig.rightBottomTaskPercent < MINIMUM_RATIO_TO_SHOW_ICON
+                if (hideLeftTopIcon || hideRightBottomIcon) {
+                    oneIconHiddenDueToSmallWidth = true
+                }
+            }
+
+            leftTopTaskContainer.iconView.setFlexSplitAlpha(if (hideLeftTopIcon) 0f else 1f)
+            rightBottomTaskContainer.iconView.setFlexSplitAlpha(if (hideRightBottomIcon) 0f else 1f)
         }
 
         if (enableOverviewIconMenu()) {
@@ -223,6 +233,7 @@
                 deviceProfile,
                 splitBoundsConfig,
                 inSplitSelection,
+                oneIconHiddenDueToSmallWidth,
             )
         } else {
             pagedOrientationHandler.setSplitIconParams(
@@ -237,6 +248,7 @@
                 deviceProfile,
                 splitBoundsConfig,
                 inSplitSelection,
+                oneIconHiddenDueToSmallWidth,
             )
         }
     }
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/IconView.kt b/quickstep/src/com/android/quickstep/views/IconView.kt
index 6da52d6..cb69b22 100644
--- a/quickstep/src/com/android/quickstep/views/IconView.kt
+++ b/quickstep/src/com/android/quickstep/views/IconView.kt
@@ -78,7 +78,8 @@
     override fun setDrawable(d: Drawable?) {
         drawable?.callback = null
 
-        drawable = d
+        // Copy drawable so that mutations below do not affect other users of the drawable
+        drawable = d?.constantState?.newDrawable()?.mutate()
         drawable?.let {
             it.callback = this
             setDrawableSizeInternal(width, height)
diff --git a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
index c6bd677..0f1c294 100644
--- a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
@@ -275,7 +275,8 @@
         boolean showDesktopApps = false;
         GestureState.GestureEndTarget endTarget = mCurrentGestureEndTarget;
         if (endTarget == GestureState.GestureEndTarget.LAST_TASK
-                && desktopVisibilityController.areDesktopTasksVisibleAndNotInOverview()) {
+                && desktopVisibilityController.isInDesktopModeAndNotInOverview(
+                        mContainer.getDisplayId())) {
             // Recents gesture was cancelled and we are returning to the previous task.
             // After super class has handled clean up, show desktop apps on top again
             showDesktopApps = true;
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..3430b39
--- /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.dismissTaskView(
+                            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 88850ab..6b91df1 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -38,6 +38,7 @@
 import static com.android.launcher3.Flags.enableAdditionalHomeAnimations;
 import static com.android.launcher3.Flags.enableDesktopExplodedView;
 import static com.android.launcher3.Flags.enableDesktopTaskAlphaAnimation;
+import static com.android.launcher3.Flags.enableExpressiveDismissTaskMotion;
 import static com.android.launcher3.Flags.enableGridOnlyOverview;
 import static com.android.launcher3.Flags.enableLargeDesktopWindowingTile;
 import static com.android.launcher3.Flags.enableRefactorTaskThumbnail;
@@ -65,11 +66,6 @@
 import static com.android.quickstep.BaseContainerInterface.getTaskDimension;
 import static com.android.quickstep.TaskUtils.checkCurrentOrManagedUserId;
 import static com.android.quickstep.util.LogUtils.splitFailureMessage;
-import static com.android.quickstep.util.TaskGridNavHelper.DIRECTION_DOWN;
-import static com.android.quickstep.util.TaskGridNavHelper.DIRECTION_LEFT;
-import static com.android.quickstep.util.TaskGridNavHelper.DIRECTION_RIGHT;
-import static com.android.quickstep.util.TaskGridNavHelper.DIRECTION_TAB;
-import static com.android.quickstep.util.TaskGridNavHelper.DIRECTION_UP;
 import static com.android.quickstep.views.ClearAllButton.DISMISS_ALPHA;
 import static com.android.quickstep.views.OverviewActionsView.HIDDEN_ACTIONS_IN_MENU;
 import static com.android.quickstep.views.OverviewActionsView.HIDDEN_DESKTOP;
@@ -77,6 +73,7 @@
 import static com.android.quickstep.views.OverviewActionsView.HIDDEN_NO_RECENTS;
 import static com.android.quickstep.views.OverviewActionsView.HIDDEN_NO_TASKS;
 import static com.android.quickstep.views.OverviewActionsView.HIDDEN_SPLIT_SELECT_ACTIVE;
+import static com.android.quickstep.views.RecentsViewUtils.DESK_EXPLODE_PROGRESS;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -132,6 +129,7 @@
 import android.widget.Toast;
 import android.window.DesktopModeFlags;
 import android.window.PictureInPictureSurfaceTransaction;
+import android.window.TransitionInfo;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -161,6 +159,7 @@
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.statehandlers.DepthController;
+import com.android.launcher3.statehandlers.DesktopVisibilityController;
 import com.android.launcher3.statemanager.BaseState;
 import com.android.launcher3.statemanager.StateManager;
 import com.android.launcher3.statemanager.StatefulContainer;
@@ -183,6 +182,7 @@
 import com.android.launcher3.util.VibratorWrapper;
 import com.android.launcher3.util.ViewPool;
 import com.android.launcher3.util.coroutines.DispatcherProvider;
+import com.android.launcher3.util.window.WindowManagerProxy.DesktopVisibilityListener;
 import com.android.quickstep.BaseContainerInterface;
 import com.android.quickstep.GestureState;
 import com.android.quickstep.HighResLoadingState;
@@ -269,7 +269,7 @@
         CONTAINER_TYPE extends Context & RecentsViewContainer & StatefulContainer<STATE_TYPE>,
         STATE_TYPE extends BaseState<STATE_TYPE>> extends PagedView implements Insettable,
         HighResLoadingState.HighResLoadingStateChangedCallback,
-        TaskVisualsChangeListener {
+        TaskVisualsChangeListener, DesktopVisibilityListener {
 
     private static final String TAG = "RecentsView";
     private static final boolean DEBUG = false;
@@ -560,6 +560,10 @@
     private final Rect mTaskViewDeadZoneRect = new Rect();
     private final Rect mTopRowDeadZoneRect = new Rect();
     private final Rect mBottomRowDeadZoneRect = new Rect();
+
+    @Nullable
+    private DesktopVisibilityController mDesktopVisibilityController = null;
+
     /**
      * Reflects if Recents is currently in the middle of a gesture, and if so, which tasks are
      * running. If a gesture is not in progress, this will be null.
@@ -672,11 +676,11 @@
                     MAIN_EXECUTOR,
                     apkRemoved -> {
                         if (apkRemoved) {
-                            dismissTask(taskId);
+                            dismissTask(taskId, /*animate=*/true, /*removeTask=*/false);
                         } else {
                             mModel.isTaskRemoved(taskKey.id, taskRemoved -> {
                                 if (taskRemoved) {
-                                    dismissTask(taskId);
+                                    dismissTask(taskId, /*animate=*/true, /*removeTask=*/false);
                                 }
                             }, RecentsFilterState.getFilter(mFilterState.getPackageNameToFilter()));
                         }
@@ -855,6 +859,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,14 +921,18 @@
             mAddDesktopButton = (AddDesktopButton) LayoutInflater.from(context).inflate(
                     R.layout.overview_add_desktop_button, this, false);
             mAddDesktopButton.setOnClickListener(this::createDesk);
+
+            mDesktopVisibilityController = DesktopVisibilityController.INSTANCE.get(mContext);
         }
 
         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());
@@ -1228,13 +1237,16 @@
         mSyncTransactionApplier = new SurfaceTransactionApplier(this);
         runActionOnRemoteHandles(remoteTargetHandle -> remoteTargetHandle.getTransformParams()
                 .setSyncTransactionApplier(mSyncTransactionApplier));
-        RecentsModel.INSTANCE.get(getContext()).addThumbnailChangeListener(this);
+        RecentsModel.INSTANCE.get(mContext).addThumbnailChangeListener(this);
         mIPipAnimationListener.setActivityAndRecentsView(mContainer, this);
-        SystemUiProxy.INSTANCE.get(getContext()).setPipAnimationListener(
+        SystemUiProxy.INSTANCE.get(mContext).setPipAnimationListener(
                 mIPipAnimationListener);
         mOrientationState.initListeners();
         mTaskOverlayFactory.initListeners();
         mSplitSelectStateController.registerSplitListener(mSplitSelectionListener);
+        if (mDesktopVisibilityController != null) {
+            mDesktopVisibilityController.registerDesktopVisibilityListener(this);
+        }
     }
 
     @Override
@@ -1249,12 +1261,15 @@
         runActionOnRemoteHandles(remoteTargetHandle -> remoteTargetHandle.getTransformParams()
                 .setSyncTransactionApplier(null));
         executeSideTaskLaunchCallback();
-        RecentsModel.INSTANCE.get(getContext()).removeThumbnailChangeListener(this);
-        SystemUiProxy.INSTANCE.get(getContext()).setPipAnimationListener(null);
+        RecentsModel.INSTANCE.get(mContext).removeThumbnailChangeListener(this);
+        SystemUiProxy.INSTANCE.get(mContext).setPipAnimationListener(null);
         mIPipAnimationListener.setActivityAndRecentsView(null, null);
         mOrientationState.destroyListeners();
         mTaskOverlayFactory.removeListeners();
         mSplitSelectStateController.unregisterSplitListener(mSplitSelectionListener);
+        if (mDesktopVisibilityController != null) {
+            mDesktopVisibilityController.unregisterDesktopVisibilityListener(this);
+        }
         reset();
     }
 
@@ -1395,12 +1410,13 @@
         RemoteAnimationTargets targets = params.getTargetSet();
         if (targets != null && targets.findTask(taskId) != null) {
             launchSideTaskInLiveTileMode(taskId, targets.apps, targets.wallpapers,
-                    targets.nonApps);
+                    targets.nonApps, /* transitionInfo= */ null);
         }
     }
 
     public void launchSideTaskInLiveTileMode(int taskId, RemoteAnimationTarget[] apps,
-            RemoteAnimationTarget[] wallpaper, RemoteAnimationTarget[] nonApps) {
+            RemoteAnimationTarget[] wallpaper, RemoteAnimationTarget[] nonApps,
+            @Nullable TransitionInfo transitionInfo) {
         AnimatorSet anim = new AnimatorSet();
         TaskView taskView = getTaskViewByTaskId(taskId);
         if (taskView == null || !isTaskViewVisible(taskView)) {
@@ -1452,7 +1468,7 @@
         } else {
             TaskViewUtils.composeRecentsLaunchAnimator(anim, taskView, apps, wallpaper, nonApps,
                     true /* launcherClosing */, getStateManager(), this,
-                    getDepthController());
+                    getDepthController(), transitionInfo);
         }
         anim.start();
     }
@@ -2013,7 +2029,7 @@
             }
             // If the list changed, maybe the focused task doesn't exist anymore.
             if (newFocusedTaskView == null) {
-                newFocusedTaskView = mUtils.getExpectedFocusedTask();
+                newFocusedTaskView = mUtils.getFirstNonDesktopTaskView();
             }
         }
         setFocusedTaskViewId(
@@ -2119,15 +2135,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();
@@ -2694,8 +2701,6 @@
         }
         if (enableRefactorTaskThumbnail()) {
             mRecentsViewModel.onReset();
-            // TODO(b/391842220) Remove TaskViews rather than calling specific logic to cancel scope
-            getTaskViews().forEach(TaskView::destroyScopes);
         }
     }
 
@@ -2907,13 +2912,15 @@
         }
 
         if (enableDesktopExplodedView()) {
+            if (animatorSet == null) {
+                mUtils.setDeskExplodeProgress(1);
+            } else {
+                animatorSet.play(
+                        ObjectAnimator.ofFloat(this, DESK_EXPLODE_PROGRESS, 1));
+            }
+
             for (TaskView taskView : getTaskViews()) {
                 if (taskView instanceof DesktopTaskView desktopTaskView) {
-                    if (animatorSet == null) {
-                        desktopTaskView.setExplodeProgress(1.0f);
-                    } else {
-                        animatorSet.play(desktopTaskView.startWindowExplodeAnimation());
-                    }
                     desktopTaskView.setRemoteTargetHandles(remoteTargetHandles);
                 }
             }
@@ -2922,28 +2929,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));
+                    }
                 }
             }
         }
@@ -3029,8 +3048,12 @@
             // Add an empty view for now until the task plan is loaded and applied
             final TaskView taskView;
             if (needDesktopTask) {
+                final int activeDeskId =
+                        DesktopVisibilityController.INSTANCE.get(mContext).getActiveDeskId(
+                                mContainer.getDisplay().getDisplayId());
                 taskView = getTaskViewFromPool(TaskViewType.DESKTOP);
-                ((DesktopTaskView) taskView).bind(new DesktopTask(Arrays.asList(runningTasks)),
+                ((DesktopTaskView) taskView).bind(
+                        new DesktopTask(activeDeskId, Arrays.asList(runningTasks)),
                         mOrientationState, mTaskOverlayFactory);
             } else if (needGroupTaskView) {
                 taskView = getTaskViewFromPool(TaskViewType.GROUPED);
@@ -3069,7 +3092,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 {
@@ -3496,8 +3519,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();
+                    }
             );
         }
 
@@ -3611,12 +3639,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();
             });
         }
@@ -3837,7 +3862,8 @@
                 newClearAllShortTotalWidthTranslation = expectedFirstTaskStart - firstTaskStart;
             }
         }
-        if (lastGridTaskView != null && lastGridTaskView.isVisibleToUser()) {
+        if (lastGridTaskView != null && (lastGridTaskView.isVisibleToUser() || (
+                enableExpressiveDismissTaskMotion() && lastGridTaskView == dismissedTaskView))) {
             // After dismissal, animate translation of the remaining tasks to fill any gap left
             // between the end of the grid and the clear all button. Only animate if the clear
             // all button is visible or would become visible after dismissal.
@@ -3882,6 +3908,22 @@
                 // the only invariant point in landscape split screen.
                 snapToLastTask = true;
             }
+            if (mUtils.getGridTaskCount() == 1 && dismissedTaskView.isGridTask()) {
+                TaskView lastLargeTile = mUtils.getLastLargeTaskView();
+                if (lastLargeTile != null) {
+                    // Calculate the distance to put last large tile back to middle of the screen.
+                    int primaryScroll = getPagedOrientationHandler().getPrimaryScroll(this);
+                    int lastLargeTileScroll = getScrollForPage(indexOfChild(lastLargeTile));
+                    longGridRowWidthDiff = primaryScroll - lastLargeTileScroll;
+
+                    if (!isClearAllHidden) {
+                        // If ClearAllButton is visible, reduce the distance by scroll difference
+                        // between ClearAllButton and the last task.
+                        longGridRowWidthDiff += getLastTaskScroll(/*clearAllScroll=*/0,
+                                getPagedOrientationHandler().getPrimarySize(mClearAllButton));
+                    }
+                }
+            }
 
             // If we need to animate the grid to compensate the clear all gap, we split the second
             // half of the dismiss pending animation (in which the non-dismissed tasks slide into
@@ -3938,9 +3980,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
@@ -3966,7 +4008,7 @@
                             Math.abs(i - dismissedIndex),
                             scrollDiff,
                             anim,
-                            splitTimings, i);
+                            splitTimings);
                     needsCurveUpdates = true;
                 }
             } else if (child instanceof TaskView taskView) {
@@ -4342,8 +4384,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();
@@ -4538,7 +4584,7 @@
     }
 
     private boolean snapToPageRelative(int delta, boolean cycle,
-            @TaskGridNavHelper.TASK_NAV_DIRECTION int direction) {
+            TaskGridNavHelper.TaskNavDirection direction) {
         // Set next page if scroll animation is still running, otherwise cannot snap to the
         // next page on successive key presses. Setting the current page aborts the scroll.
         if (!mScroller.isFinished()) {
@@ -4557,7 +4603,7 @@
         return true;
     }
 
-    private int getNextPageInternal(int delta, @TaskGridNavHelper.TASK_NAV_DIRECTION int direction,
+    private int getNextPageInternal(int delta, TaskGridNavHelper.TaskNavDirection direction,
             boolean cycle) {
         if (!showAsGrid()) {
             return getNextPage() + delta;
@@ -4601,17 +4647,27 @@
     }
 
     @UiThread
-    private void dismissTask(int taskId) {
+    public void dismissTask(int taskId, boolean animate, boolean removeTask) {
         TaskView taskView = getTaskViewByTaskId(taskId);
         if (taskView == null) {
             Log.d(TAG, "dismissTask: " + taskId + ",  no associated TaskView");
             return;
         }
         Log.d(TAG, "dismissTask: " + taskId);
-        dismissTask(taskView, true /* animate */, false /* removeTask */);
+
+        if (enableDesktopExplodedView() && taskView instanceof  DesktopTaskView desktopTaskView) {
+            desktopTaskView.removeTaskFromExplodedView(taskId, animate);
+
+            if (removeTask) {
+                ActivityManagerWrapper.getInstance().removeTask(taskId);
+            }
+        } else {
+            dismissTaskView(taskView, animate, removeTask);
+        }
     }
 
-    public void dismissTask(TaskView taskView, boolean animateTaskView, boolean removeTask) {
+    /** Dismisses the entire [taskView]. */
+    public void dismissTaskView(TaskView taskView, boolean animateTaskView, boolean removeTask) {
         PendingAnimation pa = new PendingAnimation(DISMISS_TASK_DURATION);
         createTaskDismissAnimation(pa, taskView, animateTaskView, removeTask, DISMISS_TASK_DURATION,
                 false /* dismissingForSplitSelection*/);
@@ -4627,7 +4683,7 @@
     private void dismissCurrentTask() {
         TaskView taskView = getNextPageTaskView();
         if (taskView != null) {
-            dismissTask(taskView, true /*animateTaskView*/, true /*removeTask*/);
+            dismissTaskView(taskView, true /*animateTaskView*/, true /*removeTask*/);
         }
     }
 
@@ -4645,15 +4701,19 @@
         switch (event.getKeyCode()) {
             case KeyEvent.KEYCODE_TAB:
                 return snapToPageRelative(event.isShiftPressed() ? -1 : 1, true /* cycle */,
-                        DIRECTION_TAB);
+                        TaskGridNavHelper.TaskNavDirection.TAB);
             case KeyEvent.KEYCODE_DPAD_RIGHT:
-                return snapToPageRelative(mIsRtl ? -1 : 1, true /* cycle */, DIRECTION_RIGHT);
+                return snapToPageRelative(mIsRtl ? -1 : 1, true /* cycle */,
+                        TaskGridNavHelper.TaskNavDirection.RIGHT);
             case KeyEvent.KEYCODE_DPAD_LEFT:
-                return snapToPageRelative(mIsRtl ? 1 : -1, true /* cycle */, DIRECTION_LEFT);
+                return snapToPageRelative(mIsRtl ? 1 : -1, true /* cycle */,
+                        TaskGridNavHelper.TaskNavDirection.LEFT);
             case KeyEvent.KEYCODE_DPAD_UP:
-                return snapToPageRelative(1, false /* cycle */, DIRECTION_UP);
+                return snapToPageRelative(1, false /* cycle */,
+                        TaskGridNavHelper.TaskNavDirection.UP);
             case KeyEvent.KEYCODE_DPAD_DOWN:
-                return snapToPageRelative(1, false /* cycle */, DIRECTION_DOWN);
+                return snapToPageRelative(1, false /* cycle */,
+                        TaskGridNavHelper.TaskNavDirection.DOWN);
             case KeyEvent.KEYCODE_DEL:
             case KeyEvent.KEYCODE_FORWARD_DEL:
                 dismissCurrentTask();
@@ -4702,9 +4762,8 @@
         }
         mClearAllButton.setContentAlpha(mContentAlpha);
 
-        // TODO(b/389209338): Handle the visibility of the `mAddDesktopButton`.
         if (mAddDesktopButton != null) {
-            mAddDesktopButton.setAlpha(mContentAlpha);
+            mAddDesktopButton.setContentAlpha(mContentAlpha);
         }
         int alphaInt = Math.round(alpha * 255);
         mEmptyMessagePaint.setAlpha(alphaInt);
@@ -5656,7 +5715,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,
@@ -5678,6 +5737,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.
@@ -5786,10 +5857,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) {
@@ -6204,8 +6277,8 @@
         int addDesktopButtonIndex = indexOfChild(mAddDesktopButton);
         if (addDesktopButtonIndex != -1 && addDesktopButtonIndex < outPageScrolls.length) {
             outPageScrolls[addDesktopButtonIndex] =
-                    newPageScrolls[addDesktopButtonIndex] + Math.round(
-                            mAddDesktopButton.getGridTranslationX());
+                    newPageScrolls[addDesktopButtonIndex] + mAddDesktopButton.getScrollAdjustment(
+                            showAsGrid);
         }
 
         int lastTaskScroll = getLastTaskScroll(clearAllScroll, clearAllWidth);
@@ -6242,12 +6315,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()));
     }
 
@@ -6255,6 +6328,11 @@
         return mClearAllButton;
     }
 
+    @Nullable
+    public AddDesktopButton getAddDeskButton() {
+        return mAddDesktopButton;
+    }
+
     /**
      * @return How many pixels the running task is offset on the currently laid out dominant axis.
      */
@@ -6306,7 +6384,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
@@ -6834,6 +6912,11 @@
         }
     }
 
+    @Override
+    public void onCanCreateDesksChanged(boolean canCreateDesks) {
+        // TODO: b/389209338 - update the AddDesktopButton's visibility on this.
+    }
+
     /** Get the color used for foreground scrimming the RecentsView for sharing. */
     public static int getForegroundScrimDimColor(Context context) {
         return context.getColor(R.color.overview_foreground_scrim_color);
@@ -6944,10 +7027,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 d37a3f9..67318ac 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt
+++ b/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt
@@ -17,25 +17,17 @@
 package com.android.quickstep.views
 
 import android.graphics.Rect
+import android.util.FloatProperty
 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.touch.SingleAxisSwipeDetector
-import com.android.launcher3.util.DynamicResource
 import com.android.launcher3.util.IntArray
 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 java.util.function.BiConsumer
-import kotlin.math.abs
 
 /**
  * Helper class for [RecentsView]. This util class contains refactored and extracted functions from
@@ -82,7 +74,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 }
@@ -93,6 +85,9 @@
     /** Counts [TaskView]s that are large tiles. */
     fun getLargeTileCount(): Int = taskViews.count { it.isLargeTile }
 
+    /** Counts [TaskView]s that are grid tasks. */
+    fun getGridTaskCount(): Int = taskViews.count { it.isGridTask }
+
     /** Returns the first TaskView that should be displayed as a large tile. */
     fun getFirstLargeTaskView(): TaskView? =
         taskViews.firstOrNull {
@@ -100,7 +95,7 @@
         }
 
     /** Returns the expected focus task. */
-    fun getExpectedFocusedTask(): TaskView? =
+    fun getFirstNonDesktopTaskView(): TaskView? =
         if (enableLargeDesktopWindowingTile()) taskViews.firstOrNull { it !is DesktopTaskView }
         else taskViews.firstOrNull()
 
@@ -303,204 +298,24 @@
         }
     }
 
-    /**
-     * 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,
-            )
+    var deskExplodeProgress: Float = 0f
+        set(value) {
+            field = value
+            taskViews.filterIsInstance<DesktopTaskView>().forEach { it.explodeProgress = field }
         }
-        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 stifness and damping ratio.
-            if (startSettling) return@addUpdateListener
-            if (lastPosition < 0 && value >= 0) {
-                startSettling = true
-            }
-            lastPosition = value
-            if (startSettling) {
-                neighborsToSettle.setStartVelocity(velocity).animateToFinalPosition(0f)
-            }
-        }
-
-        // 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
-        getTasksAdjacentToDraggedTask(draggedTaskView, towardsStart = true).forEach {
-            previousNeighbor = createNeighboringTaskViewSpringAnimation(it, 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
-        getTasksAdjacentToDraggedTask(draggedTaskView, towardsStart = false).forEach {
-            previousNeighbor = createNeighboringTaskViewSpringAnimation(it, previousNeighbor)
-        }
-    }
-
-    /** Gets adjacent tasks either before or after the dragged task in visual order. */
-    private fun getTasksAdjacentToDraggedTask(
-        draggedTaskView: TaskView,
-        towardsStart: Boolean,
-    ): Sequence<TaskView> {
-        if (recentsView.showAsGrid()) {
-            return gridTaskViewInTabOrderSequence(draggedTaskView, towardsStart)
-        } else {
-            val taskViewList = taskViews.toList()
-            val draggedTaskViewIndex = taskViewList.indexOf(draggedTaskView)
-
-            return if (towardsStart) {
-                taskViewList.take(draggedTaskViewIndex).reversed().asSequence()
-            } else {
-                taskViewList.takeLast(taskViewList.size - draggedTaskViewIndex - 1).asSequence()
-            }
-        }
-    }
-
-    /**
-     * Returns a sequence of TaskViews in the grid, ordered according to tab navigation, starting
-     * from the dragged TaskView, in the direction of the provided delta.
-     *
-     * <p>A positive delta moves forward in the tab order towards the end of the grid, while a
-     * negative value moves backward towards the beginning.
-     */
-    private fun gridTaskViewInTabOrderSequence(
-        draggedTaskView: TaskView,
-        towardsStart: Boolean,
-    ): Sequence<TaskView> = sequence {
-        val taskGridNavHelper =
-            TaskGridNavHelper(
-                recentsView.topRowIdArray,
-                recentsView.bottomRowIdArray,
-                getLargeTaskViewIds(),
-                /* hasAddDesktopButton= */ false,
-            )
-        var nextTaskView: TaskView? = draggedTaskView
-        var previousTaskView: TaskView? = null
-        while (nextTaskView != previousTaskView && nextTaskView != null) {
-            previousTaskView = nextTaskView
-            nextTaskView =
-                recentsView.getTaskViewFromTaskViewId(
-                    taskGridNavHelper.getNextGridPage(
-                        nextTaskView.taskViewId,
-                        if (towardsStart) -1 else 1,
-                        TaskGridNavHelper.DIRECTION_TAB,
-                        /* cycle = */ false,
-                    )
-                )
-            if (nextTaskView != null && nextTaskView != previousTaskView) {
-                yield(nextTaskView)
-            }
-        }
-    }
-
-    /** Creates a neighboring task view spring, driven by the spring of its neighbor. */
-    private fun createNeighboringTaskViewSpringAnimation(
-        taskView: TaskView,
-        previousNeighborSpringAnimation: SpringAnimation,
-    ): SpringAnimation {
-        val neighboringTaskViewSpringAnimation =
-            SpringAnimation(
-                    taskView,
-                    FloatPropertyCompat.createFloatPropertyCompat(
-                        taskView.secondaryDismissTranslationProperty
-                    ),
-                )
-                .setSpring(createExpressiveDismissSpringForce())
-        // 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(): SpringForce {
-        val resourceProvider = DynamicResource.provider(recentsView.mContainer)
-        return SpringForce()
-            .setDampingRatio(
-                resourceProvider.getFloat(R.dimen.expressive_dismiss_task_trans_y_damping_ratio)
-            )
-            .setStiffness(
-                resourceProvider.getFloat(R.dimen.expressive_dismiss_task_trans_y_stiffness)
-            )
-    }
 
     companion object {
+        @JvmField
+        val DESK_EXPLODE_PROGRESS =
+            object : FloatProperty<RecentsView<*, *>>("deskExplodeProgress") {
+                override fun setValue(recentsView: RecentsView<*, *>, value: Float) {
+                    recentsView.mUtils.deskExplodeProgress = value
+                }
+
+                override fun get(recentsView: RecentsView<*, *>) =
+                    recentsView.mUtils.deskExplodeProgress
+            }
+
         val TEMP_RECT = Rect()
     }
 }
diff --git a/quickstep/src/com/android/quickstep/views/TaskContainer.kt b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
index bbe1af4..2b9d036 100644
--- a/quickstep/src/com/android/quickstep/views/TaskContainer.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
@@ -17,20 +17,18 @@
 package com.android.quickstep.views
 
 import android.graphics.Bitmap
+import android.graphics.Matrix
 import android.view.View
+import android.view.View.OnClickListener
 import com.android.launcher3.Flags.enableRefactorTaskThumbnail
 import com.android.launcher3.model.data.TaskViewItemInfo
 import com.android.launcher3.util.SplitConfigurationOptions
 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.di.inject
 import com.android.quickstep.recents.ui.mapper.TaskUiStateMapper
 import com.android.quickstep.recents.ui.viewmodel.TaskData
 import com.android.quickstep.task.thumbnail.TaskThumbnailView
-import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel
 import com.android.systemui.shared.recents.model.Task
 import com.android.systemui.shared.recents.model.ThumbnailData
 
@@ -55,27 +53,19 @@
 ) {
     val overlay: TaskOverlayFactory.TaskOverlay<*> = taskOverlayFactory.createOverlay(this)
 
-    // TODO(b/390581380): Remove this after this bug is fixed
-    private val taskThumbnailViewModel: TaskThumbnailViewModel by
-        RecentsDependencies.inject(snapshotView)
-
     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)
         }
     }
 
     internal var thumbnailData: ThumbnailData? = null
-    val splitAnimationThumbnail: Bitmap?
+        private set
+
+    val thumbnail: Bitmap?
+        /** If possible don't use this. It should be replaced as part of b/331753115. */
         get() =
             if (enableRefactorTaskThumbnail()) thumbnailData?.thumbnail
             else thumbnailViewDeprecated.thumbnail
@@ -106,9 +96,7 @@
 
     fun bind() {
         digitalWellBeingToast?.bind(task, taskView, snapshotView, stagePosition)
-        if (enableRefactorTaskThumbnail()) {
-            taskThumbnailViewModel.bind(task.key.id)
-        } else {
+        if (!enableRefactorTaskThumbnail()) {
             thumbnailViewDeprecated.bind(task, overlay, taskView)
         }
         overlay.init()
@@ -120,19 +108,14 @@
         snapshotView.scaleY = 1f
         overlay.destroy()
         if (enableRefactorTaskThumbnail()) {
-            RecentsDependencies.getInstance().removeScope(snapshotView)
-            RecentsDependencies.getInstance().removeScope(this)
             isThumbnailValid = false
+            thumbnailData = null
+            thumbnailView.onRecycle()
         } else {
             thumbnailViewDeprecated.setShowSplashForSplitSelection(false)
         }
     }
 
-    // TODO(b/391842220): Cancel scope in onDetach instead of having a specific method for this.
-    fun destroyScopes() {
-        thumbnailView.destroyScopes()
-    }
-
     fun setOverlayEnabled(enabled: Boolean) {
         if (!enableRefactorTaskThumbnail()) {
             thumbnailViewDeprecated.setOverlayEnabled(enabled)
@@ -147,9 +130,19 @@
         overlay.addChildForAccessibility(outChildren)
     }
 
-    fun setState(state: TaskData?, liveTile: Boolean, hasHeader: Boolean) {
+    fun setState(
+        state: TaskData?,
+        liveTile: Boolean,
+        hasHeader: Boolean,
+        clickCloseListener: OnClickListener?,
+    ) {
         thumbnailView.setState(
-            TaskUiStateMapper.toTaskThumbnailUiState(state, liveTile, hasHeader),
+            TaskUiStateMapper.toTaskThumbnailUiState(
+                state,
+                liveTile,
+                hasHeader,
+                clickCloseListener,
+            ),
             state?.taskId,
         )
         thumbnailData = if (state is TaskData.Data) state.thumbnailData else null
@@ -189,4 +182,8 @@
             thumbnailViewDeprecated.setSplashAlpha(progress)
         }
     }
+
+    fun updateThumbnailMatrix(matrix: Matrix) {
+        thumbnailView.setImageMatrix(matrix)
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/views/TaskMenuView.java b/quickstep/src/com/android/quickstep/views/TaskMenuView.java
deleted file mode 100644
index 0b3eb75..0000000
--- a/quickstep/src/com/android/quickstep/views/TaskMenuView.java
+++ /dev/null
@@ -1,429 +0,0 @@
-/*
- * Copyright (C) 2018 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.launcher3.Flags.enableOverviewIconMenu;
-import static com.android.launcher3.Flags.enableRefactorTaskThumbnail;
-import static com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE;
-import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
-import static com.android.quickstep.views.TaskThumbnailViewDeprecated.DIM_ALPHA;
-
-import android.animation.Animator;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.animation.ValueAnimator;
-import android.content.Context;
-import android.graphics.Outline;
-import android.graphics.Rect;
-import android.graphics.drawable.GradientDrawable;
-import android.graphics.drawable.ShapeDrawable;
-import android.graphics.drawable.shapes.RectShape;
-import android.util.AttributeSet;
-import android.view.Gravity;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewOutlineProvider;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import androidx.annotation.Nullable;
-
-import com.android.app.animation.Interpolators;
-import com.android.launcher3.AbstractFloatingView;
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.R;
-import com.android.launcher3.anim.AnimationSuccessListener;
-import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
-import com.android.launcher3.popup.SystemShortcut;
-import com.android.launcher3.views.BaseDragLayer;
-import com.android.quickstep.TaskOverlayFactory;
-import com.android.quickstep.TaskUtils;
-import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
-import com.android.quickstep.util.TaskCornerRadius;
-
-/**
- * Contains options for a recent task when long-pressing its icon.
- */
-public class TaskMenuView extends AbstractFloatingView {
-
-    private static final Rect sTempRect = new Rect();
-
-    private static final int REVEAL_OPEN_DURATION = enableOverviewIconMenu() ? 417 : 150;
-    private static final int REVEAL_CLOSE_DURATION = enableOverviewIconMenu() ? 333 : 100;
-
-    private RecentsViewContainer mContainer;
-    private TextView mTaskName;
-    @Nullable
-    private AnimatorSet mOpenCloseAnimator;
-    @Nullable
-    private ValueAnimator mRevealAnimator;
-    @Nullable private Runnable mOnClosingStartCallback;
-    private TaskView mTaskView;
-    private TaskContainer mTaskContainer;
-    private LinearLayout mOptionLayout;
-    private float mMenuTranslationYBeforeOpen;
-    private float mMenuTranslationXBeforeOpen;
-
-    public TaskMenuView(Context context, AttributeSet attrs) {
-        this(context, attrs, 0);
-    }
-
-    public TaskMenuView(Context context, AttributeSet attrs, int defStyleAttr) {
-        super(context, attrs, defStyleAttr);
-
-        mContainer = RecentsViewContainer.containerFromContext(context);
-        setClipToOutline(true);
-    }
-
-    @Override
-    protected void onFinishInflate() {
-        super.onFinishInflate();
-        mTaskName = findViewById(R.id.task_name);
-        mOptionLayout = findViewById(R.id.menu_option_layout);
-    }
-
-    @Override
-    public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
-        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
-            BaseDragLayer dl = mContainer.getDragLayer();
-            if (!dl.isEventOverView(this, ev)) {
-                // TODO: log this once we have a new container type for it?
-                close(true);
-                return true;
-            }
-        }
-        return false;
-    }
-
-    @Override
-    protected void handleClose(boolean animate) {
-        animateClose();
-    }
-
-    @Override
-    protected boolean isOfType(int type) {
-        return (type & TYPE_TASK_MENU) != 0;
-    }
-
-    @Override
-    public ViewOutlineProvider getOutlineProvider() {
-        return new ViewOutlineProvider() {
-            @Override
-            public void getOutline(View view, Outline outline) {
-                outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(),
-                        TaskCornerRadius.get(view.getContext()));
-            }
-        };
-    }
-
-    @Override
-    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
-        if (!(enableOverviewIconMenu()
-                && ((RecentsView) mContainer.getOverviewPanel()).isOnGridBottomRow(mTaskView))) {
-            // TODO(b/326952853): Cap menu height for grid bottom row in a way that doesn't break
-            // additionalTranslationY.
-            int maxMenuHeight = calculateMaxHeight();
-            if (MeasureSpec.getSize(heightMeasureSpec) > maxMenuHeight) {
-                heightMeasureSpec = MeasureSpec.makeMeasureSpec(maxMenuHeight, MeasureSpec.AT_MOST);
-            }
-        }
-        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
-    }
-
-    public void onRotationChanged() {
-        if (mOpenCloseAnimator != null && mOpenCloseAnimator.isRunning()) {
-            mOpenCloseAnimator.end();
-        }
-        if (mIsOpen) {
-            mOptionLayout.removeAllViews();
-            if (enableOverviewIconMenu() || !populateAndLayoutMenu()) {
-                close(false);
-            }
-        }
-    }
-
-    /**
-     * Show a task menu for the given taskContainer.
-     */
-    public static boolean showForTask(TaskContainer taskContainer,
-            @Nullable Runnable onClosingStartCallback) {
-        RecentsViewContainer container = RecentsViewContainer.containerFromContext(
-                taskContainer.getTaskView().getContext());
-        final TaskMenuView taskMenuView = (TaskMenuView) container.getLayoutInflater().inflate(
-                        R.layout.task_menu, container.getDragLayer(), false);
-        taskMenuView.setOnClosingStartCallback(onClosingStartCallback);
-        return taskMenuView.populateAndShowForTask(taskContainer);
-    }
-
-    /**
-     * Show a task menu for the given taskContainer.
-     */
-    public static boolean showForTask(TaskContainer taskContainer) {
-        return showForTask(taskContainer, null);
-    }
-
-    private boolean populateAndShowForTask(TaskContainer taskContainer) {
-        if (isAttachedToWindow()) {
-            return false;
-        }
-        mContainer.getDragLayer().addView(this);
-        mTaskView = taskContainer.getTaskView();
-        mTaskContainer = taskContainer;
-        if (!populateAndLayoutMenu()) {
-            return false;
-        }
-        post(this::animateOpen);
-        return true;
-    }
-
-    /** @return true if successfully able to populate task view menu, false otherwise */
-    private boolean populateAndLayoutMenu() {
-        addMenuOptions(mTaskContainer);
-        orientAroundTaskView(mTaskContainer);
-        return true;
-    }
-
-    private void addMenuOptions(TaskContainer taskContainer) {
-        if (enableOverviewIconMenu()) {
-            removeView(mTaskName);
-        } else {
-            mTaskName.setText(TaskUtils.getTitle(getContext(), taskContainer.getTask()));
-            mTaskName.setOnClickListener(v -> close(true));
-        }
-        TaskOverlayFactory.getEnabledShortcuts(mTaskView, taskContainer)
-                .forEach(this::addMenuOption);
-    }
-
-    private void addMenuOption(SystemShortcut menuOption) {
-        LinearLayout menuOptionView = (LinearLayout) mContainer.getLayoutInflater().inflate(
-                R.layout.task_view_menu_option, this, false);
-        if (enableOverviewIconMenu()) {
-            ((GradientDrawable) menuOptionView.getBackground()).setCornerRadius(0);
-        }
-        menuOption.setIconAndLabelFor(
-                menuOptionView.findViewById(R.id.icon), menuOptionView.findViewById(R.id.text));
-        LayoutParams lp = (LayoutParams) menuOptionView.getLayoutParams();
-        mTaskView.getPagedOrientationHandler().setLayoutParamsForTaskMenuOptionItem(lp,
-                menuOptionView, mContainer.getDeviceProfile());
-        // Set an onClick listener on each menu option. The onClick method is responsible for
-        // ending LiveTile mode on the thumbnail if needed.
-        menuOptionView.setOnClickListener(menuOption::onClick);
-        mOptionLayout.addView(menuOptionView);
-    }
-
-    private void orientAroundTaskView(TaskContainer taskContainer) {
-        RecentsView recentsView = mContainer.getOverviewPanel();
-        RecentsPagedOrientationHandler orientationHandler =
-                recentsView.getPagedOrientationHandler();
-        measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
-
-        // Get Position
-        DeviceProfile deviceProfile = mContainer.getDeviceProfile();
-        mContainer.getDragLayer().getDescendantRectRelativeToSelf(
-                enableOverviewIconMenu()
-                        ? getIconView().findViewById(R.id.icon_view_menu_anchor)
-                        : taskContainer.getSnapshotView(),
-                sTempRect);
-        Rect insets = mContainer.getDragLayer().getInsets();
-        BaseDragLayer.LayoutParams params = (BaseDragLayer.LayoutParams) getLayoutParams();
-        params.width = orientationHandler.getTaskMenuWidth(
-                taskContainer.getSnapshotView(), deviceProfile,
-                taskContainer.getStagePosition());
-        // Gravity set to Left instead of Start as sTempRect.left measures Left distance not Start
-        params.gravity = Gravity.LEFT;
-        setLayoutParams(params);
-        setScaleX(mTaskView.getScaleX());
-        setScaleY(mTaskView.getScaleY());
-
-        // Set divider spacing
-        ShapeDrawable divider = new ShapeDrawable(new RectShape());
-        divider.getPaint().setColor(getResources().getColor(android.R.color.transparent));
-        int dividerSpacing = (int) getResources().getDimension(R.dimen.task_menu_spacing);
-        mOptionLayout.setShowDividers(
-                enableOverviewIconMenu() ? SHOW_DIVIDER_NONE : SHOW_DIVIDER_MIDDLE);
-
-        orientationHandler.setTaskOptionsMenuLayoutOrientation(
-                deviceProfile, mOptionLayout, dividerSpacing, divider);
-        float thumbnailAlignedX = sTempRect.left - insets.left;
-        float thumbnailAlignedY = sTempRect.top - insets.top;
-
-        // Changing pivot to make computations easier
-        // NOTE: Changing the pivots means the rotated view gets rotated about the new pivots set,
-        // which would render the X and Y position set here incorrect
-        setPivotX(0);
-        setPivotY(0);
-        setRotation(orientationHandler.getDegreesRotated());
-
-        if (enableOverviewIconMenu()) {
-            setTranslationX(thumbnailAlignedX);
-            setTranslationY(thumbnailAlignedY);
-        } else {
-            // Margin that insets the menuView inside the taskView
-            float taskInsetMargin = getResources().getDimension(R.dimen.task_card_margin);
-            setTranslationX(orientationHandler.getTaskMenuX(thumbnailAlignedX,
-                    mTaskContainer.getSnapshotView(), deviceProfile, taskInsetMargin,
-                    getIconView()));
-            setTranslationY(orientationHandler.getTaskMenuY(
-                    thumbnailAlignedY, mTaskContainer.getSnapshotView(),
-                    mTaskContainer.getStagePosition(), this, taskInsetMargin,
-                    getIconView()));
-        }
-    }
-
-    private void animateOpen() {
-        mMenuTranslationYBeforeOpen = getTranslationY();
-        mMenuTranslationXBeforeOpen = getTranslationX();
-        animateOpenOrClosed(false);
-        mIsOpen = true;
-    }
-
-    private View getIconView() {
-        return mTaskContainer.getIconView().asView();
-    }
-
-    private void animateClose() {
-        animateOpenOrClosed(true);
-    }
-
-    private void animateOpenOrClosed(boolean closing) {
-        if (mOpenCloseAnimator != null && mOpenCloseAnimator.isRunning()) {
-            mOpenCloseAnimator.cancel();
-        }
-        mOpenCloseAnimator = new AnimatorSet();
-        // If we're opening, we just start from the beginning as a new `TaskMenuView` is created
-        // each time we do the open animation so there will never be a partial value here.
-        float revealAnimationStartProgress = 0f;
-        if (closing && mRevealAnimator != null) {
-            revealAnimationStartProgress = 1f - mRevealAnimator.getAnimatedFraction();
-        }
-        mRevealAnimator = createOpenCloseOutlineProvider()
-                .createRevealAnimator(this, closing, revealAnimationStartProgress);
-        mRevealAnimator.setInterpolator(enableOverviewIconMenu() ? Interpolators.EMPHASIZED
-                : Interpolators.DECELERATE);
-        AnimatorSet.Builder openCloseAnimatorBuilder = mOpenCloseAnimator.play(mRevealAnimator);
-        if (enableOverviewIconMenu()) {
-            IconAppChipView iconAppChip = (IconAppChipView) mTaskContainer.getIconView().asView();
-
-            float additionalTranslationY = 0;
-            if (((RecentsView) mContainer.getOverviewPanel()).isOnGridBottomRow(mTaskView)) {
-                // Animate menu up for enough room to display full menu when task on bottom row.
-                float menuBottom = getHeight() + mMenuTranslationYBeforeOpen;
-                float taskBottom = mTaskView.getHeight() + mTaskView.getPersistentTranslationY();
-                float taskbarTop = mContainer.getDeviceProfile().heightPx
-                        - mContainer.getDeviceProfile().getOverviewActionsClaimedSpaceBelow();
-                float midpoint = (taskBottom + taskbarTop) / 2f;
-                additionalTranslationY = -Math.max(menuBottom - midpoint, 0);
-            }
-            ObjectAnimator translationYAnim = ObjectAnimator.ofFloat(this, TRANSLATION_Y,
-                    closing ? mMenuTranslationYBeforeOpen
-                            : mMenuTranslationYBeforeOpen + additionalTranslationY);
-            translationYAnim.setInterpolator(EMPHASIZED);
-            openCloseAnimatorBuilder.with(translationYAnim);
-
-            ObjectAnimator menuTranslationYAnim = ObjectAnimator.ofFloat(
-                    iconAppChip.getMenuTranslationY(),
-                    MULTI_PROPERTY_VALUE, closing ? 0 : additionalTranslationY);
-            menuTranslationYAnim.setInterpolator(EMPHASIZED);
-            openCloseAnimatorBuilder.with(menuTranslationYAnim);
-
-            float additionalTranslationX = 0;
-            if (mContainer.getDeviceProfile().isLandscape
-                    && mTaskContainer.getStagePosition() == STAGE_POSITION_BOTTOM_OR_RIGHT) {
-                // Animate menu and icon when split task would display off the side of the screen.
-                additionalTranslationX = Math.max(
-                        getTranslationX() + getWidth() - (mContainer.getDeviceProfile().widthPx
-                                - getResources().getDimensionPixelSize(
-                                R.dimen.task_menu_edge_padding) * 2), 0);
-            }
-
-            ObjectAnimator translationXAnim = ObjectAnimator.ofFloat(this, TRANSLATION_X,
-                    closing ? mMenuTranslationXBeforeOpen
-                            : mMenuTranslationXBeforeOpen - additionalTranslationX);
-            translationXAnim.setInterpolator(EMPHASIZED);
-            openCloseAnimatorBuilder.with(translationXAnim);
-
-            ObjectAnimator menuTranslationXAnim = ObjectAnimator.ofFloat(
-                    iconAppChip.getMenuTranslationX(),
-                    MULTI_PROPERTY_VALUE, closing ? 0 : -additionalTranslationX);
-            menuTranslationXAnim.setInterpolator(EMPHASIZED);
-            openCloseAnimatorBuilder.with(menuTranslationXAnim);
-        }
-        openCloseAnimatorBuilder.with(ObjectAnimator.ofFloat(this, ALPHA, closing ? 0 : 1));
-        if (enableRefactorTaskThumbnail()) {
-            mRevealAnimator.addUpdateListener(animation -> {
-                float animatedFraction = animation.getAnimatedFraction();
-                float openProgress = closing ? (1 - animatedFraction) : animatedFraction;
-                mTaskContainer.updateMenuOpenProgress(openProgress);
-            });
-        } else {
-            openCloseAnimatorBuilder.with(ObjectAnimator.ofFloat(
-                    mTaskContainer.getThumbnailViewDeprecated(), DIM_ALPHA,
-                    closing ? 0 : TaskView.MAX_PAGE_SCRIM_ALPHA));
-        }
-        mOpenCloseAnimator.addListener(new AnimationSuccessListener() {
-            @Override
-            public void onAnimationStart(Animator animation) {
-                setVisibility(VISIBLE);
-                if (closing && mOnClosingStartCallback != null) {
-                    mOnClosingStartCallback.run();
-                }
-            }
-
-            @Override
-            public void onAnimationSuccess(Animator animator) {
-                if (closing) {
-                    closeComplete();
-                }
-            }
-        });
-        mOpenCloseAnimator.setDuration(closing ? REVEAL_CLOSE_DURATION: REVEAL_OPEN_DURATION);
-        mOpenCloseAnimator.start();
-    }
-
-    private void closeComplete() {
-        mIsOpen = false;
-        mContainer.getDragLayer().removeView(this);
-        mRevealAnimator = null;
-    }
-
-    private RoundedRectRevealOutlineProvider createOpenCloseOutlineProvider() {
-        float radius = TaskCornerRadius.get(mContext);
-        Rect fromRect = new Rect(
-                enableOverviewIconMenu() && isLayoutRtl() ? getWidth() : 0,
-                0,
-                enableOverviewIconMenu() && !isLayoutRtl() ? 0 : getWidth(),
-                0);
-        Rect toRect = new Rect(0, 0, getWidth(), getHeight());
-        return new RoundedRectRevealOutlineProvider(radius, radius, fromRect, toRect);
-    }
-
-    /**
-     * Calculates max height based on how much space we have available.
-     * If not enough space then the view will scroll. The maximum menu size will sit inside the task
-     * with a margin on the top and bottom.
-     */
-    private int calculateMaxHeight() {
-        float taskInsetMargin = getResources().getDimension(R.dimen.task_card_margin);
-        return mTaskView.getPagedOrientationHandler().getTaskMenuHeight(taskInsetMargin,
-                mContainer.getDeviceProfile(), getTranslationX(), getTranslationY());
-    }
-
-    private void setOnClosingStartCallback(Runnable onClosingStartCallback) {
-        mOnClosingStartCallback = onClosingStartCallback;
-    }
-}
diff --git a/quickstep/src/com/android/quickstep/views/TaskMenuView.kt b/quickstep/src/com/android/quickstep/views/TaskMenuView.kt
new file mode 100644
index 0000000..95336cf
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/TaskMenuView.kt
@@ -0,0 +1,455 @@
+/*
+ * Copyright (C) 2018 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.Animator
+import android.animation.AnimatorSet
+import android.animation.ObjectAnimator
+import android.animation.ValueAnimator
+import android.content.Context
+import android.graphics.Outline
+import android.graphics.Rect
+import android.graphics.drawable.GradientDrawable
+import android.graphics.drawable.ShapeDrawable
+import android.graphics.drawable.shapes.RectShape
+import android.util.AttributeSet
+import android.view.Gravity
+import android.view.MotionEvent
+import android.view.View
+import android.view.ViewOutlineProvider
+import android.widget.LinearLayout
+import android.widget.TextView
+import com.android.app.animation.Interpolators
+import com.android.launcher3.AbstractFloatingView
+import com.android.launcher3.Flags.enableOverviewIconMenu
+import com.android.launcher3.Flags.enableRefactorTaskThumbnail
+import com.android.launcher3.R
+import com.android.launcher3.anim.AnimationSuccessListener
+import com.android.launcher3.anim.RoundedRectRevealOutlineProvider
+import com.android.launcher3.popup.SystemShortcut
+import com.android.launcher3.util.MultiPropertyFactory
+import com.android.launcher3.util.SplitConfigurationOptions
+import com.android.launcher3.views.BaseDragLayer
+import com.android.quickstep.TaskOverlayFactory
+import com.android.quickstep.TaskUtils
+import com.android.quickstep.util.TaskCornerRadius
+import java.util.function.Consumer
+import kotlin.math.max
+
+/** Contains options for a recent task when long-pressing its icon. */
+class TaskMenuView
+@JvmOverloads
+constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int = 0) :
+    AbstractFloatingView(context, attrs, defStyleAttr) {
+    private val recentsViewContainer: RecentsViewContainer =
+        RecentsViewContainer.containerFromContext(context)
+    private val tempRect = Rect()
+    private val taskName: TextView by lazy { findViewById(R.id.task_name) }
+    private val optionLayout: LinearLayout by lazy { findViewById(R.id.menu_option_layout) }
+    private var openCloseAnimator: AnimatorSet? = null
+    private var revealAnimator: ValueAnimator? = null
+    private var onClosingStartCallback: Runnable? = null
+    private lateinit var taskView: TaskView
+    private lateinit var taskContainer: TaskContainer
+    private var menuTranslationXBeforeOpen = 0f
+    private var menuTranslationYBeforeOpen = 0f
+
+    init {
+        clipToOutline = true
+    }
+
+    override fun onControllerInterceptTouchEvent(ev: MotionEvent): Boolean {
+        if (ev.action == MotionEvent.ACTION_DOWN) {
+            if (!recentsViewContainer.dragLayer.isEventOverView(this, ev)) {
+                // TODO: log this once we have a new container type for it?
+                close(true)
+                return true
+            }
+        }
+        return false
+    }
+
+    override fun handleClose(animate: Boolean) {
+        animateClose()
+    }
+
+    override fun isOfType(type: Int): Boolean = (type and TYPE_TASK_MENU) != 0
+
+    override fun getOutlineProvider(): ViewOutlineProvider =
+        object : ViewOutlineProvider() {
+            override fun getOutline(view: View, outline: Outline) {
+                outline.setRoundRect(
+                    0,
+                    0,
+                    view.width,
+                    view.height,
+                    TaskCornerRadius.get(view.context),
+                )
+            }
+        }
+
+    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+        var heightMeasure = heightMeasureSpec
+        if (!(enableOverviewIconMenu() && taskView.isOnGridBottomRow())) {
+            // TODO(b/326952853): Cap menu height for grid bottom row in a way that doesn't break
+            // additionalTranslationY.
+            val maxMenuHeight = calculateMaxHeight()
+            if (MeasureSpec.getSize(heightMeasure) > maxMenuHeight) {
+                heightMeasure = MeasureSpec.makeMeasureSpec(maxMenuHeight, MeasureSpec.AT_MOST)
+            }
+        }
+        super.onMeasure(widthMeasureSpec, heightMeasure)
+    }
+
+    fun onRotationChanged() {
+        openCloseAnimator?.let { if (it.isRunning) it.end() }
+        if (mIsOpen) {
+            optionLayout.removeAllViews()
+            if (enableOverviewIconMenu() || !populateAndLayoutMenu()) {
+                close(false)
+            }
+        }
+    }
+
+    private fun populateAndShowForTask(taskContainer: TaskContainer): Boolean {
+        if (isAttachedToWindow) return false
+        recentsViewContainer.dragLayer.addView(this)
+        taskView = taskContainer.taskView
+        this.taskContainer = taskContainer
+        if (!populateAndLayoutMenu()) return false
+        post { this.animateOpen() }
+        return true
+    }
+
+    /** @return true if successfully able to populate task view menu, false otherwise */
+    private fun populateAndLayoutMenu(): Boolean {
+        addMenuOptions(taskContainer)
+        orientAroundTaskView(taskContainer)
+        return true
+    }
+
+    private fun addMenuOptions(taskContainer: TaskContainer) {
+        if (enableOverviewIconMenu()) {
+            removeView(taskName)
+        } else {
+            taskName.text = TaskUtils.getTitle(context, taskContainer.task)
+            taskName.setOnClickListener { close(true) }
+        }
+        TaskOverlayFactory.getEnabledShortcuts(taskView, taskContainer)
+            .forEach(Consumer { menuOption: SystemShortcut<*> -> this.addMenuOption(menuOption) })
+    }
+
+    private fun addMenuOption(menuOption: SystemShortcut<*>) {
+        val menuOptionView =
+            recentsViewContainer.layoutInflater.inflate(R.layout.task_view_menu_option, this, false)
+                as LinearLayout
+        if (enableOverviewIconMenu()) {
+            (menuOptionView.background as GradientDrawable).cornerRadius = 0f
+        }
+        menuOption.setIconAndLabelFor(
+            menuOptionView.findViewById(R.id.icon),
+            menuOptionView.findViewById(R.id.text),
+        )
+        val lp = menuOptionView.layoutParams as LayoutParams
+        taskView.pagedOrientationHandler.setLayoutParamsForTaskMenuOptionItem(
+            lp,
+            menuOptionView,
+            recentsViewContainer.deviceProfile,
+        )
+        // Set an onClick listener on each menu option. The onClick method is responsible for
+        // ending LiveTile mode on the thumbnail if needed.
+        menuOptionView.setOnClickListener { v: View? -> menuOption.onClick(v) }
+        optionLayout.addView(menuOptionView)
+    }
+
+    private fun orientAroundTaskView(taskContainer: TaskContainer) {
+        val recentsView = recentsViewContainer.getOverviewPanel<RecentsView<*, *>>()
+        val orientationHandler = recentsView.pagedOrientationHandler
+        measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED)
+
+        // Get Position
+        val deviceProfile = recentsViewContainer.deviceProfile
+        recentsViewContainer.dragLayer.getDescendantRectRelativeToSelf(
+            if (enableOverviewIconMenu()) iconView.findViewById(R.id.icon_view_menu_anchor)
+            else taskContainer.snapshotView,
+            tempRect,
+        )
+        val insets = recentsViewContainer.dragLayer.getInsets()
+        val params = layoutParams as BaseDragLayer.LayoutParams
+        params.width =
+            orientationHandler.getTaskMenuWidth(
+                taskContainer.snapshotView,
+                deviceProfile,
+                taskContainer.stagePosition,
+            )
+        // Gravity set to Left instead of Start as sTempRect.left measures Left distance not Start
+        params.gravity = Gravity.START
+        layoutParams = params
+        scaleX = taskView.scaleX
+        scaleY = taskView.scaleY
+
+        // Set divider spacing
+        val divider = ShapeDrawable(RectShape())
+        divider.paint.color = resources.getColor(android.R.color.transparent)
+        val dividerSpacing = resources.getDimension(R.dimen.task_menu_spacing).toInt()
+        optionLayout.showDividers =
+            if (enableOverviewIconMenu()) SHOW_DIVIDER_NONE else SHOW_DIVIDER_MIDDLE
+
+        orientationHandler.setTaskOptionsMenuLayoutOrientation(
+            deviceProfile,
+            optionLayout,
+            dividerSpacing,
+            divider,
+        )
+        val thumbnailAlignedX = (tempRect.left - insets.left).toFloat()
+        val thumbnailAlignedY = (tempRect.top - insets.top).toFloat()
+
+        // Changing pivot to make computations easier
+        // NOTE: Changing the pivots means the rotated view gets rotated about the new pivots set,
+        // which would render the X and Y position set here incorrect
+        pivotX = 0f
+        pivotY = 0f
+        rotation = orientationHandler.degreesRotated
+
+        if (enableOverviewIconMenu()) {
+            translationX = thumbnailAlignedX
+            translationY = thumbnailAlignedY
+        } else {
+            // Margin that insets the menuView inside the taskView
+            val taskInsetMargin = resources.getDimension(R.dimen.task_card_margin)
+            translationX =
+                orientationHandler.getTaskMenuX(
+                    thumbnailAlignedX,
+                    this.taskContainer.snapshotView,
+                    deviceProfile,
+                    taskInsetMargin,
+                    iconView,
+                )
+            translationY =
+                orientationHandler.getTaskMenuY(
+                    thumbnailAlignedY,
+                    this.taskContainer.snapshotView,
+                    this.taskContainer.stagePosition,
+                    this,
+                    taskInsetMargin,
+                    iconView,
+                )
+        }
+    }
+
+    private fun animateOpen() {
+        menuTranslationYBeforeOpen = translationY
+        menuTranslationXBeforeOpen = translationX
+        animateOpenOrClosed(false)
+        mIsOpen = true
+    }
+
+    private val iconView: View
+        get() = taskContainer.iconView.asView()
+
+    private fun animateClose() {
+        animateOpenOrClosed(true)
+    }
+
+    private fun animateOpenOrClosed(closing: Boolean) {
+        openCloseAnimator?.let { if (it.isRunning) it.cancel() }
+        openCloseAnimator = AnimatorSet()
+        // If we're opening, we just start from the beginning as a new `TaskMenuView` is created
+        // each time we do the open animation so there will never be a partial value here.
+        var revealAnimationStartProgress = 0f
+        if (closing && revealAnimator != null) {
+            revealAnimationStartProgress = 1f - revealAnimator!!.animatedFraction
+        }
+        revealAnimator =
+            createOpenCloseOutlineProvider()
+                .createRevealAnimator(this, closing, revealAnimationStartProgress)
+        revealAnimator!!.interpolator =
+            if (enableOverviewIconMenu()) Interpolators.EMPHASIZED else Interpolators.DECELERATE
+        val openCloseAnimatorBuilder = openCloseAnimator!!.play(revealAnimator)
+        if (enableOverviewIconMenu()) {
+            animateOpenOrCloseAppChip(closing, openCloseAnimatorBuilder)
+        }
+        openCloseAnimatorBuilder.with(
+            ObjectAnimator.ofFloat(this, ALPHA, (if (closing) 0 else 1).toFloat())
+        )
+        if (enableRefactorTaskThumbnail()) {
+            revealAnimator?.addUpdateListener { animation: ValueAnimator ->
+                val animatedFraction = animation.animatedFraction
+                val openProgress = if (closing) (1 - animatedFraction) else animatedFraction
+                taskContainer.updateMenuOpenProgress(openProgress)
+            }
+        } else {
+            openCloseAnimatorBuilder.with(
+                ObjectAnimator.ofFloat(
+                    taskContainer.thumbnailViewDeprecated,
+                    TaskThumbnailViewDeprecated.DIM_ALPHA,
+                    if (closing) 0f else TaskView.MAX_PAGE_SCRIM_ALPHA,
+                )
+            )
+        }
+        openCloseAnimator!!.addListener(
+            object : AnimationSuccessListener() {
+                override fun onAnimationStart(animation: Animator) {
+                    visibility = VISIBLE
+                    if (closing) onClosingStartCallback?.run()
+                }
+
+                override fun onAnimationSuccess(animator: Animator) {
+                    if (closing) closeComplete()
+                }
+            }
+        )
+        val animationDuration = if (closing) REVEAL_CLOSE_DURATION else REVEAL_OPEN_DURATION
+        openCloseAnimator!!.setDuration(animationDuration)
+        openCloseAnimator!!.start()
+    }
+
+    private fun TaskView.isOnGridBottomRow(): Boolean =
+        (recentsViewContainer.getOverviewPanel<View>() as RecentsView<*, *>).isOnGridBottomRow(this)
+
+    private fun closeComplete() {
+        mIsOpen = false
+        recentsViewContainer.dragLayer.removeView(this)
+        revealAnimator = null
+    }
+
+    private fun createOpenCloseOutlineProvider(): RoundedRectRevealOutlineProvider {
+        val radius = TaskCornerRadius.get(mContext)
+        val fromRect =
+            Rect(
+                if (enableOverviewIconMenu() && isLayoutRtl) width else 0,
+                0,
+                if (enableOverviewIconMenu() && !isLayoutRtl) 0 else width,
+                0,
+            )
+        val toRect = Rect(0, 0, width, height)
+        return RoundedRectRevealOutlineProvider(radius, radius, fromRect, toRect)
+    }
+
+    /**
+     * Calculates max height based on how much space we have available. If not enough space then the
+     * view will scroll. The maximum menu size will sit inside the task with a margin on the top and
+     * bottom.
+     */
+    private fun calculateMaxHeight(): Int {
+        val taskInsetMargin = resources.getDimension(R.dimen.task_card_margin)
+        return taskView.pagedOrientationHandler.getTaskMenuHeight(
+            taskInsetMargin,
+            recentsViewContainer.deviceProfile,
+            translationX,
+            translationY,
+        )
+    }
+
+    private fun setOnClosingStartCallback(onClosingStartCallback: Runnable?) {
+        this.onClosingStartCallback = onClosingStartCallback
+    }
+
+    private fun animateOpenOrCloseAppChip(closing: Boolean, animatorBuilder: AnimatorSet.Builder) {
+        val iconAppChip = taskContainer.iconView.asView() as IconAppChipView
+
+        var additionalTranslationY = 0f
+        if (taskView.isOnGridBottomRow()) {
+            // Animate menu up for enough room to display full menu when task on bottom row.
+            val menuBottom = height + menuTranslationYBeforeOpen
+            val taskBottom = taskView.height + taskView.persistentTranslationY
+            val taskbarTop =
+                (recentsViewContainer.deviceProfile.heightPx -
+                        recentsViewContainer.deviceProfile.overviewActionsClaimedSpaceBelow)
+                    .toFloat()
+            val midpoint = (taskBottom + taskbarTop) / 2f
+            additionalTranslationY = (-max((menuBottom - midpoint).toDouble(), 0.0)).toFloat()
+        }
+        val translationYAnim =
+            ObjectAnimator.ofFloat(
+                this,
+                TRANSLATION_Y,
+                if (closing) menuTranslationYBeforeOpen
+                else menuTranslationYBeforeOpen + additionalTranslationY,
+            )
+        translationYAnim.interpolator = Interpolators.EMPHASIZED
+        animatorBuilder.with(translationYAnim)
+
+        val menuTranslationYAnim: ObjectAnimator =
+            ObjectAnimator.ofFloat(
+                iconAppChip.getMenuTranslationY(),
+                MultiPropertyFactory.MULTI_PROPERTY_VALUE,
+                if (closing) 0f else additionalTranslationY,
+            )
+        menuTranslationYAnim.interpolator = Interpolators.EMPHASIZED
+        animatorBuilder.with(menuTranslationYAnim)
+
+        var additionalTranslationX = 0f
+        if (
+            recentsViewContainer.deviceProfile.isLandscape &&
+                taskContainer.stagePosition ==
+                    SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT
+        ) {
+            // Animate menu and icon when split task would display off the side of the screen.
+            additionalTranslationX =
+                max(
+                        (translationX + width -
+                                (recentsViewContainer.deviceProfile.widthPx -
+                                    resources.getDimensionPixelSize(
+                                        R.dimen.task_menu_edge_padding
+                                    ) * 2))
+                            .toDouble(),
+                        0.0,
+                    )
+                    .toFloat()
+        }
+
+        val translationXAnim =
+            ObjectAnimator.ofFloat(
+                this,
+                TRANSLATION_X,
+                if (closing) menuTranslationXBeforeOpen
+                else menuTranslationXBeforeOpen - additionalTranslationX,
+            )
+        translationXAnim.interpolator = Interpolators.EMPHASIZED
+        animatorBuilder.with(translationXAnim)
+
+        val menuTranslationXAnim: ObjectAnimator =
+            ObjectAnimator.ofFloat(
+                iconAppChip.getMenuTranslationX(),
+                MultiPropertyFactory.MULTI_PROPERTY_VALUE,
+                if (closing) 0f else -additionalTranslationX,
+            )
+        menuTranslationXAnim.interpolator = Interpolators.EMPHASIZED
+        animatorBuilder.with(menuTranslationXAnim)
+    }
+
+    companion object {
+        private val REVEAL_OPEN_DURATION = if (enableOverviewIconMenu()) 417L else 150L
+        private val REVEAL_CLOSE_DURATION = if (enableOverviewIconMenu()) 333L else 100L
+
+        /** Show a task menu for the given taskContainer. */
+        /** Show a task menu for the given taskContainer. */
+        @JvmOverloads
+        fun showForTask(
+            taskContainer: TaskContainer,
+            onClosingStartCallback: Runnable? = null,
+        ): Boolean {
+            val container: RecentsViewContainer =
+                RecentsViewContainer.containerFromContext(taskContainer.taskView.context)
+            val taskMenuView =
+                container.layoutInflater.inflate(R.layout.task_menu, container.dragLayer, false)
+                    as TaskMenuView
+            taskMenuView.setOnClosingStartCallback(onClosingStartCallback)
+            return taskMenuView.populateAndShowForTask(taskContainer)
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/views/TaskThumbnailViewHeader.kt b/quickstep/src/com/android/quickstep/views/TaskThumbnailViewHeader.kt
index 9eb294a..9a8805b 100644
--- a/quickstep/src/com/android/quickstep/views/TaskThumbnailViewHeader.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskThumbnailViewHeader.kt
@@ -19,6 +19,7 @@
 import android.content.Context
 import android.util.AttributeSet
 import android.widget.FrameLayout
+import android.widget.ImageButton
 import android.widget.ImageView
 import android.widget.TextView
 import com.android.launcher3.R
@@ -30,9 +31,11 @@
 
     private val headerTitleView: TextView by lazy { findViewById(R.id.header_app_title) }
     private val headerIconView: ImageView by lazy { findViewById(R.id.header_app_icon) }
+    private val headerCloseButton: ImageButton by lazy { findViewById(R.id.header_close_button) }
 
     fun setHeader(header: ThumbnailHeader) {
         headerTitleView.setText(header.title)
         headerIconView.setImageDrawable(header.icon)
+        headerCloseButton.setOnClickListener(header.clickCloseListener)
     }
 }
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index 609262f..27db6d6 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -41,10 +41,10 @@
 import android.widget.Toast
 import androidx.annotation.IntDef
 import androidx.annotation.VisibleForTesting
-import androidx.core.view.doOnLayout
 import androidx.core.view.updateLayoutParams
 import com.android.app.animation.Interpolators
 import com.android.launcher3.Flags.enableCursorHoverStates
+import com.android.launcher3.Flags.enableDesktopExplodedView
 import com.android.launcher3.Flags.enableGridOnlyOverview
 import com.android.launcher3.Flags.enableHoverOfChildElementsInTaskview
 import com.android.launcher3.Flags.enableLargeDesktopWindowingTile
@@ -199,7 +199,7 @@
          */
         get() = (getNonGridTrans(nonGridTranslationX) + getGridTrans(this.gridTranslationX))
 
-    protected val persistentTranslationY: Float
+    val persistentTranslationY: Float
         /**
          * Returns addition of translationY that is persistent (e.g. fullscreen and grid), and does
          * not change according to a temporary state (e.g. task offset).
@@ -671,11 +671,6 @@
         taskContainers.forEach { it.destroy() }
     }
 
-    fun destroyScopes() {
-        // TODO(b/391842220): Cancel scope in onDetach instead of having a specific method for this.
-        taskContainers.forEach { it.destroyScopes() }
-    }
-
     // TODO: Clip-out the icon region from the thumbnail, since they are overlapping.
     override fun hasOverlappingRendering() = false
 
@@ -774,13 +769,34 @@
         // Updating containers
         val mapOfTasks = state.tasks.associateBy { it.taskId }
         taskContainers.forEach { container ->
-            val containerState = mapOfTasks[container.task.key.id]
+            val taskId = container.task.key.id
+            val containerState = mapOfTasks[taskId]
+            val shouldHaveHeader = (type == TaskViewType.DESKTOP) && enableDesktopExplodedView()
             container.setState(
                 state = containerState,
                 liveTile = state.isLiveTile,
-                hasHeader = type == TaskViewType.DESKTOP,
+                hasHeader = shouldHaveHeader,
+                clickCloseListener =
+                    if (shouldHaveHeader) {
+                        {
+                            // Update the layout UI to remove this task from the layout grid, and
+                            // remove the task from ActivityManager afterwards.
+                            recentsView?.dismissTask(
+                                taskId,
+                                /* animate= */ true,
+                                /* removeTask= */ true,
+                            )
+                        }
+                    } else {
+                        null
+                    },
             )
             updateThumbnailValidity(container)
+            updateThumbnailMatrix(
+                container = container,
+                width = container.thumbnailView.width,
+                height = container.thumbnailView.height,
+            )
 
             if (enableOverviewIconMenu()) {
                 setIconState(container, containerState)
@@ -790,14 +806,32 @@
 
     private fun updateThumbnailValidity(container: TaskContainer) {
         container.isThumbnailValid =
-            viewModel!!.isThumbnailValid(
+            viewModel?.isThumbnailValid(
                 thumbnail = container.thumbnailData,
                 width = container.thumbnailView.width,
                 height = container.thumbnailView.height,
-            )
+            ) ?: return
         applyThumbnailSplashAlpha()
     }
 
+    /**
+     * Updates the thumbnail's transformation matrix and rotation state within a TaskContainer.
+     *
+     * This function is called to reposition the thumbnail in the following scenarios:
+     * - When the TTV's size changes (onSizeChanged), and it's displaying a SnapshotSplash.
+     * - When drawing a snapshot (drawSnapshot).
+     *
+     * @param container The TaskContainer holding the thumbnail to be updated.
+     * @param width The desired width of the thumbnail's container.
+     * @param height The desired height of the thumbnail's container.
+     */
+    private fun updateThumbnailMatrix(container: TaskContainer, width: Int, height: Int) {
+        val thumbnailPosition =
+            viewModel?.getThumbnailPosition(container.thumbnailData, width, height, isLayoutRtl)
+                ?: return
+        container.updateThumbnailMatrix(thumbnailPosition.matrix)
+    }
+
     override fun onDetachedFromWindow() {
         super.onDetachedFromWindow()
         if (enableRefactorTaskThumbnail()) {
@@ -843,6 +877,7 @@
                         getTaskUseCase = RecentsDependencies.get(),
                         getSysUiStatusNavFlagsUseCase = RecentsDependencies.get(),
                         isThumbnailValidUseCase = RecentsDependencies.get(),
+                        getThumbnailPositionUseCase = RecentsDependencies.get(),
                         dispatcherProvider = RecentsDependencies.get(),
                     )
                     .apply { bind(*taskIds) }
@@ -852,7 +887,10 @@
             container.bind()
             if (enableRefactorTaskThumbnail()) {
                 container.thumbnailView.cornerRadius = thumbnailFullscreenParams.currentCornerRadius
-                container.thumbnailView.doOnLayout { updateThumbnailValidity(container) }
+                container.thumbnailView.doOnSizeChange { width, height ->
+                    updateThumbnailValidity(container)
+                    updateThumbnailMatrix(container, width, height)
+                }
             }
         }
         setOrientationState(orientedState)
@@ -1220,6 +1258,7 @@
                 recentsView.stateManager,
                 recentsView,
                 recentsView.depthController,
+                /* transitionInfo= */ null,
             )
             addListener(
                 object : AnimatorListenerAdapter() {
@@ -1396,7 +1435,7 @@
             container.task,
             container.iconView.drawable,
             container.snapshotView,
-            container.splitAnimationThumbnail,
+            container.thumbnail,
             /* intent */ null,
             /* user */ null,
             container.itemInfo,
diff --git a/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/FakeTaskThumbnailViewModel.kt b/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/FakeTaskThumbnailViewModel.kt
deleted file mode 100644
index 1a2b1c3..0000000
--- a/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/FakeTaskThumbnailViewModel.kt
+++ /dev/null
@@ -1,29 +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.task.thumbnail
-
-import android.graphics.Matrix
-import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel
-
-class FakeTaskThumbnailViewModel : TaskThumbnailViewModel {
-    override fun bind(taskId: Int) {
-        // no-op
-    }
-
-    override fun getThumbnailPositionState(width: Int, height: Int, isRtl: Boolean) =
-        Matrix.IDENTITY_MATRIX
-}
diff --git a/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewScreenshotTest.kt b/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewScreenshotTest.kt
index 232a08a..80b2c16 100644
--- a/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewScreenshotTest.kt
+++ b/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewScreenshotTest.kt
@@ -16,13 +16,20 @@
 package com.android.quickstep.task.thumbnail
 
 import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.Canvas
 import android.graphics.Color
+import android.graphics.Matrix
+import android.graphics.Paint
+import android.graphics.drawable.BitmapDrawable
 import android.view.LayoutInflater
+import android.view.Surface.ROTATION_0
+import androidx.core.graphics.set
 import com.android.launcher3.R
-import com.android.quickstep.recents.di.RecentsDependencies
 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.BackgroundOnly
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.SnapshotSplash
 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
-import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel
 import com.google.android.apps.nexuslauncher.imagecomparison.goldenpathmanager.ViewScreenshotGoldenPathManager
 import org.junit.Rule
 import org.junit.Test
@@ -45,8 +52,6 @@
             ViewScreenshotGoldenPathManager(getEmulatedDevicePathConfig(emulationSpec)),
         )
 
-    private val taskThumbnailViewModel = FakeTaskThumbnailViewModel()
-
     @Test
     fun taskThumbnailView_uninitializedByDefault() {
         screenshotRule.screenshotTest("taskThumbnailView_uninitialized") { activity ->
@@ -85,6 +90,96 @@
     }
 
     @Test
+    fun taskThumbnailView_liveTile_withoutHeader() {
+        screenshotRule.screenshotTest("taskThumbnailView_liveTile") { activity ->
+            activity.actionBar?.hide()
+            createTaskThumbnailView(activity).apply {
+                setState(TaskThumbnailUiState.LiveTile.WithoutHeader)
+            }
+        }
+    }
+
+    @Test
+    fun taskThumbnailView_image_withoutHeader() {
+        screenshotRule.screenshotTest("taskThumbnailView_image") { activity ->
+            activity.actionBar?.hide()
+            createTaskThumbnailView(activity).apply {
+                setState(
+                    SnapshotSplash(
+                        Snapshot.WithoutHeader(createBitmap(), ROTATION_0, Color.DKGRAY),
+                        null,
+                    )
+                )
+            }
+        }
+    }
+
+    @Test
+    fun taskThumbnailView_image_withoutHeader_withImageMatrix() {
+        screenshotRule.screenshotTest("taskThumbnailView_image_withMatrix") { activity ->
+            activity.actionBar?.hide()
+            createTaskThumbnailView(activity).apply {
+                val lessThanHeightMatchingAspectRatio = (VIEW_ENV_HEIGHT / 2) - 200
+                setState(
+                    SnapshotSplash(
+                        Snapshot.WithoutHeader(
+                            createBitmap(
+                                width = VIEW_ENV_WIDTH / 2,
+                                height = lessThanHeightMatchingAspectRatio,
+                            ),
+                            ROTATION_0,
+                            Color.DKGRAY,
+                        ),
+                        null,
+                    )
+                )
+                setImageMatrix(Matrix().apply { postScale(2f, 2f) })
+            }
+        }
+    }
+
+    @Test
+    fun taskThumbnailView_splash_withoutHeader() {
+        screenshotRule.screenshotTest("taskThumbnailView_partial_splash") { activity ->
+            activity.actionBar?.hide()
+            createTaskThumbnailView(activity).apply {
+                setState(
+                    SnapshotSplash(
+                        Snapshot.WithoutHeader(createBitmap(), ROTATION_0, Color.DKGRAY),
+                        BitmapDrawable(activity.resources, createSplash()),
+                    )
+                )
+                updateSplashAlpha(0.5f)
+            }
+        }
+    }
+
+    @Test
+    fun taskThumbnailView_splash_withoutHeader_withImageMatrix() {
+        screenshotRule.screenshotTest("taskThumbnailView_partial_splash_withMatrix") { activity ->
+            activity.actionBar?.hide()
+            createTaskThumbnailView(activity).apply {
+                val lessThanHeightMatchingAspectRatio = (VIEW_ENV_HEIGHT / 2) - 200
+                setState(
+                    SnapshotSplash(
+                        Snapshot.WithoutHeader(
+                            createBitmap(
+                                width = VIEW_ENV_WIDTH / 2,
+                                height = lessThanHeightMatchingAspectRatio,
+                            ),
+                            ROTATION_0,
+                            Color.DKGRAY,
+                        ),
+                        BitmapDrawable(activity.resources, createSplash()),
+                    )
+                )
+                setImageMatrix(Matrix().apply { postScale(2f, 2f) })
+                updateSplashAlpha(0.5f)
+            }
+        }
+    }
+
+    @Test
     fun taskThumbnailView_dimmed_tintAmount() {
         screenshotRule.screenshotTest("taskThumbnailView_dimmed_40") { activity ->
             activity.actionBar?.hide()
@@ -118,18 +213,47 @@
         }
     }
 
+    @Test
+    fun taskThumbnailView_scaled_roundRoundedCorners() {
+        screenshotRule.screenshotTest("taskThumbnailView_scaledRoundedCorners") { activity ->
+            activity.actionBar?.hide()
+            createTaskThumbnailView(activity).apply {
+                scaleX = 0.75f
+                scaleY = 0.3f
+                setState(BackgroundOnly(Color.YELLOW))
+            }
+        }
+    }
+
     private fun createTaskThumbnailView(context: Context): TaskThumbnailView {
-        val di = RecentsDependencies.initialize(context)
         val taskThumbnailView =
             LayoutInflater.from(context).inflate(R.layout.task_thumbnail, null, false)
                 as TaskThumbnailView
         taskThumbnailView.cornerRadius = CORNER_RADIUS
-        val ttvDiScopeId = di.getScope(taskThumbnailView).scopeId
-        di.provide(TaskThumbnailViewModel::class.java, ttvDiScopeId) { taskThumbnailViewModel }
-
         return taskThumbnailView
     }
 
+    private fun createSplash() = createBitmap(width = 20, height = 20, rectColorRotation = 1)
+
+    private fun createBitmap(
+        width: Int = VIEW_ENV_WIDTH,
+        height: Int = VIEW_ENV_HEIGHT,
+        rectColorRotation: Int = 0,
+    ) =
+        Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888).apply {
+            Canvas(this).apply {
+                val paint = Paint()
+                paint.color = BITMAP_RECT_COLORS[rectColorRotation % 4]
+                drawRect(0f, 0f, width / 2f, height / 2f, paint)
+                paint.color = BITMAP_RECT_COLORS[(1 + rectColorRotation) % 4]
+                drawRect(width / 2f, 0f, width.toFloat(), height / 2f, paint)
+                paint.color = BITMAP_RECT_COLORS[(2 + rectColorRotation) % 4]
+                drawRect(0f, height / 2f, width / 2f, height.toFloat(), paint)
+                paint.color = BITMAP_RECT_COLORS[(3 + rectColorRotation) % 4]
+                drawRect(width / 2f, height / 2f, width.toFloat(), height.toFloat(), paint)
+            }
+        }
+
     companion object {
         @Parameters(name = "{0}")
         @JvmStatic
@@ -141,5 +265,8 @@
             )
 
         const val CORNER_RADIUS = 56f
+        val BITMAP_RECT_COLORS = listOf(Color.GREEN, Color.RED, Color.BLUE, Color.CYAN)
+        const val VIEW_ENV_WIDTH = 1440
+        const val VIEW_ENV_HEIGHT = 3120
     }
 }
diff --git a/quickstep/tests/src/com/android/quickstep/desktop/DesktopAppLaunchTransitionManagerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/desktop/DesktopAppLaunchTransitionManagerTest.kt
similarity index 90%
rename from quickstep/tests/src/com/android/quickstep/desktop/DesktopAppLaunchTransitionManagerTest.kt
rename to quickstep/tests/multivalentTests/src/com/android/launcher3/desktop/DesktopAppLaunchTransitionManagerTest.kt
index 9ca9fe4..7ebef45 100644
--- a/quickstep/tests/src/com/android/quickstep/desktop/DesktopAppLaunchTransitionManagerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/desktop/DesktopAppLaunchTransitionManagerTest.kt
@@ -27,12 +27,10 @@
 import android.window.TransitionFilter
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
 import com.android.quickstep.SystemUiProxy
 import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
 import com.google.common.truth.Truth.assertThat
-import org.junit.After
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
@@ -43,7 +41,6 @@
 import org.mockito.kotlin.times
 import org.mockito.kotlin.verify
 import org.mockito.kotlin.whenever
-import org.mockito.quality.Strictness
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
@@ -51,12 +48,6 @@
 
     @get:Rule val mSetFlagsRule = SetFlagsRule()
 
-    private val mockitoSession =
-        mockitoSession()
-            .strictness(Strictness.LENIENT)
-            .mockStatic(DesktopModeStatus::class.java)
-            .startMocking()
-
     private val context = mock<Context>()
     private val systemUiProxy = mock<SystemUiProxy>()
     private lateinit var transitionManager: DesktopAppLaunchTransitionManager
@@ -68,11 +59,6 @@
         transitionManager = DesktopAppLaunchTransitionManager(context, systemUiProxy)
     }
 
-    @After
-    fun tearDown() {
-        mockitoSession.finishMocking()
-    }
-
     @Test
     @EnableFlags(FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX)
     fun registerTransitions_appLaunchFlagEnabled_registersTransition() {
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/model/QuickstepModelDelegateTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/model/QuickstepModelDelegateTest.kt
index 0005df6..09c62aa 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/model/QuickstepModelDelegateTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/model/QuickstepModelDelegateTest.kt
@@ -19,7 +19,6 @@
 import android.app.prediction.AppTarget
 import android.app.prediction.AppTargetEvent
 import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.android.launcher3.LauncherAppState
 import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION
 import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_PREDICTION
 import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WALLPAPERS
@@ -54,11 +53,17 @@
     fun setUp() {
         MockitoAnnotations.initMocks(this)
         modelHelper = LauncherModelHelper()
-        underTest = QuickstepModelDelegate(modelHelper.sandboxContext)
+        underTest =
+            QuickstepModelDelegate(
+                modelHelper.sandboxContext,
+                modelHelper.sandboxContext.appComponent.idp,
+                modelHelper.sandboxContext.appComponent.packageManagerHelper,
+                "", /* dbFileName */
+            )
         underTest.mAllAppsState.predictor = allAppsPredictor
         underTest.mHotseatState.predictor = hotseatPredictor
         underTest.mWidgetsRecommendationState.predictor = widgetRecommendationPredictor
-        underTest.mApp = LauncherAppState.getInstance(modelHelper.sandboxContext)
+        underTest.mModel = modelHelper.model
         underTest.mDataModel = BgDataModel()
     }
 
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/model/data/TaskViewItemInfoTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/model/data/TaskViewItemInfoTest.kt
index de0da64..adfbca5 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/model/data/TaskViewItemInfoTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/model/data/TaskViewItemInfoTest.kt
@@ -47,6 +47,7 @@
 import com.google.common.truth.Truth.assertThat
 import dagger.BindsInstance
 import dagger.Component
+import org.junit.After
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -78,6 +79,11 @@
         RecentsDependencies.initialize(context)
     }
 
+    @After
+    fun tearDown() {
+        RecentsDependencies.destroy()
+    }
+
     @Test
     fun singleTask() {
         val taskContainers = listOf(createTaskContainer(createTask(1)))
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarOverflowTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarOverflowTest.kt
index bfd53ef..3761044 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarOverflowTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarOverflowTest.kt
@@ -19,6 +19,7 @@
 import android.animation.AnimatorTestRule
 import android.content.ComponentName
 import android.content.Intent
+import android.os.Process
 import android.platform.test.annotations.DisableFlags
 import android.platform.test.annotations.EnableFlags
 import android.platform.test.flag.junit.SetFlagsRule
@@ -27,6 +28,7 @@
 import com.android.launcher3.Flags.FLAG_TASKBAR_OVERFLOW
 import com.android.launcher3.R
 import com.android.launcher3.dagger.LauncherAppSingleton
+import com.android.launcher3.model.data.ItemInfo
 import com.android.launcher3.taskbar.TaskbarControllerTestUtil.runOnMainSync
 import com.android.launcher3.taskbar.TaskbarViewTestUtil.createHotseatItems
 import com.android.launcher3.taskbar.bubbles.BubbleBarViewController
@@ -111,6 +113,7 @@
     @InjectController lateinit var recentAppsController: TaskbarRecentAppsController
     @InjectController lateinit var bubbleBarViewController: BubbleBarViewController
     @InjectController lateinit var bubbleStashController: BubbleStashController
+    @InjectController lateinit var keyboardQuickSwitchController: KeyboardQuickSwitchController
 
     private var desktopTaskListener: IDesktopTaskListener? = null
 
@@ -209,8 +212,10 @@
         runOnMainSync {
             val taskbarView: TaskbarView =
                 taskbarUnitTestRule.activityContext.dragLayer.findViewById(R.id.taskbar_view)
+            val hotseatItems = createHotseatItems(maxNumberOfTaskbarIcons - initialIconCount)
+
             taskbarView.updateItems(
-                createHotseatItems(maxNumberOfTaskbarIcons - initialIconCount),
+                recentAppsController.updateHotseatItemInfos(hotseatItems as Array<ItemInfo?>),
                 recentAppsController.shownTasks,
             )
         }
@@ -327,16 +332,127 @@
         assertThat(taskbarIconsCentered).isTrue()
     }
 
-    private fun createDesktopTask(tasksToAdd: Int) {
-        val tasks =
-            (0..<tasksToAdd).map {
-                Task(Task.TaskKey(it, 0, Intent(), ComponentName("", ""), 0, 2000))
-            }
-        recentsModel.updateRecentTasks(listOf(DesktopTask(tasks)))
-        desktopTaskListener?.onTasksVisibilityChanged(
-            context.virtualDisplay.display.displayId,
-            tasksToAdd,
+    @Test
+    @TaskbarMode(PINNED)
+    fun testPressingOverflowButtonOpensKeyboardQuickSwitch() {
+        val maxNumIconViews = maxNumberOfTaskbarIcons
+        // Assume there are at least all apps and divider icon, as they would appear once running
+        // apps are added, even if not present initially.
+        val initialIconCount = currentNumberOfTaskbarIcons.coerceAtLeast(2)
+
+        val targetOverflowSize = 5
+        val createdTasks = maxNumIconViews - initialIconCount + targetOverflowSize
+        createDesktopTask(createdTasks)
+
+        assertThat(taskbarOverflowIconIndex).isEqualTo(initialIconCount)
+        tapOverflowIcon()
+        // Keyboard quick switch view is shown only after list of recent task is asynchronously
+        // retrieved from the recents model.
+        runOnMainSync { recentsModel.resolvePendingTaskRequests() }
+
+        assertThat(getOnUiThread { keyboardQuickSwitchController.isShownFromTaskbar }).isTrue()
+        assertThat(getOnUiThread { keyboardQuickSwitchController.shownTaskIds() })
+            .containsExactlyElementsIn(0..<createdTasks)
+
+        tapOverflowIcon()
+        assertThat(keyboardQuickSwitchController.isShown).isFalse()
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testHotseatItemTasksNotShownInRecents() {
+        val maxNumIconViews = maxNumberOfTaskbarIcons
+        // Assume there are at least all apps and divider icon, as they would appear once running
+        // apps are added, even if not present initially.
+        val initialIconCount = currentNumberOfTaskbarIcons.coerceAtLeast(2)
+        val hotseatItems = createHotseatItems(1)
+
+        val targetOverflowSize = 5
+        val createdTasks = maxNumIconViews - initialIconCount + targetOverflowSize
+        createDesktopTaskWithTasksFromPackages(
+            listOf("fake") +
+                listOf(hotseatItems[0]?.targetPackage ?: "") +
+                List(createdTasks - 2) { "fake" }
         )
+
+        runOnMainSync {
+            val taskbarView: TaskbarView =
+                taskbarUnitTestRule.activityContext.dragLayer.findViewById(R.id.taskbar_view)
+            taskbarView.updateItems(
+                recentAppsController.updateHotseatItemInfos(hotseatItems as Array<ItemInfo?>),
+                recentAppsController.shownTasks,
+            )
+        }
+
+        assertThat(maxNumberOfTaskbarIcons).isEqualTo(maxNumIconViews)
+        assertThat(currentNumberOfTaskbarIcons).isEqualTo(maxNumIconViews)
+        assertThat(taskbarOverflowIconIndex).isEqualTo(initialIconCount + hotseatItems.size)
+        assertThat(overflowItems)
+            .containsExactlyElementsIn(listOf(0) + (2..targetOverflowSize + 1).toList())
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testHotseatItemTasksNotShownInKQS() {
+        val maxNumIconViews = maxNumberOfTaskbarIcons
+        // Assume there are at least all apps and divider icon, as they would appear once running
+        // apps are added, even if not present initially.
+        val initialIconCount = currentNumberOfTaskbarIcons.coerceAtLeast(2)
+        val hotseatItems = createHotseatItems(1)
+
+        val targetOverflowSize = 5
+        val createdTasks = maxNumIconViews - initialIconCount + targetOverflowSize
+        createDesktopTaskWithTasksFromPackages(
+            listOf("fake") +
+                listOf(hotseatItems[0]?.targetPackage ?: "") +
+                List(createdTasks - 2) { "fake" }
+        )
+
+        runOnMainSync {
+            val taskbarView: TaskbarView =
+                taskbarUnitTestRule.activityContext.dragLayer.findViewById(R.id.taskbar_view)
+            taskbarView.updateItems(
+                recentAppsController.updateHotseatItemInfos(hotseatItems as Array<ItemInfo?>),
+                recentAppsController.shownTasks,
+            )
+        }
+
+        tapOverflowIcon()
+        // Keyboard quick switch view is shown only after list of recent task is asynchronously
+        // retrieved from the recents model.
+        runOnMainSync { recentsModel.resolvePendingTaskRequests() }
+
+        assertThat(getOnUiThread { keyboardQuickSwitchController.isShownFromTaskbar }).isTrue()
+        assertThat(getOnUiThread { keyboardQuickSwitchController.shownTaskIds() })
+            .containsExactlyElementsIn(listOf(0) + (2..<createdTasks).toList())
+    }
+
+    private fun createDesktopTask(tasksToAdd: Int) {
+        createDesktopTaskWithTasksFromPackages((0..<tasksToAdd).map { "fake" })
+    }
+
+    private fun createDesktopTaskWithTasksFromPackages(packages: List<String>) {
+        val tasks =
+            packages.mapIndexed({ index, p ->
+                Task(
+                    Task.TaskKey(
+                        index,
+                        0,
+                        Intent().apply { `package` = p },
+                        ComponentName(p, ""),
+                        Process.myUserHandle().identifier,
+                        2000,
+                    )
+                )
+            })
+
+        recentsModel.updateRecentTasks(listOf(DesktopTask(deskId = 0, tasks)))
+        for (task in 1..tasks.size) {
+            desktopTaskListener?.onTasksVisibilityChanged(
+                context.virtualDisplay.display.displayId,
+                task,
+            )
+        }
         runOnMainSync { recentsModel.resolvePendingTaskRequests() }
     }
 
@@ -392,6 +508,14 @@
             }
         }
 
+    private fun tapOverflowIcon() {
+        runOnMainSync {
+            val overflowIcon =
+                taskbarViewController.iconViews.firstOrNull { it is TaskbarOverflowView }
+            assertThat(overflowIcon?.callOnClick()).isTrue()
+        }
+    }
+
     /**
      * Adds enough running apps for taskbar to enter overflow of `targetOverflowSize`, and verifies
      * * max number of icons in the taskbar remains unchanged
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
index c792783..8376bc1 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
@@ -877,7 +877,7 @@
         val allTasks =
             ArrayList<GroupTask>().apply {
                 if (!runningTasks.isEmpty()) {
-                    add(DesktopTask(ArrayList(runningTasks)))
+                    add(DesktopTask(deskId = 0, ArrayList(runningTasks)))
                 }
                 addAll(recentTasks)
             }
@@ -959,6 +959,8 @@
     private fun setInDesktopMode(inDesktopMode: Boolean) {
         whenever(taskbarControllers.taskbarDesktopModeController.shouldShowDesktopTasksInTaskbar())
             .thenReturn(inDesktopMode)
+        whenever(taskbarControllers.taskbarDesktopModeController.isInDesktopMode)
+            .thenReturn(inDesktopMode)
     }
 
     private fun createItemInfo(
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/launcher3/taskbar/rules/MockedRecentsModelHelper.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/MockedRecentsModelHelper.kt
index a7bfa9a..5f7b360 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/MockedRecentsModelHelper.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/MockedRecentsModelHelper.kt
@@ -19,6 +19,7 @@
 import com.android.quickstep.RecentsModel
 import com.android.quickstep.RecentsModel.RecentTasksChangedListener
 import com.android.quickstep.TaskIconCache
+import com.android.quickstep.TaskThumbnailCache
 import com.android.quickstep.util.GroupTask
 import java.util.function.Consumer
 import org.mockito.kotlin.any
@@ -27,9 +28,11 @@
 import org.mockito.kotlin.doReturn
 import org.mockito.kotlin.mock
 
-/** Helper class to mock the {@link RecentsModel} object in test */
+/** Helper class to mock the [RecentsModel] object in test */
 class MockedRecentsModelHelper {
     private val mockIconCache: TaskIconCache = mock()
+    private val mockThumbnailCache: TaskThumbnailCache = mock()
+
     var taskListId = 0
     var recentTasksChangedListener: RecentTasksChangedListener? = null
     var taskRequests: MutableList<(List<GroupTask>) -> Unit> = mutableListOf()
@@ -37,6 +40,8 @@
     val mockRecentsModel: RecentsModel = mock {
         on { iconCache } doReturn mockIconCache
 
+        on { thumbnailCache } doReturn mockThumbnailCache
+
         on { unregisterRecentTasksChangedListener() } doAnswer { recentTasksChangedListener = null }
 
         on { registerRecentTasksChangedListener(any<RecentTasksChangedListener>()) } doAnswer
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt
index 0204b2d..2dacf69 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt
@@ -113,8 +113,8 @@
                                 object : TaskbarNavButtonCallbacks {},
                                 RecentsDisplayModel.INSTANCE.get(context),
                             ) {
-                            override fun recreateTaskbar() {
-                                super.recreateTaskbar()
+                            override fun recreateTaskbars() {
+                                super.recreateTaskbars()
                                 if (currentActivityContext != null) {
                                     injectControllers()
                                     controllerInjectionCallback.invoke()
@@ -146,7 +146,7 @@
     }
 
     /** Simulates Taskbar recreation lifecycle. */
-    fun recreateTaskbar() = instrumentation.runOnMainSync { taskbarManager.recreateTaskbar() }
+    fun recreateTaskbar() = instrumentation.runOnMainSync { taskbarManager.recreateTaskbars() }
 
     private fun injectControllers() {
         val bubbleControllerTypes =
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContext.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContext.kt
index 95e8980..e6806b7 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContext.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContext.kt
@@ -26,7 +26,7 @@
 import com.android.launcher3.dagger.ApplicationContext
 import com.android.launcher3.dagger.LauncherAppComponent
 import com.android.launcher3.dagger.LauncherAppSingleton
-import com.android.launcher3.util.AllModulesForTest
+import com.android.launcher3.util.AllModulesMinusWMProxy
 import com.android.launcher3.util.DaggerSingletonTracker
 import com.android.launcher3.util.DisplayController
 import com.android.launcher3.util.FakePrefsModule
@@ -138,7 +138,13 @@
 
 @LauncherAppSingleton
 @Component(
-    modules = [AllModulesForTest::class, FakePrefsModule::class, DisplayControllerModule::class]
+    modules =
+        [
+            AllModulesMinusWMProxy::class,
+            FakePrefsModule::class,
+            DisplayControllerModule::class,
+            TaskbarSandboxModule::class,
+        ]
 )
 interface TaskbarSandboxComponent : LauncherAppComponent {
 
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/util/SettingsCacheSandbox.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/util/SettingsCacheSandbox.kt
index dcd5352..52238c8 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/util/SettingsCacheSandbox.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/util/SettingsCacheSandbox.kt
@@ -17,19 +17,22 @@
 package com.android.launcher3.util
 
 import android.net.Uri
+import com.android.launcher3.util.SettingsCache.OnChangeListener
 import org.mockito.kotlin.any
 import org.mockito.kotlin.doAnswer
 import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
 
-/**
- * Provides a sandboxed [SettingsCache] for testing.
- *
- * Note that listeners registered to [cache] will never be invoked.
- */
+/** Provides [SettingsCache] sandboxed from system settings for testing. */
 class SettingsCacheSandbox {
     private val values = mutableMapOf<Uri, Int>()
+    private val listeners = mutableMapOf<Uri, MutableSet<OnChangeListener>>()
 
-    /** Fake cache that delegates [SettingsCache.getValue] to [values]. */
+    /**
+     * Fake cache that delegates:
+     * - [SettingsCache.getValue] to [values]
+     * - [SettingsCache.mListenerMap] to [listeners].
+     */
     val cache =
         mock<SettingsCache> {
             on { getValue(any<Uri>()) } doAnswer { mock.getValue(it.getArgument(0), 1) }
@@ -37,11 +40,22 @@
                 {
                     values.getOrDefault(it.getArgument(0), it.getArgument(1)) == 1
                 }
+
+            doAnswer {
+                    listeners.getOrPut(it.getArgument(0)) { mutableSetOf() }.add(it.getArgument(1))
+                }
+                .whenever(mock)
+                .register(any(), any())
+            doAnswer { listeners[it.getArgument(0)]?.remove(it.getArgument(1)) }
+                .whenever(mock)
+                .unregister(any(), any())
         }
 
     operator fun get(key: Uri): Int? = values[key]
 
     operator fun set(key: Uri, value: Int) {
+        if (value == values[key]) return
         values[key] = value
+        listeners[key]?.forEach { it.onSettingsChanged(value == 1) }
     }
 }
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/AllAppsActionManagerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/AllAppsActionManagerTest.kt
index 73b35e8..a1bd107 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/AllAppsActionManagerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/AllAppsActionManagerTest.kt
@@ -18,32 +18,59 @@
 
 import android.app.PendingIntent
 import android.content.IIntentSender
+import android.provider.Settings
+import android.provider.Settings.Secure.USER_SETUP_COMPLETE
 import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.platform.app.InstrumentationRegistry
+import com.android.launcher3.dagger.LauncherAppComponent
+import com.android.launcher3.dagger.LauncherAppSingleton
+import com.android.launcher3.util.AllModulesForTest
 import com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR
+import com.android.launcher3.util.SandboxApplication
+import com.android.launcher3.util.SettingsCache
+import com.android.launcher3.util.SettingsCacheSandbox
 import com.android.launcher3.util.TestUtil
 import com.google.common.truth.Truth.assertThat
+import dagger.BindsInstance
+import dagger.Component
 import java.util.concurrent.Semaphore
 import java.util.concurrent.TimeUnit.SECONDS
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
 private const val TIMEOUT = 5L
+private val USER_SETUP_COMPLETE_URI = Settings.Secure.getUriFor(USER_SETUP_COMPLETE)
 
 @RunWith(AndroidJUnit4::class)
 class AllAppsActionManagerTest {
     private val callbackSemaphore = Semaphore(0)
     private val bgExecutor = UI_HELPER_EXECUTOR
 
-    private val allAppsActionManager =
-        AllAppsActionManager(
-            InstrumentationRegistry.getInstrumentation().targetContext,
-            bgExecutor,
-        ) {
-            callbackSemaphore.release()
-            PendingIntent(IIntentSender.Default())
+    @get:Rule val context = SandboxApplication()
+
+    private val settingsCacheSandbox =
+        SettingsCacheSandbox().also { it[USER_SETUP_COMPLETE_URI] = 1 }
+
+    private val allAppsActionManager by
+        lazy(LazyThreadSafetyMode.NONE) {
+            AllAppsActionManager(context, bgExecutor) {
+                callbackSemaphore.release()
+                PendingIntent(IIntentSender.Default())
+            }
         }
 
+    @Before
+    fun initDaggerComponent() {
+        context.initDaggerComponent(
+            DaggerAllAppsActionManagerTestComponent.builder()
+                .bindSettingsCache(settingsCacheSandbox.cache)
+        )
+    }
+
+    @After fun destroyManager() = allAppsActionManager.onDestroy()
+
     @Test
     fun taskbarPresent_actionRegistered() {
         allAppsActionManager.isTaskbarPresent = true
@@ -88,4 +115,50 @@
         assertThat(callbackSemaphore.tryAcquire(TIMEOUT, SECONDS)).isTrue()
         assertThat(allAppsActionManager.isActionRegistered).isTrue()
     }
+
+    @Test
+    fun taskbarPresent_userSetupIncomplete_actionUnregistered() {
+        settingsCacheSandbox[USER_SETUP_COMPLETE_URI] = 0
+        allAppsActionManager.isTaskbarPresent = true
+        assertThat(allAppsActionManager.isActionRegistered).isFalse()
+    }
+
+    @Test
+    fun taskbarPresent_setupUiVisible_actionUnregistered() {
+        allAppsActionManager.isSetupUiVisible = true
+        allAppsActionManager.isTaskbarPresent = true
+        assertThat(allAppsActionManager.isActionRegistered).isFalse()
+    }
+
+    @Test
+    fun taskbarPresent_userSetupCompleted_actionRegistered() {
+        settingsCacheSandbox[USER_SETUP_COMPLETE_URI] = 0
+        allAppsActionManager.isTaskbarPresent = true
+
+        settingsCacheSandbox[USER_SETUP_COMPLETE_URI] = 1
+        assertThat(callbackSemaphore.tryAcquire(TIMEOUT, SECONDS)).isTrue()
+        assertThat(allAppsActionManager.isActionRegistered).isTrue()
+    }
+
+    @Test
+    fun taskbarPresent_setupUiDismissed_actionRegistered() {
+        allAppsActionManager.isSetupUiVisible = true
+        allAppsActionManager.isTaskbarPresent = true
+
+        allAppsActionManager.isSetupUiVisible = false
+        assertThat(callbackSemaphore.tryAcquire(TIMEOUT, SECONDS)).isTrue()
+        assertThat(allAppsActionManager.isActionRegistered).isTrue()
+    }
+}
+
+@LauncherAppSingleton
+@Component(modules = [AllModulesForTest::class])
+interface AllAppsActionManagerTestComponent : LauncherAppComponent {
+
+    @Component.Builder
+    interface Builder : LauncherAppComponent.Builder {
+        @BindsInstance fun bindSettingsCache(settingsCache: SettingsCache): Builder
+
+        override fun build(): AllAppsActionManagerTestComponent
+    }
 }
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentTasksListTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentTasksListTest.java
index 8b17958..ad9bbb9 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentTasksListTest.java
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentTasksListTest.java
@@ -177,8 +177,8 @@
                 createRecentTaskInfo(4 /* taskId */, DEFAULT_DISPLAY),
                 createRecentTaskInfo(5 /* taskId */, 1 /* displayId */),
                 createRecentTaskInfo(6 /* taskId */, 1 /* displayId */));
-        GroupedTaskInfo recentTaskInfos = GroupedTaskInfo.forFreeformTasks(
-                tasks, Collections.emptySet() /* minimizedTaskIds */);
+        GroupedTaskInfo recentTaskInfos = GroupedTaskInfo.forDeskTasks(
+                0 /* deskId */, tasks, Collections.emptySet() /* minimizedTaskIds */);
         when(mSystemUiProxy.getRecentTasks(anyInt(), anyInt()))
                 .thenReturn(new ArrayList<>(Collections.singletonList(recentTaskInfos)));
 
@@ -207,8 +207,8 @@
                 createRecentTaskInfo(4 /* taskId */, DEFAULT_DISPLAY),
                 createRecentTaskInfo(5 /* taskId */, 1 /* displayId */),
                 createRecentTaskInfo(6 /* taskId */, 1 /* displayId */));
-        GroupedTaskInfo recentTaskInfos = GroupedTaskInfo.forFreeformTasks(
-                tasks, Collections.emptySet() /* minimizedTaskIds */);
+        GroupedTaskInfo recentTaskInfos = GroupedTaskInfo.forDeskTasks(
+                0 /* deskId */, tasks, Collections.emptySet() /* minimizedTaskIds */);
         when(mSystemUiProxy.getRecentTasks(anyInt(), anyInt()))
                 .thenReturn(new ArrayList<>(Collections.singletonList(recentTaskInfos)));
 
@@ -241,7 +241,8 @@
                 createRecentTaskInfo(5 /* taskId */, DEFAULT_DISPLAY));
         Set<Integer> minimizedTaskIds =
                 Arrays.stream(new Integer[]{1, 4, 5}).collect(Collectors.toSet());
-        GroupedTaskInfo recentTaskInfos = GroupedTaskInfo.forFreeformTasks(tasks, minimizedTaskIds);
+        GroupedTaskInfo recentTaskInfos = GroupedTaskInfo.forDeskTasks(
+                0 /* deskId */, tasks, minimizedTaskIds);
         when(mSystemUiProxy.getRecentTasks(anyInt(), anyInt()))
                 .thenReturn(new ArrayList<>(Collections.singletonList(recentTaskInfos)));
 
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumerTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumerTest.java
index 7776351..99a34ea 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumerTest.java
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumerTest.java
@@ -24,8 +24,11 @@
 
 import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
 
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LONG_PRESS_NAVBAR;
+import static com.android.launcher3.logging.StatsLogManager.LauncherLatencyEvent.LAUNCHER_LATENCY_CONTEXTUAL_SEARCH_LPNH_ABANDON;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.quickstep.DeviceConfigWrapper.DEFAULT_LPNH_TIMEOUT_MS;
+import static com.android.quickstep.inputconsumers.NavHandleLongPressInputConsumer.MIN_TIME_TO_LOG_ABANDON_MS;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -33,9 +36,11 @@
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
 
 import android.os.SystemClock;
@@ -47,6 +52,7 @@
 
 import com.android.launcher3.dagger.LauncherAppComponent;
 import com.android.launcher3.dagger.LauncherAppSingleton;
+import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.util.AllModulesForTest;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext;
@@ -83,6 +89,7 @@
     private NavHandleLongPressInputConsumer mUnderTest;
     private SandboxContext mContext;
     private float mScreenWidth;
+    private long mDownTimeMs;
     @Mock InputConsumer mDelegate;
     @Mock InputMonitorCompat mInputMonitor;
     @Mock RecentsAnimationDeviceState mDeviceState;
@@ -91,6 +98,9 @@
     @Mock NavHandleLongPressHandler mNavHandleLongPressHandler;
     @Mock TopTaskTracker mTopTaskTracker;
     @Mock TopTaskTracker.CachedTaskInfo mTaskInfo;
+    @Mock StatsLogManager mStatsLogManager;
+    @Mock StatsLogManager.StatsLogger mStatsLogger;
+    @Mock StatsLogManager.StatsLatencyLogger mStatsLatencyLogger;
 
     @Before
     public void setup() {
@@ -100,6 +110,11 @@
         when(mDelegate.allowInterceptByParent()).thenReturn(true);
         mLongPressTriggered.set(false);
         when(mNavHandleLongPressHandler.getLongPressRunnable(any())).thenReturn(mLongPressRunnable);
+        when(mStatsLogger.withPackageName(any())).thenReturn(mStatsLogger);
+        when(mStatsLatencyLogger.withInstanceId(any())).thenReturn(mStatsLatencyLogger);
+        when(mStatsLatencyLogger.withLatency(anyLong())).thenReturn(mStatsLatencyLogger);
+        when(mStatsLogManager.logger()).thenReturn(mStatsLogger);
+        when(mStatsLogManager.latencyLogger()).thenReturn(mStatsLatencyLogger);
         initializeObjectUnderTest();
     }
 
@@ -124,17 +139,23 @@
         assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
         verify(mNavHandleLongPressHandler, never()).onTouchStarted(any());
         verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any());
+        verifyNoMoreInteractions(mStatsLogManager);
+        verifyNoMoreInteractions(mStatsLogger);
+        verifyNoMoreInteractions(mStatsLatencyLogger);
     }
 
     @Test
     public void testDelegateDisallowsTouchInterceptAfterTouchDown() {
+        // Touch down and wait the minimum abandonment time.
         mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN));
+        sleep(MIN_TIME_TO_LOG_ABANDON_MS);
 
         // Delegate should still get touches unless long press is triggered.
         verify(mDelegate).onMotionEvent(any());
         verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
         verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any());
 
+        // Child delegate blocks us from intercepting further motion events.
         when(mDelegate.allowInterceptByParent()).thenReturn(false);
         mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_MOVE));
 
@@ -144,46 +165,54 @@
         assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
         verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
         verify(mNavHandleLongPressHandler, times(1)).onTouchFinished(any(), any());
+        verifyNoMoreInteractions(mStatsLogger);
+        // Because we handled touch down before the child blocked additional events, log abandon.
+        verify(mStatsLatencyLogger).log(LAUNCHER_LATENCY_CONTEXTUAL_SEARCH_LPNH_ABANDON);
     }
 
     @Test
     public void testLongPressTriggered() {
         mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN));
-        SystemClock.sleep(DEFAULT_LPNH_TIMEOUT_MS);
-        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+        sleep(DEFAULT_LPNH_TIMEOUT_MS);
 
         assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_ACTIVE);
         assertTrue(mLongPressTriggered.get());
         verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
         verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any());
+        verify(mStatsLogger).log(LAUNCHER_LONG_PRESS_NAVBAR);
+        verifyNoMoreInteractions(mStatsLatencyLogger);
+
+        // Ensure abandon latency is still not logged after long press.
+        mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_UP));
+        verifyNoMoreInteractions(mStatsLatencyLogger);
     }
 
     @Test
     public void testLongPressTriggeredWithSlightVerticalMovement() {
         mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN));
-        mUnderTest.onMotionEvent(generateCenteredMotionEventWithYOffset(ACTION_MOVE,
-                -(TOUCH_SLOP - 1)));
-        SystemClock.sleep(DEFAULT_LPNH_TIMEOUT_MS);
-        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+        mUnderTest.onMotionEvent(generateCenteredMotionEventWithYOffset(ACTION_MOVE, 1));
+        sleep(DEFAULT_LPNH_TIMEOUT_MS);
 
         assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_ACTIVE);
         assertTrue(mLongPressTriggered.get());
         verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
         verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any());
+        verify(mStatsLogger).log(LAUNCHER_LONG_PRESS_NAVBAR);
+        verifyNoMoreInteractions(mStatsLatencyLogger);
     }
 
     @Test
     public void testLongPressTriggeredWithSlightHorizontalMovement() {
         mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN));
-        mUnderTest.onMotionEvent(generateMotionEvent(ACTION_MOVE,
-                mScreenWidth / 2f - (TOUCH_SLOP - 1), 0));
-        SystemClock.sleep(DEFAULT_LPNH_TIMEOUT_MS);
-        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+        mUnderTest.onMotionEvent(generateMotionEvent(ACTION_MOVE, mScreenWidth / 2f + 1, 0));
+        sleep(DEFAULT_LPNH_TIMEOUT_MS);
 
         assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_ACTIVE);
         assertTrue(mLongPressTriggered.get());
         verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
         verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any());
+        verify(mStatsLogger).log(LAUNCHER_LONG_PRESS_NAVBAR);
+        verifyNoMoreInteractions(mStatsLatencyLogger);
     }
 
     @Test
@@ -196,8 +225,7 @@
             mUnderTest.onMotionEvent(generateMotionEvent(ACTION_MOVE,
                     mScreenWidth / 2f - (TOUCH_SLOP - 1), 0));
             // We have entered the second stage, so the normal timeout shouldn't trigger.
-            SystemClock.sleep(DEFAULT_LPNH_TIMEOUT_MS);
-            InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+            sleep(DEFAULT_LPNH_TIMEOUT_MS);
 
             assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
             assertFalse(mLongPressTriggered.get());
@@ -207,14 +235,15 @@
             // After an extended time, the long press should trigger.
             float extendedDurationMultiplier =
                     (DeviceConfigWrapper.get().getTwoStageDurationPercentage() / 100f);
-            SystemClock.sleep((long) (DEFAULT_LPNH_TIMEOUT_MS
+            sleep((long) (DEFAULT_LPNH_TIMEOUT_MS
                     * (extendedDurationMultiplier - 1)));  // -1 because we already waited 1x
-            InstrumentationRegistry.getInstrumentation().waitForIdleSync();
 
             assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_ACTIVE);
             assertTrue(mLongPressTriggered.get());
             verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
             verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any());
+            verify(mStatsLogger).log(LAUNCHER_LONG_PRESS_NAVBAR);
+            verifyNoMoreInteractions(mStatsLatencyLogger);
         } catch (Exception e) {
             throw new RuntimeException(e);
         }
@@ -228,13 +257,14 @@
 
             mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN));
             // We have not entered the second stage, so the normal timeout should trigger.
-            SystemClock.sleep(DEFAULT_LPNH_TIMEOUT_MS);
-            InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+            sleep(DEFAULT_LPNH_TIMEOUT_MS);
 
             assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_ACTIVE);
             assertTrue(mLongPressTriggered.get());
             verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
             verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any());
+            verify(mStatsLogger).log(LAUNCHER_LONG_PRESS_NAVBAR);
+            verifyNoMoreInteractions(mStatsLatencyLogger);
         } catch (Exception e) {
             throw new RuntimeException(e);
         }
@@ -243,48 +273,47 @@
     @Test
     public void testLongPressAbortedByTouchUp() {
         mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN));
-        SystemClock.sleep(DEFAULT_LPNH_TIMEOUT_MS - 10);
-        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+        sleep(MIN_TIME_TO_LOG_ABANDON_MS);
 
         assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
         assertFalse(mLongPressTriggered.get());
 
         mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_UP));
         // Wait past the long press timeout, to be extra sure it wouldn't have triggered.
-        SystemClock.sleep(20);
-        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+        sleep(DEFAULT_LPNH_TIMEOUT_MS);
 
         assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
         assertFalse(mLongPressTriggered.get());
         verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
         verify(mNavHandleLongPressHandler, times(1)).onTouchFinished(any(), any());
+        verifyNoMoreInteractions(mStatsLogger);
+        verify(mStatsLatencyLogger).log(LAUNCHER_LATENCY_CONTEXTUAL_SEARCH_LPNH_ABANDON);
     }
 
     @Test
     public void testLongPressAbortedByTouchCancel() {
         mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN));
-        SystemClock.sleep(DEFAULT_LPNH_TIMEOUT_MS - 10);
-        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+        sleep(MIN_TIME_TO_LOG_ABANDON_MS);
 
         assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
         assertFalse(mLongPressTriggered.get());
 
         mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_CANCEL));
         // Wait past the long press timeout, to be extra sure it wouldn't have triggered.
-        SystemClock.sleep(20);
-        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+        sleep(DEFAULT_LPNH_TIMEOUT_MS);
 
         assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
         assertFalse(mLongPressTriggered.get());
         verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
         verify(mNavHandleLongPressHandler, times(1)).onTouchFinished(any(), any());
+        verifyNoMoreInteractions(mStatsLogger);
+        verify(mStatsLatencyLogger).log(LAUNCHER_LATENCY_CONTEXTUAL_SEARCH_LPNH_ABANDON);
     }
 
     @Test
     public void testLongPressAbortedByTouchSlopPassedVertically() {
         mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN));
-        SystemClock.sleep(DEFAULT_LPNH_TIMEOUT_MS - 10);
-        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+        sleep(MIN_TIME_TO_LOG_ABANDON_MS);
 
         assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
         assertFalse(mLongPressTriggered.get());
@@ -292,20 +321,20 @@
         mUnderTest.onMotionEvent(generateCenteredMotionEventWithYOffset(ACTION_MOVE,
                 -(TOUCH_SLOP + 1)));
         // Wait past the long press timeout, to be extra sure it wouldn't have triggered.
-        SystemClock.sleep(20);
-        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+        sleep(DEFAULT_LPNH_TIMEOUT_MS);
 
         assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
         assertFalse(mLongPressTriggered.get());
         verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
         verify(mNavHandleLongPressHandler, times(1)).onTouchFinished(any(), any());
+        verifyNoMoreInteractions(mStatsLogger);
+        verify(mStatsLatencyLogger).log(LAUNCHER_LATENCY_CONTEXTUAL_SEARCH_LPNH_ABANDON);
     }
 
     @Test
     public void testLongPressAbortedByTouchSlopPassedHorizontally() {
         mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN));
-        SystemClock.sleep(DEFAULT_LPNH_TIMEOUT_MS - 10);
-        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+        sleep(MIN_TIME_TO_LOG_ABANDON_MS);
 
         assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
         assertFalse(mLongPressTriggered.get());
@@ -313,13 +342,14 @@
         mUnderTest.onMotionEvent(generateMotionEvent(ACTION_MOVE,
                 mScreenWidth / 2f - (TOUCH_SLOP + 1), 0));
         // Wait past the long press timeout, to be extra sure it wouldn't have triggered.
-        SystemClock.sleep(20);
-        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+        sleep(DEFAULT_LPNH_TIMEOUT_MS);
 
         assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
         assertFalse(mLongPressTriggered.get());
         verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
         verify(mNavHandleLongPressHandler, times(1)).onTouchFinished(any(), any());
+        verifyNoMoreInteractions(mStatsLogger);
+        verify(mStatsLatencyLogger).log(LAUNCHER_LATENCY_CONTEXTUAL_SEARCH_LPNH_ABANDON);
     }
 
     @Test
@@ -333,8 +363,7 @@
             mUnderTest.onMotionEvent(generateCenteredMotionEventWithYOffset(ACTION_MOVE,
                     -(TOUCH_SLOP - 1)));
             // Normal duration shouldn't trigger.
-            SystemClock.sleep(DEFAULT_LPNH_TIMEOUT_MS);
-            InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+            sleep(DEFAULT_LPNH_TIMEOUT_MS);
 
             assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
             assertFalse(mLongPressTriggered.get());
@@ -345,15 +374,16 @@
             // Wait past the extended long press timeout, to be sure it wouldn't have triggered.
             float extendedDurationMultiplier =
                     (DeviceConfigWrapper.get().getTwoStageDurationPercentage() / 100f);
-            SystemClock.sleep((long) (DEFAULT_LPNH_TIMEOUT_MS
+            sleep((long) (DEFAULT_LPNH_TIMEOUT_MS
                     * (extendedDurationMultiplier - 1)));  // -1 because we already waited 1x
-            InstrumentationRegistry.getInstrumentation().waitForIdleSync();
 
             assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
             assertFalse(mLongPressTriggered.get());
             verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
             // Touch cancelled.
             verify(mNavHandleLongPressHandler, times(1)).onTouchFinished(any(), any());
+            verifyNoMoreInteractions(mStatsLogger);
+            verify(mStatsLatencyLogger).log(LAUNCHER_LATENCY_CONTEXTUAL_SEARCH_LPNH_ABANDON);
         } catch (Exception e) {
             throw new RuntimeException(e);
         }
@@ -370,8 +400,7 @@
             mUnderTest.onMotionEvent(generateMotionEvent(ACTION_MOVE,
                     mScreenWidth / 2f - (TOUCH_SLOP - 1), 0));
             // Normal duration shouldn't trigger.
-            SystemClock.sleep(DEFAULT_LPNH_TIMEOUT_MS);
-            InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+            sleep(DEFAULT_LPNH_TIMEOUT_MS);
 
             assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
             assertFalse(mLongPressTriggered.get());
@@ -382,15 +411,16 @@
             // Wait past the extended long press timeout, to be sure it wouldn't have triggered.
             float extendedDurationMultiplier =
                     (DeviceConfigWrapper.get().getTwoStageDurationPercentage() / 100f);
-            SystemClock.sleep((long) (DEFAULT_LPNH_TIMEOUT_MS
+            sleep((long) (DEFAULT_LPNH_TIMEOUT_MS
                     * (extendedDurationMultiplier - 1)));  // -1 because we already waited 1x
-            InstrumentationRegistry.getInstrumentation().waitForIdleSync();
 
             assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
             assertFalse(mLongPressTriggered.get());
             verify(mNavHandleLongPressHandler, times(1)).onTouchStarted(any());
             // Touch cancelled.
             verify(mNavHandleLongPressHandler, times(1)).onTouchFinished(any(), any());
+            verifyNoMoreInteractions(mStatsLogger);
+            verify(mStatsLatencyLogger).log(LAUNCHER_LATENCY_CONTEXTUAL_SEARCH_LPNH_ABANDON);
         } catch (Exception e) {
             throw new RuntimeException(e);
         }
@@ -400,14 +430,16 @@
     public void testTouchOutsideNavHandleIgnored() {
         // Touch the far left side of the screen. (y=0 is top of navbar region, picked arbitrarily)
         mUnderTest.onMotionEvent(generateMotionEvent(ACTION_DOWN, 0, 0));
-        SystemClock.sleep(DEFAULT_LPNH_TIMEOUT_MS);
-        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+        sleep(DEFAULT_LPNH_TIMEOUT_MS);
 
         // Should be ignored because the x position was not centered in the navbar region.
         assertThat(mUnderTest.mState).isEqualTo(DelegateInputConsumer.STATE_INACTIVE);
         assertFalse(mLongPressTriggered.get());
         verify(mNavHandleLongPressHandler, never()).onTouchStarted(any());
         verify(mNavHandleLongPressHandler, never()).onTouchFinished(any(), any());
+        verifyNoMoreInteractions(mStatsLogManager);
+        verifyNoMoreInteractions(mStatsLogger);
+        verifyNoMoreInteractions(mStatsLatencyLogger);
     }
 
     @Test
@@ -422,6 +454,20 @@
         mUnderTest.onHoverEvent(generateCenteredMotionEvent(ACTION_HOVER_ENTER));
 
         verify(mDelegate, times(2)).onHoverEvent(any());
+
+        verifyNoMoreInteractions(mStatsLogManager);
+        verifyNoMoreInteractions(mStatsLogger);
+        verifyNoMoreInteractions(mStatsLatencyLogger);
+    }
+
+    @Test
+    public void testNoLogsForShortTouch() {
+        mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_DOWN));
+        sleep(10);
+        mUnderTest.onMotionEvent(generateCenteredMotionEvent(ACTION_UP));
+        verifyNoMoreInteractions(mStatsLogManager);
+        verifyNoMoreInteractions(mStatsLogger);
+        verifyNoMoreInteractions(mStatsLatencyLogger);
     }
 
     private void initializeObjectUnderTest() {
@@ -437,6 +483,13 @@
         mUnderTest = new NavHandleLongPressInputConsumer(mContext, mDelegate, mInputMonitor,
                 mDeviceState, mNavHandle, mGestureState);
         mUnderTest.setNavHandleLongPressHandler(mNavHandleLongPressHandler);
+        mUnderTest.setStatsLogManager(mStatsLogManager);
+        mDownTimeMs = 0;
+    }
+
+    private static void sleep(long sleepMs) {
+        SystemClock.sleep(sleepMs);
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
     }
 
     /** Generate a motion event centered horizontally in the screen. */
@@ -449,8 +502,12 @@
         return generateMotionEvent(motionAction, mScreenWidth / 2f, y);
     }
 
-    private static MotionEvent generateMotionEvent(int motionAction, float x, float y) {
-        return MotionEvent.obtain(0, 0, motionAction, x, y, 0);
+    private MotionEvent generateMotionEvent(int motionAction, float x, float y) {
+        if (motionAction == ACTION_DOWN) {
+            mDownTimeMs = SystemClock.uptimeMillis();
+        }
+        long eventTime = SystemClock.uptimeMillis();
+        return MotionEvent.obtain(mDownTimeMs, eventTime, motionAction, x, y, 0);
     }
 
     private static AutoCloseable overrideTwoStageFlag(boolean value) {
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/orientation/LandscapePagedViewHandlerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/orientation/LandscapePagedViewHandlerTest.kt
index ea52842..0570c26 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/orientation/LandscapePagedViewHandlerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/orientation/LandscapePagedViewHandlerTest.kt
@@ -62,6 +62,7 @@
             isRTL,
             OVERVIEW_TASK_MARGIN_PX,
             DIVIDER_SIZE_PX,
+            oneIconHiddenDueToSmallWidth = false,
         )
     }
 
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/orientation/SeascapePagedViewHandlerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/orientation/SeascapePagedViewHandlerTest.kt
index 2bc182c..3788688 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/orientation/SeascapePagedViewHandlerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/orientation/SeascapePagedViewHandlerTest.kt
@@ -62,6 +62,7 @@
             isRTL,
             OVERVIEW_TASK_MARGIN_PX,
             DIVIDER_SIZE_PX,
+            oneIconHiddenDueToSmallWidth = false,
         )
     }
 
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskThumbnailDataSource.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskThumbnailDataSource.kt
index e10afc4..40d5e02 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskThumbnailDataSource.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskThumbnailDataSource.kt
@@ -22,7 +22,6 @@
 import com.android.systemui.shared.recents.model.ThumbnailData
 import kotlinx.coroutines.yield
 import org.mockito.kotlin.mock
-import org.mockito.kotlin.whenever
 
 class FakeTaskThumbnailDataSource : TaskThumbnailDataSource {
 
@@ -31,6 +30,8 @@
     private val completionPrevented: MutableSet<Int> = mutableSetOf()
     private val getThumbnailCalls = mutableMapOf<Int, Int>()
 
+    var highResEnabled = true
+
     /** Retrieves and sets a thumbnail on [task] from [taskIdToBitmap]. */
     override suspend fun getThumbnail(task: Task): ThumbnailData {
         getThumbnailCalls[task.key.id] = (getThumbnailCalls[task.key.id] ?: 0) + 1
@@ -38,9 +39,10 @@
         while (task.key.id in completionPrevented) {
             yield()
         }
-        return mock<ThumbnailData>().also {
-            whenever(it.thumbnail).thenReturn(taskIdToBitmap[task.key.id])
-        }
+        return ThumbnailData(
+            thumbnail = taskIdToBitmap[task.key.id],
+            reducedResolution = !highResEnabled,
+        )
     }
 
     fun getNumberOfGetThumbnailCalls(taskId: Int): Int = getThumbnailCalls[taskId] ?: 0
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegateTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegateTest.kt
index 41f6bfd..b91f8bd 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegateTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TaskVisualsChangedDelegateTest.kt
@@ -19,16 +19,19 @@
 import android.content.ComponentName
 import android.content.Intent
 import android.os.UserHandle
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.android.quickstep.recents.data.TaskVisualsChangedDelegate.TaskIconChangedCallback
 import com.android.quickstep.recents.data.TaskVisualsChangedDelegate.TaskThumbnailChangedCallback
 import com.android.systemui.shared.recents.model.Task.TaskKey
 import com.android.systemui.shared.recents.model.ThumbnailData
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
+import org.junit.runner.RunWith
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.verify
 import org.mockito.kotlin.verifyNoMoreInteractions
 
+@RunWith(AndroidJUnit4::class)
 class TaskVisualsChangedDelegateTest {
     private val taskVisualsChangeNotifier = FakeTaskVisualsChangeNotifier()
     private val highResLoadingStateNotifier = FakeHighResLoadingStateNotifier()
@@ -83,21 +86,21 @@
         // Correct match
         systemUnderTest.registerTaskIconChangedCallback(
             createTaskKey(id = 1, pkg = ALTERNATIVE_PACKAGE_NAME, userId = 1),
-            expectedListener
+            expectedListener,
         )
         // 1 out of 2 match
         systemUnderTest.registerTaskIconChangedCallback(
             createTaskKey(id = 2, pkg = PACKAGE_NAME, userId = 1),
-            listener
+            listener,
         )
         systemUnderTest.registerTaskIconChangedCallback(
             createTaskKey(id = 3, pkg = ALTERNATIVE_PACKAGE_NAME, userId = 2),
-            listener
+            listener,
         )
         // 0 out of 2 match
         systemUnderTest.registerTaskIconChangedCallback(
             createTaskKey(id = 4, pkg = PACKAGE_NAME, userId = 2),
-            listener
+            listener,
         )
 
         systemUnderTest.onTaskIconChanged(ALTERNATIVE_PACKAGE_NAME, UserHandle(1))
@@ -112,11 +115,11 @@
         val newListener = mock<TaskIconChangedCallback>()
         systemUnderTest.registerTaskIconChangedCallback(
             createTaskKey(id = 1, pkg = ALTERNATIVE_PACKAGE_NAME, userId = 1),
-            replacedListener
+            replacedListener,
         )
         systemUnderTest.registerTaskIconChangedCallback(
             createTaskKey(id = 1, pkg = ALTERNATIVE_PACKAGE_NAME, userId = 1),
-            newListener
+            newListener,
         )
 
         systemUnderTest.onTaskIconChanged(ALTERNATIVE_PACKAGE_NAME, UserHandle(1))
@@ -132,11 +135,11 @@
         val expectedThumbnailData = ThumbnailData(snapshotId = 12345)
         systemUnderTest.registerTaskThumbnailChangedCallback(
             createTaskKey(id = 1),
-            expectedListener
+            expectedListener,
         )
         systemUnderTest.registerTaskThumbnailChangedCallback(
             createTaskKey(id = 2),
-            additionalListener
+            additionalListener,
         )
 
         systemUnderTest.onTaskThumbnailChanged(1, expectedThumbnailData)
@@ -146,22 +149,41 @@
     }
 
     @Test
-    fun onHighResLoadingStateChanged_notifiesAllListeners() {
+    fun onHighResLoadingStateChanged_toEnabled_notifiesAllListeners() {
         val expectedListener = mock<TaskThumbnailChangedCallback>()
         val additionalListener = mock<TaskThumbnailChangedCallback>()
         systemUnderTest.registerTaskThumbnailChangedCallback(
             createTaskKey(id = 1),
-            expectedListener
+            expectedListener,
         )
         systemUnderTest.registerTaskThumbnailChangedCallback(
             createTaskKey(id = 2),
-            additionalListener
+            additionalListener,
         )
 
         systemUnderTest.onHighResLoadingStateChanged(true)
 
-        verify(expectedListener).onHighResLoadingStateChanged()
-        verify(additionalListener).onHighResLoadingStateChanged()
+        verify(expectedListener).onHighResLoadingStateChanged(true)
+        verify(additionalListener).onHighResLoadingStateChanged(true)
+    }
+
+    @Test
+    fun onHighResLoadingStateChanged_toDisabled_notifiesAllListeners() {
+        val expectedListener = mock<TaskThumbnailChangedCallback>()
+        val additionalListener = mock<TaskThumbnailChangedCallback>()
+        systemUnderTest.registerTaskThumbnailChangedCallback(
+            createTaskKey(id = 1),
+            expectedListener,
+        )
+        systemUnderTest.registerTaskThumbnailChangedCallback(
+            createTaskKey(id = 2),
+            additionalListener,
+        )
+
+        systemUnderTest.onHighResLoadingStateChanged(false)
+
+        verify(expectedListener).onHighResLoadingStateChanged(false)
+        verify(additionalListener).onHighResLoadingStateChanged(false)
     }
 
     @Test
@@ -171,7 +193,7 @@
         val expectedThumbnailData = ThumbnailData(snapshotId = 12345)
         systemUnderTest.registerTaskThumbnailChangedCallback(
             createTaskKey(id = 1),
-            replacedListener1
+            replacedListener1,
         )
         systemUnderTest.registerTaskThumbnailChangedCallback(createTaskKey(id = 1), newListener1)
 
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TasksRepositoryTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TasksRepositoryTest.kt
index 823f808..6790567 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TasksRepositoryTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/TasksRepositoryTest.kt
@@ -64,7 +64,7 @@
                     /* snapPosition = */ SNAP_TO_2_50_50,
                 ),
             ),
-            DesktopTask(tasks.subList(3, 6)),
+            DesktopTask(deskId = 0, tasks.subList(3, 6)),
         )
     private val recentsModel = FakeRecentTasksDataSource()
     private val taskThumbnailDataSource = FakeTaskThumbnailDataSource()
@@ -326,8 +326,9 @@
         }
 
     @Test
-    fun onHighResLoadingStateChanged_setsNewThumbnailDataOnTask() =
+    fun onHighResLoadingStateChanged_highResReplacesLowResThumbnail() =
         testScope.runTest {
+            taskThumbnailDataSource.highResEnabled = false
             recentsModel.seedTasks(defaultTaskList)
             systemUnderTest.getAllTaskData(forceRefresh = true)
 
@@ -337,16 +338,77 @@
             val expectedPreviousBitmap = taskThumbnailDataSource.taskIdToBitmap[1]
             val taskDataFlow = systemUnderTest.getTaskDataById(1)
 
-            val task1ThumbnailValues = mutableListOf<Bitmap?>()
+            val task1ThumbnailValues = mutableListOf<ThumbnailData?>()
             testScope.backgroundScope.launch {
-                taskDataFlow.map { it?.thumbnail?.thumbnail }.toList(task1ThumbnailValues)
+                taskDataFlow.map { it?.thumbnail }.toList(task1ThumbnailValues)
             }
 
             taskThumbnailDataSource.taskIdToBitmap[1] = expectedBitmap
+            taskThumbnailDataSource.highResEnabled = true
             taskVisualsChangedDelegate.onHighResLoadingStateChanged(true)
 
-            assertThat(task1ThumbnailValues.first()).isEqualTo(expectedPreviousBitmap)
-            assertThat(task1ThumbnailValues.last()).isEqualTo(expectedBitmap)
+            val firstThumbnailValue = task1ThumbnailValues.first()!!
+            assertThat(firstThumbnailValue.thumbnail).isEqualTo(expectedPreviousBitmap)
+            assertThat(firstThumbnailValue.reducedResolution).isTrue()
+
+            val lastThumbnailValue = task1ThumbnailValues.last()!!
+            assertThat(lastThumbnailValue.thumbnail).isEqualTo(expectedBitmap)
+            assertThat(lastThumbnailValue.reducedResolution).isFalse()
+        }
+
+    @Test
+    fun onHighResLoadingStateChanged_invisibleTaskIgnored() =
+        testScope.runTest {
+            taskThumbnailDataSource.highResEnabled = false
+            recentsModel.seedTasks(defaultTaskList)
+            systemUnderTest.getAllTaskData(forceRefresh = true)
+
+            systemUnderTest.setVisibleTasks(setOf(1))
+
+            val invisibleTaskId = 2
+            val taskDataFlow = systemUnderTest.getTaskDataById(invisibleTaskId)
+
+            val task2ThumbnailValues = mutableListOf<ThumbnailData?>()
+            testScope.backgroundScope.launch {
+                taskDataFlow.map { it?.thumbnail }.toList(task2ThumbnailValues)
+            }
+
+            taskThumbnailDataSource.highResEnabled = true
+            taskVisualsChangedDelegate.onHighResLoadingStateChanged(true)
+
+            assertThat(task2ThumbnailValues.filterNotNull()).isEmpty()
+            assertThat(taskThumbnailDataSource.getNumberOfGetThumbnailCalls(2)).isEqualTo(0)
+        }
+
+    @Test
+    fun onHighResLoadingStateChanged_lowResDoesNotReplaceHighResThumbnail() =
+        testScope.runTest {
+            taskThumbnailDataSource.highResEnabled = true
+            recentsModel.seedTasks(defaultTaskList)
+            systemUnderTest.getAllTaskData(forceRefresh = true)
+
+            systemUnderTest.setVisibleTasks(setOf(1))
+
+            val expectedBitmap = mock<Bitmap>()
+            val expectedPreviousBitmap = taskThumbnailDataSource.taskIdToBitmap[1]
+            val taskDataFlow = systemUnderTest.getTaskDataById(1)
+
+            val task1ThumbnailValues = mutableListOf<ThumbnailData?>()
+            testScope.backgroundScope.launch {
+                taskDataFlow.map { it?.thumbnail }.toList(task1ThumbnailValues)
+            }
+
+            taskThumbnailDataSource.taskIdToBitmap[1] = expectedBitmap
+            taskThumbnailDataSource.highResEnabled = false
+            taskVisualsChangedDelegate.onHighResLoadingStateChanged(false)
+
+            val firstThumbnailValue = task1ThumbnailValues.first()!!
+            assertThat(firstThumbnailValue.thumbnail).isEqualTo(expectedPreviousBitmap)
+            assertThat(firstThumbnailValue.reducedResolution).isFalse()
+
+            val lastThumbnailValue = task1ThumbnailValues.last()!!
+            assertThat(lastThumbnailValue.thumbnail).isEqualTo(expectedPreviousBitmap)
+            assertThat(lastThumbnailValue.reducedResolution).isFalse()
         }
 
     @Test
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailPositionUseCaseTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/domain/usecase/GetThumbnailPositionUseCaseTest.kt
similarity index 61%
rename from quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailPositionUseCaseTest.kt
rename to quickstep/tests/multivalentTests/src/com/android/quickstep/recents/domain/usecase/GetThumbnailPositionUseCaseTest.kt
index bd7d970..a253280 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailPositionUseCaseTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/domain/usecase/GetThumbnailPositionUseCaseTest.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2024 The Android Open Source Project
+ * 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.
@@ -14,22 +14,15 @@
  * limitations under the License.
  */
 
-package com.android.quickstep.recents.usecase
+package com.android.quickstep.recents.domain.usecase
 
-import android.content.ComponentName
-import android.content.Intent
 import android.graphics.Bitmap
-import android.graphics.Color
 import android.graphics.Matrix
 import android.graphics.Rect
 import android.view.Surface.ROTATION_90
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.android.quickstep.recents.data.FakeRecentsDeviceProfileRepository
 import com.android.quickstep.recents.data.FakeRecentsRotationStateRepository
-import com.android.quickstep.recents.data.FakeTasksRepository
-import com.android.quickstep.recents.usecase.ThumbnailPositionState.MatrixScaling
-import com.android.quickstep.recents.usecase.ThumbnailPositionState.MissingThumbnail
-import com.android.systemui.shared.recents.model.Task
 import com.android.systemui.shared.recents.model.ThumbnailData
 import com.android.systemui.shared.recents.utilities.PreviewPositionHelper
 import com.google.common.truth.Truth.assertThat
@@ -43,55 +36,34 @@
 /** Test for [GetThumbnailPositionUseCase] */
 @RunWith(AndroidJUnit4::class)
 class GetThumbnailPositionUseCaseTest {
-    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 deviceProfileRepository = FakeRecentsDeviceProfileRepository()
     private val rotationStateRepository = FakeRecentsRotationStateRepository()
-    private val tasksRepository = FakeTasksRepository()
     private val previewPositionHelper = mock<PreviewPositionHelper>()
 
     private val systemUnderTest =
         GetThumbnailPositionUseCase(
             deviceProfileRepository,
             rotationStateRepository,
-            tasksRepository,
             previewPositionHelper,
         )
 
     @Test
-    fun invisibleTask_returnsIdentityMatrix() = runTest {
-        tasksRepository.seedTasks(listOf(task))
-
-        assertThat(systemUnderTest.run(TASK_ID, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl = true))
-            .isInstanceOf(MissingThumbnail::class.java)
+    fun nullThumbnailData_returnsIdentityMatrix() = runTest {
+        val expectedResult = ThumbnailPosition(Matrix.IDENTITY_MATRIX, false)
+        val result = systemUnderTest.invoke(null, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl = true)
+        assertThat(result).isEqualTo(expectedResult)
     }
 
     @Test
-    fun visibleTaskWithoutThumbnailData_returnsIdentityMatrix() = runTest {
-        tasksRepository.seedTasks(listOf(task))
-        tasksRepository.setVisibleTasks(setOf(TASK_ID))
-
-        assertThat(systemUnderTest.run(TASK_ID, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl = true))
-            .isInstanceOf(MissingThumbnail::class.java)
+    fun withoutThumbnail_returnsIdentityMatrix() = runTest {
+        val expectedResult = ThumbnailPosition(Matrix.IDENTITY_MATRIX, false)
+        val result =
+            systemUnderTest.invoke(ThumbnailData(), CANVAS_WIDTH, CANVAS_HEIGHT, isRtl = true)
+        assertThat(result).isEqualTo(expectedResult)
     }
 
     @Test
     fun visibleTaskWithThumbnailData_returnsTransformedMatrix() = runTest {
-        tasksRepository.seedThumbnailData(mapOf(TASK_ID to thumbnailData))
-        tasksRepository.seedTasks(listOf(task))
-        tasksRepository.setVisibleTasks(setOf(TASK_ID))
-
         val isLargeScreen = true
         deviceProfileRepository.setRecentsDeviceProfile(
             deviceProfileRepository.getRecentsDeviceProfile().copy(isLargeScreen = isLargeScreen)
@@ -108,13 +80,14 @@
         whenever(previewPositionHelper.matrix).thenReturn(MATRIX)
         whenever(previewPositionHelper.isOrientationChanged).thenReturn(isRotated)
 
-        assertThat(systemUnderTest.run(TASK_ID, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
-            .isEqualTo(MatrixScaling(MATRIX, isRotated))
+        val result = systemUnderTest.invoke(THUMBNAIL_DATA, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl)
+        val expectedResult = ThumbnailPosition(MATRIX, isRotated)
+        assertThat(result).isEqualTo(expectedResult)
 
         verify(previewPositionHelper)
             .updateThumbnailMatrix(
                 Rect(0, 0, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT),
-                thumbnailData,
+                THUMBNAIL_DATA,
                 CANVAS_WIDTH,
                 CANVAS_HEIGHT,
                 isLargeScreen,
@@ -123,8 +96,7 @@
             )
     }
 
-    companion object {
-        const val TASK_ID = 2
+    private companion object {
         const val THUMBNAIL_WIDTH = 100
         const val THUMBNAIL_HEIGHT = 200
         const val CANVAS_WIDTH = 300
@@ -133,5 +105,14 @@
             Matrix().apply {
                 setValues(floatArrayOf(2.3f, 4.5f, 2.6f, 7.4f, 3.4f, 2.3f, 2.5f, 6.0f, 3.4f))
             }
+
+        val THUMBNAIL_DATA =
+            ThumbnailData(
+                thumbnail =
+                    mock<Bitmap>().apply {
+                        whenever(width).thenReturn(THUMBNAIL_WIDTH)
+                        whenever(height).thenReturn(THUMBNAIL_HEIGHT)
+                    }
+            )
     }
 }
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/ui/mapper/TaskUiStateMapperTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/ui/mapper/TaskUiStateMapperTest.kt
index 124045f..7ca194a 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/ui/mapper/TaskUiStateMapperTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/ui/mapper/TaskUiStateMapperTest.kt
@@ -21,6 +21,7 @@
 import android.graphics.drawable.ShapeDrawable
 import android.platform.test.annotations.EnableFlags
 import android.view.Surface
+import android.view.View
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.android.launcher3.Flags
 import com.android.quickstep.recents.ui.viewmodel.TaskData
@@ -43,6 +44,7 @@
                 taskData = null,
                 isLiveTile = false,
                 hasHeader = false,
+                clickCloseListener = null,
             )
         assertThat(result).isEqualTo(TaskThumbnailUiState.Uninitialized)
     }
@@ -57,6 +59,7 @@
                     taskData = input,
                     isLiveTile = true,
                     hasHeader = false,
+                    clickCloseListener = null,
                 )
             assertThat(result).isEqualTo(LiveTile.WithoutHeader)
         }
@@ -72,14 +75,18 @@
                 TASK_DATA.copy(isLocked = true),
                 TASK_DATA.copy(title = null),
             )
+        val closeCallback = View.OnClickListener {}
         val expected =
-            LiveTile.WithHeader(header = ThumbnailHeader(TASK_ICON, TASK_TITLE_DESCRIPTION))
+            LiveTile.WithHeader(
+                header = ThumbnailHeader(TASK_ICON, TASK_TITLE_DESCRIPTION, closeCallback)
+            )
         inputs.forEach { taskData ->
             val result =
                 TaskUiStateMapper.toTaskThumbnailUiState(
                     taskData = taskData,
                     isLiveTile = true,
                     hasHeader = true,
+                    clickCloseListener = closeCallback,
                 )
             assertThat(result).isEqualTo(expected)
         }
@@ -101,6 +108,7 @@
                     taskData = taskData,
                     isLiveTile = true,
                     hasHeader = true,
+                    clickCloseListener = {},
                 )
             assertThat(result).isEqualTo(LiveTile.WithoutHeader)
         }
@@ -113,6 +121,7 @@
                 taskData = TASK_DATA,
                 isLiveTile = false,
                 hasHeader = false,
+                clickCloseListener = null,
             )
 
         val expected =
@@ -133,6 +142,7 @@
     @Test
     fun taskData_isStaticTile_withHeader_returns_SnapshotSplashWithHeader() {
         val inputs = listOf(TASK_DATA, TASK_DATA.copy(title = null))
+        val closeCallback = View.OnClickListener {}
         val expected =
             TaskThumbnailUiState.SnapshotSplash(
                 snapshot =
@@ -140,7 +150,7 @@
                         backgroundColor = TASK_BACKGROUND_COLOR,
                         bitmap = TASK_THUMBNAIL,
                         thumbnailRotation = Surface.ROTATION_0,
-                        header = ThumbnailHeader(TASK_ICON, TASK_TITLE_DESCRIPTION),
+                        header = ThumbnailHeader(TASK_ICON, TASK_TITLE_DESCRIPTION, closeCallback),
                     ),
                 splash = TASK_ICON,
             )
@@ -150,6 +160,7 @@
                     taskData = taskData,
                     isLiveTile = false,
                     hasHeader = true,
+                    clickCloseListener = closeCallback,
                 )
             assertThat(result).isEqualTo(expected)
         }
@@ -176,6 +187,7 @@
                     taskData = taskData,
                     isLiveTile = false,
                     hasHeader = true,
+                    clickCloseListener = {},
                 )
 
             assertThat(result).isInstanceOf(TaskThumbnailUiState.SnapshotSplash::class.java)
@@ -191,6 +203,7 @@
                 taskData = TASK_DATA.copy(thumbnailData = null),
                 isLiveTile = false,
                 hasHeader = false,
+                clickCloseListener = null,
             )
 
         val expected = TaskThumbnailUiState.BackgroundOnly(TASK_BACKGROUND_COLOR)
@@ -204,6 +217,7 @@
                 taskData = TASK_DATA.copy(isLocked = true),
                 isLiveTile = false,
                 hasHeader = false,
+                clickCloseListener = null,
             )
 
         val expected = TaskThumbnailUiState.BackgroundOnly(TASK_BACKGROUND_COLOR)
@@ -212,6 +226,7 @@
 
     private companion object {
         const val TASK_TITLE_DESCRIPTION = "Title Description 1"
+        var TASK_ID = 1
         val TASK_ICON = ShapeDrawable()
         val TASK_THUMBNAIL = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
         val TASK_THUMBNAIL_DATA =
@@ -219,7 +234,7 @@
         val TASK_BACKGROUND_COLOR = Color.rgb(1, 2, 3)
         val TASK_DATA =
             TaskData.Data(
-                1,
+                TASK_ID,
                 title = "Task 1",
                 titleDescription = TASK_TITLE_DESCRIPTION,
                 icon = TASK_ICON,
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModelTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModelTest.kt
index 08e459b..a97ef0c 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModelTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModelTest.kt
@@ -29,6 +29,7 @@
 import com.android.quickstep.recents.domain.model.TaskModel
 import com.android.quickstep.recents.domain.usecase.GetSysUiStatusNavFlagsUseCase
 import com.android.quickstep.recents.domain.usecase.GetTaskUseCase
+import com.android.quickstep.recents.domain.usecase.GetThumbnailPositionUseCase
 import com.android.quickstep.recents.domain.usecase.IsThumbnailValidUseCase
 import com.android.quickstep.recents.viewmodel.RecentsViewData
 import com.android.quickstep.views.TaskViewType
@@ -58,6 +59,7 @@
 
     private val recentsViewData = RecentsViewData()
     private val getTaskUseCase = mock<GetTaskUseCase>()
+    private val getThumbnailPositionUseCase = mock<GetThumbnailPositionUseCase>()
     private val isThumbnailValidUseCase =
         spy(IsThumbnailValidUseCase(FakeRecentsRotationStateRepository()))
     private lateinit var sut: TaskViewModel
@@ -71,6 +73,7 @@
                 getTaskUseCase = getTaskUseCase,
                 getSysUiStatusNavFlagsUseCase = GetSysUiStatusNavFlagsUseCase(),
                 isThumbnailValidUseCase = isThumbnailValidUseCase,
+                getThumbnailPositionUseCase = getThumbnailPositionUseCase,
                 dispatcherProvider = TestDispatcherProvider(unconfinedTestDispatcher),
             )
         whenever(getTaskUseCase.invoke(TASK_MODEL_1.id)).thenReturn(flow { emit(TASK_MODEL_1) })
@@ -112,6 +115,7 @@
                         getTaskUseCase = getTaskUseCase,
                         getSysUiStatusNavFlagsUseCase = GetSysUiStatusNavFlagsUseCase(),
                         isThumbnailValidUseCase = isThumbnailValidUseCase,
+                        getThumbnailPositionUseCase = getThumbnailPositionUseCase,
                         dispatcherProvider = TestDispatcherProvider(unconfinedTestDispatcher),
                     )
                 sut.bind(TASK_MODEL_1.id)
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 0044631..0000000
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailUseCaseTest.kt
+++ /dev/null
@@ -1,84 +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.quickstep.task.viewmodel.TaskOverlayViewModelTest
-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(TaskOverlayViewModelTest.TASK_ID to thumbnailData))
-
-        assertThat(systemUnderTest.run(TASK_ID)).isNull()
-    }
-
-    @Test
-    fun taskVisible_returnsThumbnail() {
-        tasksRepository.seedTasks(listOf(task))
-        tasksRepository.seedThumbnailData(mapOf(TaskOverlayViewModelTest.TASK_ID to thumbnailData))
-        tasksRepository.setVisibleTasks(setOf(TaskOverlayViewModelTest.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/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelImplTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelImplTest.kt
deleted file mode 100644
index 4b4e2eb..0000000
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelImplTest.kt
+++ /dev/null
@@ -1,88 +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.task.thumbnail
-
-import android.graphics.Matrix
-import android.platform.test.flag.junit.SetFlagsRule
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
-import com.android.quickstep.recents.usecase.ThumbnailPositionState.MatrixScaling
-import com.android.quickstep.recents.usecase.ThumbnailPositionState.MissingThumbnail
-import com.android.quickstep.task.viewmodel.TaskThumbnailViewModelImpl
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.test.StandardTestDispatcher
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.runTest
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.kotlin.mock
-import org.mockito.kotlin.whenever
-
-/** Test for [TaskThumbnailView] */
-@RunWith(AndroidJUnit4::class)
-class TaskThumbnailViewModelImplTest {
-    @get:Rule val setFlagsRule = SetFlagsRule()
-
-    private val dispatcher = StandardTestDispatcher()
-    private val testScope = TestScope(dispatcher)
-
-    private val mGetThumbnailPositionUseCase = mock<GetThumbnailPositionUseCase>()
-
-    private val systemUnderTest by lazy { TaskThumbnailViewModelImpl(mGetThumbnailPositionUseCase) }
-
-    @Test
-    fun getSnapshotMatrix_MissingThumbnail() =
-        testScope.runTest {
-            val taskId = 2
-            val isRtl = true
-
-            whenever(mGetThumbnailPositionUseCase.run(taskId, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
-                .thenReturn(MissingThumbnail)
-
-            systemUnderTest.bind(taskId)
-            assertThat(
-                    systemUnderTest.getThumbnailPositionState(CANVAS_WIDTH, CANVAS_HEIGHT, isRtl)
-                )
-                .isEqualTo(Matrix.IDENTITY_MATRIX)
-        }
-
-    @Test
-    fun getSnapshotMatrix_MatrixScaling() =
-        testScope.runTest {
-            val taskId = 2
-            val isRtl = true
-
-            whenever(mGetThumbnailPositionUseCase.run(taskId, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
-                .thenReturn(MatrixScaling(MATRIX, isRotated = false))
-
-            systemUnderTest.bind(taskId)
-            assertThat(
-                    systemUnderTest.getThumbnailPositionState(CANVAS_WIDTH, CANVAS_HEIGHT, isRtl)
-                )
-                .isEqualTo(MATRIX)
-        }
-
-    private companion object {
-        const val CANVAS_WIDTH = 300
-        const val CANVAS_HEIGHT = 600
-        val MATRIX =
-            Matrix().apply {
-                setValues(floatArrayOf(2.3f, 4.5f, 2.6f, 7.4f, 3.4f, 2.3f, 2.5f, 6.0f, 3.4f))
-            }
-    }
-}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModelTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModelTest.kt
deleted file mode 100644
index 95504af..0000000
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModelTest.kt
+++ /dev/null
@@ -1,195 +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.task.viewmodel
-
-import android.content.ComponentName
-import android.content.Intent
-import android.graphics.Bitmap
-import android.graphics.Color
-import android.graphics.Matrix
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.android.launcher3.util.TestDispatcherProvider
-import com.android.quickstep.recents.data.FakeTasksRepository
-import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
-import com.android.quickstep.recents.usecase.ThumbnailPositionState.MatrixScaling
-import com.android.quickstep.recents.usecase.ThumbnailPositionState.MissingThumbnail
-import com.android.quickstep.recents.viewmodel.RecentsViewData
-import com.android.quickstep.task.thumbnail.TaskOverlayUiState.Disabled
-import com.android.quickstep.task.thumbnail.TaskOverlayUiState.Enabled
-import com.android.quickstep.task.viewmodel.TaskOverlayViewModel.ThumbnailPositionState
-import com.android.systemui.shared.recents.model.Task
-import com.android.systemui.shared.recents.model.ThumbnailData
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
-import kotlinx.coroutines.test.runTest
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.kotlin.mock
-import org.mockito.kotlin.whenever
-
-/** Test for [TaskOverlayViewModel] */
-@OptIn(ExperimentalCoroutinesApi::class)
-@RunWith(AndroidJUnit4::class)
-class TaskOverlayViewModelTest {
-    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 recentsViewData = RecentsViewData()
-    private val tasksRepository = FakeTasksRepository()
-    private val mGetThumbnailPositionUseCase = mock<GetThumbnailPositionUseCase>()
-    private val dispatcher = UnconfinedTestDispatcher()
-    private val testScope = TestScope(dispatcher)
-    private val systemUnderTest =
-        TaskOverlayViewModel(
-            task,
-            recentsViewData,
-            mGetThumbnailPositionUseCase,
-            tasksRepository,
-            TestDispatcherProvider(dispatcher),
-        )
-
-    @Test
-    fun initialStateIsDisabled() =
-        testScope.runTest { assertThat(systemUnderTest.overlayState.first()).isEqualTo(Disabled) }
-
-    @Test
-    fun recentsViewOverlayDisabled_Disabled() =
-        testScope.runTest {
-            recentsViewData.overlayEnabled.value = false
-            recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID)
-
-            assertThat(systemUnderTest.overlayState.first()).isEqualTo(Disabled)
-        }
-
-    @Test
-    fun taskNotFullyVisible_Disabled() =
-        testScope.runTest {
-            recentsViewData.overlayEnabled.value = true
-            recentsViewData.settledFullyVisibleTaskIds.value = setOf()
-
-            assertThat(systemUnderTest.overlayState.first()).isEqualTo(Disabled)
-        }
-
-    @Test
-    fun noThumbnail_Enabled() =
-        testScope.runTest {
-            recentsViewData.overlayEnabled.value = true
-            recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID)
-            task.isLocked = false
-
-            assertThat(systemUnderTest.overlayState.first())
-                .isEqualTo(Enabled(isRealSnapshot = false, thumbnail = null))
-        }
-
-    @Test
-    fun withThumbnail_RealSnapshot_NotLocked_Enabled() =
-        testScope.runTest {
-            recentsViewData.overlayEnabled.value = true
-            recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID)
-            tasksRepository.seedTasks(listOf(task))
-            tasksRepository.seedThumbnailData(mapOf(TASK_ID to thumbnailData))
-            tasksRepository.setVisibleTasks(setOf(TASK_ID))
-            thumbnailData.isRealSnapshot = true
-            task.isLocked = false
-
-            assertThat(systemUnderTest.overlayState.first())
-                .isEqualTo(Enabled(isRealSnapshot = true, thumbnail = thumbnailData.thumbnail))
-        }
-
-    @Test
-    fun withThumbnail_RealSnapshot_Locked_Enabled() =
-        testScope.runTest {
-            recentsViewData.overlayEnabled.value = true
-            recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID)
-            tasksRepository.seedTasks(listOf(task))
-            tasksRepository.seedThumbnailData(mapOf(TASK_ID to thumbnailData))
-            tasksRepository.setVisibleTasks(setOf(TASK_ID))
-            thumbnailData.isRealSnapshot = true
-            task.isLocked = true
-
-            assertThat(systemUnderTest.overlayState.first())
-                .isEqualTo(Enabled(isRealSnapshot = false, thumbnail = thumbnailData.thumbnail))
-        }
-
-    @Test
-    fun withThumbnail_FakeSnapshot_Enabled() =
-        testScope.runTest {
-            recentsViewData.overlayEnabled.value = true
-            recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID)
-            tasksRepository.seedTasks(listOf(task))
-            tasksRepository.seedThumbnailData(mapOf(TASK_ID to thumbnailData))
-            tasksRepository.setVisibleTasks(setOf(TASK_ID))
-            thumbnailData.isRealSnapshot = false
-            task.isLocked = false
-
-            assertThat(systemUnderTest.overlayState.first())
-                .isEqualTo(Enabled(isRealSnapshot = false, thumbnail = thumbnailData.thumbnail))
-        }
-
-    @Test
-    fun getThumbnailMatrix_MissingThumbnail() =
-        testScope.runTest {
-            val isRtl = true
-
-            whenever(mGetThumbnailPositionUseCase.run(TASK_ID, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
-                .thenReturn(MissingThumbnail)
-
-            assertThat(
-                    systemUnderTest.getThumbnailPositionState(CANVAS_WIDTH, CANVAS_HEIGHT, isRtl)
-                )
-                .isEqualTo(ThumbnailPositionState(Matrix.IDENTITY_MATRIX, isRotated = false))
-        }
-
-    @Test
-    fun getThumbnailMatrix_MatrixScaling() =
-        testScope.runTest {
-            val isRtl = true
-            val isRotated = true
-
-            whenever(mGetThumbnailPositionUseCase.run(TASK_ID, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
-                .thenReturn(MatrixScaling(MATRIX, isRotated))
-
-            assertThat(
-                    systemUnderTest.getThumbnailPositionState(CANVAS_WIDTH, CANVAS_HEIGHT, isRtl)
-                )
-                .isEqualTo(ThumbnailPositionState(MATRIX, isRotated))
-        }
-
-    companion object {
-        const val TASK_ID = 0
-        const val THUMBNAIL_WIDTH = 100
-        const val THUMBNAIL_HEIGHT = 200
-        const val CANVAS_WIDTH = 300
-        const val CANVAS_HEIGHT = 600
-        val MATRIX =
-            Matrix().apply {
-                setValues(floatArrayOf(2.3f, 4.5f, 2.6f, 7.4f, 3.4f, 2.3f, 2.5f, 6.0f, 3.4f))
-            }
-    }
-}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/taskbar/controllers/TaskbarPinningControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/taskbar/controllers/TaskbarPinningControllerTest.kt
index 5b42d6c..26418d8 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/taskbar/controllers/TaskbarPinningControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/taskbar/controllers/TaskbarPinningControllerTest.kt
@@ -71,8 +71,9 @@
         whenever(taskbarActivityContext.dragLayer).thenReturn(taskbarDragLayer)
         whenever(taskbarActivityContext.statsLogManager).thenReturn(statsLogManager)
         whenever(
-                taskbarControllers.taskbarDesktopModeController
-                    .areDesktopTasksVisibleAndNotInOverview
+                taskbarControllers.taskbarDesktopModeController.isInDesktopModeAndNotInOverview(
+                    taskbarActivityContext.displayId
+                )
             )
             .thenAnswer { _ -> isInDesktopMode }
         pinningController = spy(TaskbarPinningController(taskbarActivityContext))
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/AppPairsControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/AppPairsControllerTest.kt
index ee70e0a..76d36d3 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/AppPairsControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/AppPairsControllerTest.kt
@@ -16,7 +16,9 @@
 
 package com.android.quickstep.util
 
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
 import android.content.Context
+import android.content.res.Resources
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.android.launcher3.apppairs.AppPairIcon
 import com.android.launcher3.logging.StatsLogManager
@@ -28,6 +30,7 @@
 import com.android.quickstep.TopTaskTracker.CachedTaskInfo
 import com.android.systemui.shared.recents.model.Task
 import com.android.systemui.shared.recents.model.Task.TaskKey
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
 import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_33_66
 import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50
 import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_66_33
@@ -54,6 +57,7 @@
 @RunWith(AndroidJUnit4::class)
 class AppPairsControllerTest {
     @Mock lateinit var context: Context
+    @Mock lateinit var resources: Resources
     @Mock lateinit var splitSelectStateController: SplitSelectStateController
     @Mock lateinit var statsLogManager: StatsLogManager
 
@@ -109,6 +113,8 @@
         doNothing()
             .whenever(spyAppPairsController)
             .launchToSide(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull())
+        whenever(mockAppPairIcon.context.resources).thenReturn(resources)
+        whenever(DesktopModeStatus.canEnterDesktopMode(mockAppPairIcon.context)).thenReturn(false)
     }
 
     @Test
@@ -392,6 +398,68 @@
     }
 
     @Test
+    fun handleAppPairLaunchInApp_freeformTask1IsOnScreen_shouldLaunchAppPair() {
+        whenever(DesktopModeStatus.canEnterDesktopMode(mockAppPairIcon.context)).thenReturn(true)
+        /// Test launching apps 1 and 2 from app pair
+        whenever(mockTaskKey1.getId()).thenReturn(1)
+        whenever(mockTaskKey2.getId()).thenReturn(2)
+        // Task 1 is in freeform windowing mode
+        mockTaskKey1.windowingMode = WINDOWING_MODE_FREEFORM
+        // ... and app 1 is already on screen
+        if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+            whenever(mockCachedTaskInfo.topGroupedTaskContainsTask(eq(1))).thenReturn(true)
+        } else {
+            whenever(mockCachedTaskInfo.taskId).thenReturn(1)
+        }
+
+        // Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback
+        spyAppPairsController.handleAppPairLaunchInApp(
+            mockAppPairIcon,
+            listOf(mockItemInfo1, mockItemInfo2),
+        )
+        verify(splitSelectStateController)
+            .findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture())
+        val callback: Consumer<Array<Task>> = callbackCaptor.value
+        callback.accept(arrayOf(mockTask1, mockTask2))
+
+        // Verify that launchAppPair was called
+        verify(spyAppPairsController, times(1)).launchAppPair(any(), any())
+        verify(spyAppPairsController, never())
+            .launchToSide(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull())
+    }
+
+    @Test
+    fun handleAppPairLaunchInApp_freeformTask2IsOnScreen_shouldLaunchAppPair() {
+        whenever(DesktopModeStatus.canEnterDesktopMode(mockAppPairIcon.context)).thenReturn(true)
+        /// Test launching apps 1 and 2 from app pair
+        whenever(mockTaskKey1.getId()).thenReturn(1)
+        whenever(mockTaskKey2.getId()).thenReturn(2)
+        // Task 2 is in freeform windowing mode
+        mockTaskKey1.windowingMode = WINDOWING_MODE_FREEFORM
+        // ... and app 2 is already on screen
+        if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+            whenever(mockCachedTaskInfo.topGroupedTaskContainsTask(eq(2))).thenReturn(true)
+        } else {
+            whenever(mockCachedTaskInfo.taskId).thenReturn(2)
+        }
+
+        // Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback
+        spyAppPairsController.handleAppPairLaunchInApp(
+            mockAppPairIcon,
+            listOf(mockItemInfo1, mockItemInfo2),
+        )
+        verify(splitSelectStateController)
+            .findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture())
+        val callback: Consumer<Array<Task>> = callbackCaptor.value
+        callback.accept(arrayOf(mockTask1, mockTask2))
+
+        // Verify that launchAppPair was called
+        verify(spyAppPairsController, times(1)).launchAppPair(any(), any())
+        verify(spyAppPairsController, never())
+            .launchToSide(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull())
+    }
+
+    @Test
     fun handleAppPairLaunchInApp_shouldLaunchAppPairNormallyWhenUnrelatedSingleAppIsFullscreen() {
         // Test launching apps 1 and 2 from app pair
         whenever(mockTaskKey1.getId()).thenReturn(1)
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/DesktopTaskTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/DesktopTaskTest.kt
index 7aed579..6fbf482 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/DesktopTaskTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/DesktopTaskTest.kt
@@ -29,35 +29,42 @@
 
     @Test
     fun testDesktopTask_sameInstance_isEqual() {
-        val task = DesktopTask(createTasks(1))
+        val task = DesktopTask(deskId = 0, createTasks(1))
         assertThat(task).isEqualTo(task)
     }
 
     @Test
     fun testDesktopTask_identicalConstructor_isEqual() {
-        val task1 = DesktopTask(createTasks(1))
-        val task2 = DesktopTask(createTasks(1))
+        val task1 = DesktopTask(deskId = 0, createTasks(1))
+        val task2 = DesktopTask(deskId = 0, createTasks(1))
         assertThat(task1).isEqualTo(task2)
     }
 
     @Test
     fun testDesktopTask_copy_isEqual() {
-        val task1 = DesktopTask(createTasks(1))
+        val task1 = DesktopTask(deskId = 0, createTasks(1))
         val task2 = task1.copy()
         assertThat(task1).isEqualTo(task2)
     }
 
     @Test
-    fun testDesktopTask_differentId_isNotEqual() {
-        val task1 = DesktopTask(createTasks(1))
-        val task2 = DesktopTask(createTasks(2))
+    fun testDesktopTask_differentDeskIds_isNotEqual() {
+        val task1 = DesktopTask(deskId = 0, createTasks(1))
+        val task2 = DesktopTask(deskId = 1, createTasks(1))
+        assertThat(task1).isNotEqualTo(task2)
+    }
+
+    @Test
+    fun testDesktopTask_differentTaskIds_isNotEqual() {
+        val task1 = DesktopTask(deskId = 0, createTasks(1))
+        val task2 = DesktopTask(deskId = 0, createTasks(2))
         assertThat(task1).isNotEqualTo(task2)
     }
 
     @Test
     fun testDesktopTask_differentLength_isNotEqual() {
-        val task1 = DesktopTask(createTasks(1))
-        val task2 = DesktopTask(createTasks(1, 2))
+        val task1 = DesktopTask(deskId = 0, createTasks(1))
+        val task2 = DesktopTask(deskId = 0, createTasks(1, 2))
         assertThat(task1).isNotEqualTo(task2)
     }
 
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/GroupTaskTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/GroupTaskTest.kt
index fa043b9..67fc62f 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/GroupTaskTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/GroupTaskTest.kt
@@ -98,7 +98,7 @@
     @Test
     fun testGroupTask_differentType_isNotEqual() {
         val task1 = SingleTask(createTask(1))
-        val task2 = DesktopTask(listOf(createTask(1)))
+        val task2 = DesktopTask(deskId = 0, listOf(createTask(1)))
         assertThat(task1).isNotEqualTo(task2)
     }
 
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt
index 5051251..9d000a4 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt
@@ -89,7 +89,7 @@
     @Before
     fun setup() {
         whenever(mockTaskContainer.snapshotView).thenReturn(mockSnapshotView)
-        whenever(mockTaskContainer.splitAnimationThumbnail).thenReturn(mockBitmap)
+        whenever(mockTaskContainer.thumbnail).thenReturn(mockBitmap)
         whenever(mockTaskContainer.iconView).thenReturn(mockIconView)
         whenever(mockTaskContainer.task).thenReturn(mockTask)
         whenever(mockIconView.drawable).thenReturn(mockTaskViewDrawable)
@@ -117,7 +117,7 @@
         assertEquals(
             "Did not fallback to use splitSource icon drawable",
             mockSplitSourceDrawable,
-            splitAnimInitProps.iconDrawable
+            splitAnimInitProps.iconDrawable,
         )
     }
 
@@ -133,7 +133,7 @@
         assertEquals(
             "Did not use taskView icon drawable",
             mockTaskViewDrawable,
-            splitAnimInitProps.iconDrawable
+            splitAnimInitProps.iconDrawable,
         )
     }
 
@@ -152,7 +152,7 @@
         assertEquals(
             "Did not use taskView icon drawable",
             mockTaskViewDrawable,
-            splitAnimInitProps.iconDrawable
+            splitAnimInitProps.iconDrawable,
         )
     }
 
@@ -168,7 +168,7 @@
         assertEquals(
             "Did not use splitSource icon drawable",
             mockSplitSourceDrawable,
-            splitAnimInitProps.iconDrawable
+            splitAnimInitProps.iconDrawable,
         )
     }
 
@@ -190,13 +190,13 @@
         val splitAnimInitProps: SplitAnimationController.Companion.SplitAnimInitProps =
             splitAnimationController.getFirstAnimInitViews(
                 { mockGroupedTaskView },
-                { splitSelectSource }
+                { splitSelectSource },
             )
 
         assertEquals(
             "Did not use splitSource icon drawable",
             mockSplitSourceDrawable,
-            splitAnimInitProps.iconDrawable
+            splitAnimInitProps.iconDrawable,
         )
     }
 
@@ -214,7 +214,7 @@
                 any(),
                 any(),
                 any(),
-                any()
+                any(),
             )
 
         spySplitAnimationController.playSplitLaunchAnimation(
@@ -230,7 +230,7 @@
             null /* info */,
             null /* t */,
             {} /* finishCallback */,
-            1f /* cornerRadius */
+            1f, /* cornerRadius */
         )
 
         verify(spySplitAnimationController)
@@ -243,7 +243,7 @@
                 any(),
                 any(),
                 any(),
-                any()
+                any(),
             )
     }
 
@@ -267,7 +267,7 @@
             transitionInfo,
             transaction,
             {} /* finishCallback */,
-            1f /* cornerRadius */
+            1f, /* cornerRadius */
         )
 
         verify(spySplitAnimationController)
@@ -296,7 +296,7 @@
             transitionInfo,
             transaction,
             {} /* finishCallback */,
-            1f /* cornerRadius */
+            1f, /* cornerRadius */
         )
 
         verify(spySplitAnimationController)
@@ -325,7 +325,7 @@
             transitionInfo,
             transaction,
             {} /* finishCallback */,
-            1f /* cornerRadius */
+            1f, /* cornerRadius */
         )
 
         verify(spySplitAnimationController)
@@ -353,7 +353,7 @@
             transitionInfo,
             transaction,
             {} /* finishCallback */,
-            1f /* cornerRadius */
+            1f, /* cornerRadius */
         )
 
         verify(spySplitAnimationController)
@@ -381,7 +381,7 @@
             transitionInfo,
             transaction,
             {} /* finishCallback */,
-            1f /* cornerRadius */
+            1f, /* cornerRadius */
         )
 
         verify(spySplitAnimationController)
@@ -408,7 +408,7 @@
             transitionInfo,
             transaction,
             {} /* finishCallback */,
-            1f /* cornerRadius */
+            1f, /* cornerRadius */
         )
 
         verify(spySplitAnimationController)
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskGridNavHelperTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskGridNavHelperTest.kt
index f2fa0c5..cb088fd 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskGridNavHelperTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskGridNavHelperTest.kt
@@ -16,13 +16,13 @@
 package com.android.quickstep.util
 
 import com.android.launcher3.util.IntArray
-import com.android.quickstep.util.TaskGridNavHelper.ADD_DESK_PLACEHOLDER_ID
-import com.android.quickstep.util.TaskGridNavHelper.CLEAR_ALL_PLACEHOLDER_ID
-import com.android.quickstep.util.TaskGridNavHelper.DIRECTION_DOWN
-import com.android.quickstep.util.TaskGridNavHelper.DIRECTION_LEFT
-import com.android.quickstep.util.TaskGridNavHelper.DIRECTION_RIGHT
-import com.android.quickstep.util.TaskGridNavHelper.DIRECTION_TAB
-import com.android.quickstep.util.TaskGridNavHelper.DIRECTION_UP
+import com.android.quickstep.util.TaskGridNavHelper.Companion.ADD_DESK_PLACEHOLDER_ID
+import com.android.quickstep.util.TaskGridNavHelper.Companion.CLEAR_ALL_PLACEHOLDER_ID
+import com.android.quickstep.util.TaskGridNavHelper.TaskNavDirection.DOWN
+import com.android.quickstep.util.TaskGridNavHelper.TaskNavDirection.LEFT
+import com.android.quickstep.util.TaskGridNavHelper.TaskNavDirection.RIGHT
+import com.android.quickstep.util.TaskGridNavHelper.TaskNavDirection.TAB
+import com.android.quickstep.util.TaskGridNavHelper.TaskNavDirection.UP
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
 
@@ -35,8 +35,7 @@
     */
     @Test
     fun equalLengthRows_noFocused_onTop_pressDown_goesToBottom() {
-        assertThat(getNextGridPage(currentPageTaskViewId = 1, DIRECTION_DOWN, delta = 1))
-            .isEqualTo(2)
+        assertThat(getNextGridPage(currentPageTaskViewId = 1, DOWN, delta = 1)).isEqualTo(2)
     }
 
     /*                      ↑----→
@@ -46,7 +45,7 @@
     */
     @Test
     fun equalLengthRows_noFocused_onTop_pressUp_goesToBottom() {
-        assertThat(getNextGridPage(currentPageTaskViewId = 1, DIRECTION_UP, delta = 1)).isEqualTo(2)
+        assertThat(getNextGridPage(currentPageTaskViewId = 1, UP, delta = 1)).isEqualTo(2)
     }
 
     /*                      ↓----↑
@@ -57,8 +56,7 @@
     */
     @Test
     fun equalLengthRows_noFocused_onBottom_pressDown_goesToTop() {
-        assertThat(getNextGridPage(currentPageTaskViewId = 2, DIRECTION_DOWN, delta = 1))
-            .isEqualTo(1)
+        assertThat(getNextGridPage(currentPageTaskViewId = 2, DOWN, delta = 1)).isEqualTo(1)
     }
 
     /*
@@ -68,7 +66,7 @@
     */
     @Test
     fun equalLengthRows_noFocused_onBottom_pressUp_goesToTop() {
-        assertThat(getNextGridPage(currentPageTaskViewId = 2, DIRECTION_UP, delta = 1)).isEqualTo(1)
+        assertThat(getNextGridPage(currentPageTaskViewId = 2, UP, delta = 1)).isEqualTo(1)
     }
 
     /*
@@ -78,8 +76,7 @@
     */
     @Test
     fun equalLengthRows_noFocused_onTop_pressLeft_goesLeft() {
-        assertThat(getNextGridPage(currentPageTaskViewId = 1, DIRECTION_LEFT, delta = 1))
-            .isEqualTo(3)
+        assertThat(getNextGridPage(currentPageTaskViewId = 1, LEFT, delta = 1)).isEqualTo(3)
     }
 
     /*
@@ -89,8 +86,7 @@
     */
     @Test
     fun equalLengthRows_noFocused_onBottom_pressLeft_goesLeft() {
-        assertThat(getNextGridPage(currentPageTaskViewId = 2, DIRECTION_LEFT, delta = 1))
-            .isEqualTo(4)
+        assertThat(getNextGridPage(currentPageTaskViewId = 2, LEFT, delta = 1)).isEqualTo(4)
     }
 
     /*
@@ -100,8 +96,7 @@
     */
     @Test
     fun equalLengthRows_noFocused_onTop_secondItem_pressRight_goesRight() {
-        assertThat(getNextGridPage(currentPageTaskViewId = 3, DIRECTION_RIGHT, delta = -1))
-            .isEqualTo(1)
+        assertThat(getNextGridPage(currentPageTaskViewId = 3, RIGHT, delta = -1)).isEqualTo(1)
     }
 
     /*
@@ -111,8 +106,7 @@
     */
     @Test
     fun equalLengthRows_noFocused_onBottom_secondItem_pressRight_goesRight() {
-        assertThat(getNextGridPage(currentPageTaskViewId = 4, DIRECTION_RIGHT, delta = -1))
-            .isEqualTo(2)
+        assertThat(getNextGridPage(currentPageTaskViewId = 4, RIGHT, delta = -1)).isEqualTo(2)
     }
 
     /*
@@ -124,7 +118,7 @@
     */
     @Test
     fun equalLengthRows_noFocused_onTop_pressRight_cycleToClearAll() {
-        assertThat(getNextGridPage(currentPageTaskViewId = 1, DIRECTION_RIGHT, delta = -1))
+        assertThat(getNextGridPage(currentPageTaskViewId = 1, RIGHT, delta = -1))
             .isEqualTo(CLEAR_ALL_PLACEHOLDER_ID)
     }
 
@@ -137,7 +131,7 @@
     */
     @Test
     fun equalLengthRows_noFocused_onBottom_pressRight_cycleToClearAll() {
-        assertThat(getNextGridPage(currentPageTaskViewId = 2, DIRECTION_RIGHT, delta = -1))
+        assertThat(getNextGridPage(currentPageTaskViewId = 2, RIGHT, delta = -1))
             .isEqualTo(CLEAR_ALL_PLACEHOLDER_ID)
     }
 
@@ -149,7 +143,7 @@
     */
     @Test
     fun equalLengthRows_noFocused_onTop_lastItem_pressLeft_toClearAll() {
-        assertThat(getNextGridPage(currentPageTaskViewId = 5, DIRECTION_LEFT, delta = 1))
+        assertThat(getNextGridPage(currentPageTaskViewId = 5, LEFT, delta = 1))
             .isEqualTo(CLEAR_ALL_PLACEHOLDER_ID)
     }
 
@@ -161,7 +155,7 @@
     */
     @Test
     fun equalLengthRows_noFocused_onBottom_lastItem_pressLeft_toClearAll() {
-        assertThat(getNextGridPage(currentPageTaskViewId = 6, DIRECTION_LEFT, delta = 1))
+        assertThat(getNextGridPage(currentPageTaskViewId = 6, LEFT, delta = 1))
             .isEqualTo(CLEAR_ALL_PLACEHOLDER_ID)
     }
 
@@ -176,11 +170,7 @@
     @Test
     fun equalLengthRows_noFocused_onClearAll_pressLeft_cycleToFirst() {
         assertThat(
-                getNextGridPage(
-                    currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID,
-                    DIRECTION_LEFT,
-                    delta = 1,
-                )
+                getNextGridPage(currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID, LEFT, delta = 1)
             )
             .isEqualTo(1)
     }
@@ -194,11 +184,7 @@
     @Test
     fun equalLengthRows_noFocused_onClearAll_pressRight_toLastInBottom() {
         assertThat(
-                getNextGridPage(
-                    currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID,
-                    DIRECTION_RIGHT,
-                    delta = -1,
-                )
+                getNextGridPage(currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID, RIGHT, delta = -1)
             )
             .isEqualTo(6)
     }
@@ -214,7 +200,7 @@
         assertThat(
                 getNextGridPage(
                     currentPageTaskViewId = FOCUSED_TASK_ID,
-                    DIRECTION_LEFT,
+                    LEFT,
                     delta = 1,
                     largeTileIds = listOf(FOCUSED_TASK_ID),
                 )
@@ -233,7 +219,7 @@
         assertThat(
                 getNextGridPage(
                     currentPageTaskViewId = FOCUSED_TASK_ID,
-                    DIRECTION_UP,
+                    UP,
                     delta = 1,
                     largeTileIds = listOf(FOCUSED_TASK_ID),
                 )
@@ -254,7 +240,7 @@
         assertThat(
                 getNextGridPage(
                     currentPageTaskViewId = FOCUSED_TASK_ID,
-                    DIRECTION_DOWN,
+                    DOWN,
                     delta = 1,
                     largeTileIds = listOf(FOCUSED_TASK_ID),
                 )
@@ -275,7 +261,7 @@
         assertThat(
                 getNextGridPage(
                     currentPageTaskViewId = FOCUSED_TASK_ID,
-                    DIRECTION_RIGHT,
+                    RIGHT,
                     delta = -1,
                     largeTileIds = listOf(FOCUSED_TASK_ID),
                 )
@@ -297,7 +283,7 @@
         assertThat(
                 getNextGridPage(
                     currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID,
-                    DIRECTION_LEFT,
+                    LEFT,
                     delta = 1,
                     largeTileIds = listOf(FOCUSED_TASK_ID),
                 )
@@ -316,7 +302,7 @@
         assertThat(
                 getNextGridPage(
                     currentPageTaskViewId = 7,
-                    DIRECTION_DOWN,
+                    DOWN,
                     delta = 1,
                     topIds = IntArray.wrap(1, 3, 5, 7),
                 )
@@ -335,7 +321,7 @@
         assertThat(
                 getNextGridPage(
                     /* topIds = */ currentPageTaskViewId = 7,
-                    DIRECTION_UP,
+                    UP,
                     delta = 1,
                     topIds = IntArray.wrap(1, 3, 5, 7),
                 )
@@ -353,7 +339,7 @@
         assertThat(
                 getNextGridPage(
                     /* topIds = */ currentPageTaskViewId = 6,
-                    DIRECTION_LEFT,
+                    LEFT,
                     delta = 1,
                     topIds = IntArray.wrap(1, 3, 5, 7),
                 )
@@ -372,7 +358,7 @@
         assertThat(
                 getNextGridPage(
                     /* topIds = */ currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID,
-                    DIRECTION_RIGHT,
+                    RIGHT,
                     delta = -1,
                     topIds = IntArray.wrap(1, 3, 5, 7),
                 )
@@ -391,7 +377,7 @@
         assertThat(
                 getNextGridPage(
                     currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID,
-                    DIRECTION_RIGHT,
+                    RIGHT,
                     delta = -1,
                     bottomIds = IntArray.wrap(2, 4, 6, 7),
                 )
@@ -406,8 +392,7 @@
     */
     @Test
     fun equalLengthRows_noFocused_onTop_pressTab_goesToBottom() {
-        assertThat(getNextGridPage(currentPageTaskViewId = 1, DIRECTION_TAB, delta = 1))
-            .isEqualTo(2)
+        assertThat(getNextGridPage(currentPageTaskViewId = 1, TAB, delta = 1)).isEqualTo(2)
     }
 
     /*
@@ -418,8 +403,7 @@
     */
     @Test
     fun equalLengthRows_noFocused_onBottom_pressTab_goesToNextTop() {
-        assertThat(getNextGridPage(currentPageTaskViewId = 2, DIRECTION_TAB, delta = 1))
-            .isEqualTo(3)
+        assertThat(getNextGridPage(currentPageTaskViewId = 2, TAB, delta = 1)).isEqualTo(3)
     }
 
     /*
@@ -431,8 +415,7 @@
     */
     @Test
     fun equalLengthRows_noFocused_onTop_pressTabWithShift_goesToPreviousBottom() {
-        assertThat(getNextGridPage(currentPageTaskViewId = 3, DIRECTION_TAB, delta = -1))
-            .isEqualTo(2)
+        assertThat(getNextGridPage(currentPageTaskViewId = 3, TAB, delta = -1)).isEqualTo(2)
     }
 
     /*
@@ -442,8 +425,7 @@
     */
     @Test
     fun equalLengthRows_noFocused_onBottom_pressTabWithShift_goesToTop() {
-        assertThat(getNextGridPage(currentPageTaskViewId = 2, DIRECTION_TAB, delta = -1))
-            .isEqualTo(1)
+        assertThat(getNextGridPage(currentPageTaskViewId = 2, TAB, delta = -1)).isEqualTo(1)
     }
 
     /*
@@ -453,9 +435,7 @@
     */
     @Test
     fun equalLengthRows_noFocused_onTop_pressTabWithShift_noCycle_staysOnTop() {
-        assertThat(
-                getNextGridPage(currentPageTaskViewId = 1, DIRECTION_TAB, delta = -1, cycle = false)
-            )
+        assertThat(getNextGridPage(currentPageTaskViewId = 1, TAB, delta = -1, cycle = false))
             .isEqualTo(1)
     }
 
@@ -469,7 +449,7 @@
         assertThat(
                 getNextGridPage(
                     currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID,
-                    DIRECTION_TAB,
+                    TAB,
                     delta = 1,
                     cycle = false,
                 )
@@ -487,7 +467,7 @@
         assertThat(
                 getNextGridPage(
                     currentPageTaskViewId = DESKTOP_TASK_ID,
-                    DIRECTION_LEFT,
+                    LEFT,
                     delta = 1,
                     largeTileIds = listOf(DESKTOP_TASK_ID, FOCUSED_TASK_ID),
                 )
@@ -505,7 +485,7 @@
         assertThat(
                 getNextGridPage(
                     currentPageTaskViewId = FOCUSED_TASK_ID,
-                    DIRECTION_RIGHT,
+                    RIGHT,
                     delta = -1,
                     largeTileIds = listOf(DESKTOP_TASK_ID, FOCUSED_TASK_ID),
                 )
@@ -525,7 +505,7 @@
         assertThat(
                 getNextGridPage(
                     currentPageTaskViewId = DESKTOP_TASK_ID,
-                    DIRECTION_RIGHT,
+                    RIGHT,
                     delta = -1,
                     largeTileIds = listOf(DESKTOP_TASK_ID, FOCUSED_TASK_ID),
                 )
@@ -546,7 +526,7 @@
         assertThat(
                 getNextGridPage(
                     currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID,
-                    DIRECTION_LEFT,
+                    LEFT,
                     delta = 1,
                     largeTileIds = listOf(DESKTOP_TASK_ID, FOCUSED_TASK_ID),
                 )
@@ -565,7 +545,7 @@
         assertThat(
                 getNextGridPage(
                     currentPageTaskViewId = 2,
-                    DIRECTION_RIGHT,
+                    RIGHT,
                     delta = -1,
                     largeTileIds = listOf(DESKTOP_TASK_ID, FOCUSED_TASK_ID),
                 )
@@ -584,7 +564,7 @@
         assertThat(
                 getNextGridPage(
                     currentPageTaskViewId = 1,
-                    DIRECTION_RIGHT,
+                    RIGHT,
                     delta = -1,
                     largeTileIds = listOf(DESKTOP_TASK_ID, FOCUSED_TASK_ID),
                 )
@@ -603,7 +583,7 @@
         assertThat(
                 getNextGridPage(
                     currentPageTaskViewId = DESKTOP_TASK_ID,
-                    DIRECTION_TAB,
+                    TAB,
                     delta = 1,
                     largeTileIds = listOf(DESKTOP_TASK_ID, FOCUSED_TASK_ID),
                 )
@@ -621,7 +601,7 @@
         assertThat(
                 getNextGridPage(
                     currentPageTaskViewId = FOCUSED_TASK_ID,
-                    DIRECTION_LEFT,
+                    LEFT,
                     delta = 1,
                     topIds = IntArray(),
                     bottomIds = IntArray.wrap(2),
@@ -643,7 +623,7 @@
         assertThat(
                 getNextGridPage(
                     currentPageTaskViewId = DESKTOP_TASK_ID,
-                    DIRECTION_TAB,
+                    TAB,
                     delta = -1,
                     largeTileIds = listOf(DESKTOP_TASK_ID, FOCUSED_TASK_ID),
                 )
@@ -662,7 +642,7 @@
         assertThat(
                 getNextGridPage(
                     currentPageTaskViewId = 1,
-                    DIRECTION_RIGHT,
+                    RIGHT,
                     delta = -1,
                     hasAddDesktopButton = true,
                 )
@@ -681,7 +661,7 @@
         assertThat(
                 getNextGridPage(
                     currentPageTaskViewId = 2,
-                    DIRECTION_RIGHT,
+                    RIGHT,
                     delta = -1,
                     hasAddDesktopButton = true,
                 )
@@ -701,7 +681,7 @@
         assertThat(
                 getNextGridPage(
                     currentPageTaskViewId = ADD_DESK_PLACEHOLDER_ID,
-                    DIRECTION_RIGHT,
+                    RIGHT,
                     delta = -1,
                     hasAddDesktopButton = true,
                 )
@@ -722,7 +702,7 @@
         assertThat(
                 getNextGridPage(
                     currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID,
-                    DIRECTION_LEFT,
+                    LEFT,
                     delta = 1,
                     hasAddDesktopButton = true,
                 )
@@ -741,7 +721,7 @@
         assertThat(
                 getNextGridPage(
                     currentPageTaskViewId = ADD_DESK_PLACEHOLDER_ID,
-                    DIRECTION_UP,
+                    UP,
                     delta = 1,
                     hasAddDesktopButton = true,
                 )
@@ -760,7 +740,7 @@
         assertThat(
                 getNextGridPage(
                     currentPageTaskViewId = ADD_DESK_PLACEHOLDER_ID,
-                    DIRECTION_DOWN,
+                    DOWN,
                     delta = 1,
                     hasAddDesktopButton = true,
                 )
@@ -778,7 +758,7 @@
         assertThat(
                 getNextGridPage(
                     currentPageTaskViewId = ADD_DESK_PLACEHOLDER_ID,
-                    DIRECTION_LEFT,
+                    LEFT,
                     delta = 1,
                     largeTileIds = listOf(DESKTOP_TASK_ID),
                     hasAddDesktopButton = true,
@@ -797,7 +777,7 @@
         assertThat(
                 getNextGridPage(
                     currentPageTaskViewId = DESKTOP_TASK_ID,
-                    DIRECTION_RIGHT,
+                    RIGHT,
                     delta = -1,
                     largeTileIds = listOf(DESKTOP_TASK_ID),
                     hasAddDesktopButton = true,
@@ -806,9 +786,43 @@
             .isEqualTo(ADD_DESK_PLACEHOLDER_ID)
     }
 
+    // Col offset:  0   1   2
+    //             -----------
+    // ID grid:     4   2   0  start
+    //         end [5]  3   1
+    @Test
+    fun gridTaskViewIdOffsetPairInTabOrderSequence_towardsStart() {
+        val expected = listOf(Pair(4, 0), Pair(3, 1), Pair(2, 1), Pair(1, 2), Pair(0, 2))
+        assertThat(
+                gridTaskViewIdOffsetPairInTabOrderSequence(
+                        initialTaskViewId = 5,
+                        towardsStart = true,
+                    )
+                    .toList()
+            )
+            .isEqualTo(expected)
+    }
+
+    // Col offset:  2   1   0
+    //             -----------
+    // ID grid:     4   2  [0] start
+    //          end 5   3   1
+    @Test
+    fun gridTaskViewIdOffsetPairInTabOrderSequence_towardsEnd() {
+        val expected = listOf(Pair(1, 0), Pair(2, 1), Pair(3, 1), Pair(4, 2), Pair(5, 2))
+        assertThat(
+                gridTaskViewIdOffsetPairInTabOrderSequence(
+                        initialTaskViewId = 0,
+                        towardsStart = false,
+                    )
+                    .toList()
+            )
+            .isEqualTo(expected)
+    }
+
     private fun getNextGridPage(
         currentPageTaskViewId: Int,
-        direction: Int,
+        direction: TaskGridNavHelper.TaskNavDirection,
         delta: Int,
         topIds: IntArray = IntArray.wrap(1, 3, 5),
         bottomIds: IntArray = IntArray.wrap(2, 4, 6),
@@ -821,6 +835,22 @@
         return taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle)
     }
 
+    private fun gridTaskViewIdOffsetPairInTabOrderSequence(
+        initialTaskViewId: Int,
+        towardsStart: Boolean,
+        topIds: IntArray = IntArray.wrap(0, 2, 4),
+        bottomIds: IntArray = IntArray.wrap(1, 3, 5),
+        largeTileIds: List<Int> = emptyList(),
+        hasAddDesktopButton: Boolean = false,
+    ): Sequence<Pair<Int, Int>> {
+        val taskGridNavHelper =
+            TaskGridNavHelper(topIds, bottomIds, largeTileIds, hasAddDesktopButton)
+        return taskGridNavHelper.gridTaskViewIdOffsetPairInTabOrderSequence(
+            initialTaskViewId,
+            towardsStart,
+        )
+    }
+
     private companion object {
         const val FOCUSED_TASK_ID = 99
         const val DESKTOP_TASK_ID = 100
diff --git a/quickstep/tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java b/quickstep/tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java
index fa2eb1e..d52d054 100644
--- a/quickstep/tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java
+++ b/quickstep/tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java
@@ -91,6 +91,7 @@
     private AppWidgetProviderInfo mApp4Provider1;
     private AppWidgetProviderInfo mApp4Provider2;
     private AppWidgetProviderInfo mApp5Provider1;
+    private AppWidgetProviderInfo mApp6PinOnlyProvider1;
     private List<AppWidgetProviderInfo> allWidgets;
 
     private FakeBgDataModelCallback mCallback = new FakeBgDataModelCallback();
@@ -117,8 +118,14 @@
                 ComponentName.createRelative("app4", ".provider2"));
         mApp5Provider1 = createAppWidgetProviderInfo(
                 ComponentName.createRelative("app5", "provider1"));
+        mApp6PinOnlyProvider1 = createAppWidgetProviderInfo(
+                ComponentName.createRelative("app6", "provider1"),
+                /*hideFromPicker=*/ true
+        );
+
+
         allWidgets = Arrays.asList(mApp1Provider1, mApp1Provider2, mApp2Provider1,
-                mApp4Provider1, mApp4Provider2, mApp5Provider1);
+                mApp4Provider1, mApp4Provider2, mApp5Provider1, mApp6PinOnlyProvider1);
 
         mLauncherApps = mModelHelper.sandboxContext.spyService(LauncherApps.class);
         doAnswer(i -> {
@@ -270,6 +277,32 @@
         });
     }
 
+    @Test
+    public void widgetsRecommendations_excludesWidgetsHiddenForPicker() {
+        runOnExecutorSync(MODEL_EXECUTOR, () -> {
+
+            // Not installed widget - hence eligible
+            AppTarget widget1 = new AppTarget(new AppTargetId("app1"), "app1", "provider1",
+                    mUserHandle);
+            // Provider marked as hidden from picker - hence not eligible
+            AppTarget widget6 = new AppTarget(new AppTargetId("app6"), "app6", "provider1",
+                    mUserHandle);
+
+            mCallback.mRecommendedWidgets = null;
+            mModelHelper.getModel().enqueueModelUpdateTask(
+                    newWidgetsPredicationTask(List.of(widget1, widget6)));
+            runOnExecutorSync(MAIN_EXECUTOR, () -> { });
+
+            // Only widget 1 (and no widget 6 as its meant to be hidden from picker).
+            List<PendingAddWidgetInfo> recommendedWidgets = mCallback.mRecommendedWidgets.items
+                    .stream()
+                    .map(itemInfo -> (PendingAddWidgetInfo) itemInfo)
+                    .collect(Collectors.toList());
+            assertThat(recommendedWidgets).hasSize(1);
+            assertThat(recommendedWidgets.get(0).componentName.getPackageName()).isEqualTo("app1");
+        });
+    }
+
     private void assertWidgetInfo(
             LauncherAppWidgetProviderInfo actual, AppWidgetProviderInfo expected) {
         assertThat(actual.provider).isEqualTo(expected.provider);
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/DesktopSystemShortcutTest.kt b/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt
index 76aab39..a523e02 100644
--- a/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt
+++ b/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt
@@ -20,6 +20,7 @@
 import android.content.Context
 import android.content.Intent
 import android.content.pm.PackageManager
+import android.platform.test.annotations.DisableFlags
 import android.platform.test.annotations.EnableFlags
 import android.view.Display.DEFAULT_DISPLAY
 import androidx.test.platform.app.InstrumentationRegistry
@@ -106,6 +107,7 @@
 
     @Test
     @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY)
+    @DisableFlags(Flags.FLAG_ENABLE_MODALS_FULLSCREEN_WITH_PERMISSION)
     fun createDesktopTaskShortcutFactory_transparentTask() {
         val baseComponent = ComponentName("", /* class */ "")
         val taskKey =
diff --git a/quickstep/tests/src/com/android/quickstep/ExternalDisplaySystemShortcutTest.kt b/quickstep/tests/src/com/android/quickstep/ExternalDisplaySystemShortcutTest.kt
index 818841a..2db94f6 100644
--- a/quickstep/tests/src/com/android/quickstep/ExternalDisplaySystemShortcutTest.kt
+++ b/quickstep/tests/src/com/android/quickstep/ExternalDisplaySystemShortcutTest.kt
@@ -20,6 +20,7 @@
 import android.content.Context
 import android.content.Intent
 import android.content.pm.PackageManager
+import android.platform.test.annotations.DisableFlags
 import android.platform.test.annotations.EnableFlags
 import android.platform.test.flag.junit.SetFlagsRule
 import android.view.Display.DEFAULT_DISPLAY
@@ -113,6 +114,7 @@
         Flags.FLAG_MOVE_TO_EXTERNAL_DISPLAY_SHORTCUT,
         Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY,
     )
+    @DisableFlags(Flags.FLAG_ENABLE_MODALS_FULLSCREEN_WITH_PERMISSION)
     fun createExternalDisplayTaskShortcut_transparentTask() {
         val baseComponent = ComponentName("", /* class */ "")
         val taskKey =
diff --git a/quickstep/tests/src/com/android/quickstep/OrientationTouchTransformerTest.java b/quickstep/tests/src/com/android/quickstep/OrientationTouchTransformerTest.java
index a0ec635..154d86d 100644
--- a/quickstep/tests/src/com/android/quickstep/OrientationTouchTransformerTest.java
+++ b/quickstep/tests/src/com/android/quickstep/OrientationTouchTransformerTest.java
@@ -300,7 +300,7 @@
     @Test
     public void testSimpleOrientationTouchTransformer() {
         final DisplayController displayController = mock(DisplayController.class);
-        doReturn(mInfo).when(displayController).getInfo();
+        doReturn(mInfo).when(displayController).getInfoForDisplay(anyInt());
         final SimpleOrientationTouchTransformer transformer =
                 new SimpleOrientationTouchTransformer(getApplicationContext(), displayController,
                         mock(DaggerSingletonTracker.class));
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
index 79d3c19..1c87bce 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
@@ -23,6 +23,7 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeFalse;
@@ -50,6 +51,7 @@
 import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
 import com.android.launcher3.util.TestUtil;
 import com.android.launcher3.util.Wait;
+import com.android.launcher3.util.rule.ScreenRecordRule;
 import com.android.launcher3.util.rule.TestStabilityRule;
 import com.android.quickstep.NavigationModeSwitchRule.NavigationModeSwitch;
 import com.android.quickstep.TaskbarModeSwitchRule.TaskbarModeSwitch;
@@ -62,6 +64,7 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.Comparator;
 import java.util.function.Consumer;
 import java.util.function.Function;
 
@@ -541,6 +544,7 @@
 
     @Test
     @PortraitLandscape
+    @ScreenRecordRule.ScreenRecord // TODO(b/396447643): Remove screen record.
     public void testDismissCancel() throws Exception {
         startTestAppsWithCheck();
         Overview overview = mLauncher.goHome().switchToOverview();
@@ -557,6 +561,60 @@
                 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()));
+    }
+
+    @Test
+    @PortraitLandscape
+    public void testDismissLastGridRow() throws Exception {
+        assumeTrue(mLauncher.isTablet());
+        mLauncher.goHome().switchToOverview().dismissAllTasks();
+        startTestAppsWithCheck();
+        startTestActivity(3);
+        startTestActivity(4);
+        runOnRecentsView(
+                recentsView -> assertNotEquals("Grid overview should have unequal row counts",
+                        recentsView.getTopRowTaskCountForTablet(),
+                        recentsView.getBottomRowTaskCountForTablet()));
+        Overview overview = mLauncher.goHome().switchToOverview();
+        assertIsInState("Launcher internal state didn't switch to Overview",
+                ExpectedState.OVERVIEW);
+        overview.flingForwardUntilClearAllVisible();
+        assertTrue("Clear All not visible.", overview.isClearAllVisible());
+        final Integer numTasks = getFromRecentsView(RecentsView::getTaskViewCount);
+        OverviewTask lastGridTask = overview.getCurrentTasksForTablet().stream().min(
+                Comparator.comparingInt(OverviewTask::getTaskCenterX)).get();
+        assertNotNull("lastGridTask null.", lastGridTask);
+
+        lastGridTask.dismiss();
+
+        runOnRecentsView(recentsView -> assertEquals(
+                "Dismissing a lastGridTask didn't remove 1 lastGridTask from Overview",
+                numTasks - 1, recentsView.getTaskViewCount()));
+        runOnRecentsView(recentsView -> assertEquals("Grid overview should have equal row counts.",
+                recentsView.getTopRowTaskCountForTablet(),
+                recentsView.getBottomRowTaskCountForTablet()));
+        assertTrue("Clear All not visible.", overview.isClearAllVisible());
+    }
+
     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/drawable/private_space_install_app_icon.xml b/res/drawable/private_space_install_app_icon.xml
index cfec2b1..1e7fe43 100644
--- a/res/drawable/private_space_install_app_icon.xml
+++ b/res/drawable/private_space_install_app_icon.xml
@@ -13,19 +13,7 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="60dp"
-    android:height="60dp"
-    android:viewportWidth="60"
-    android:viewportHeight="60">
-    <group>
-        <clip-path
-            android:pathData="M30 0H30A30 30 0 0 1 60 30V30A30 30 0 0 1 30 60H30A30 30 0 0 1 0 30V30A30 30 0 0 1 30 0Z" />
-        <path
-            android:pathData="M30 0H30A30 30 0 0 1 60 30V30A30 30 0 0 1 30 60H30A30 30 0 0 1 0 30V30A30 30 0 0 1 30 0Z"
-            android:fillColor="@color/material_color_surface_container_lowest" />
-        <path
-            android:pathData="M29 31h-6v-2h6v-6h2v6h6v2h-6v6h-2v-6Z"
-            android:fillColor="@color/material_color_on_surface" />
-    </group>
-</vector>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@color/material_color_surface_container_lowest"/>
+    <foreground android:drawable="@drawable/private_space_install_app_icon_foreground" />
+</adaptive-icon>
diff --git a/res/drawable/private_space_install_app_icon_foreground.xml b/res/drawable/private_space_install_app_icon_foreground.xml
new file mode 100644
index 0000000..d55abe7
--- /dev/null
+++ b/res/drawable/private_space_install_app_icon_foreground.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ 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.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="60dp"
+    android:height="60dp"
+    android:viewportWidth="60"
+    android:viewportHeight="60">
+    <path
+        android:pathData="M29 31h-6v-2h6v-6h2v6h6v2h-6v6h-2v-6Z"
+        android:fillColor="@color/material_color_on_surface" />
+</vector>
diff --git a/res/values-bg/strings.xml b/res/values-bg/strings.xml
index 6d44a97..98571add 100644
--- a/res/values-bg/strings.xml
+++ b/res/values-bg/strings.xml
@@ -62,7 +62,7 @@
     <string name="widget_button_text" msgid="2880537293434387943">"Приспособления"</string>
     <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_widgets_available" msgid="4337693382501046170">"Няма налични преки пътища и приспособления"</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>
diff --git a/res/values-bs/strings.xml b/res/values-bs/strings.xml
index ff8a541..c0568bf 100644
--- a/res/values-bs/strings.xml
+++ b/res/values-bs/strings.xml
@@ -41,7 +41,7 @@
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Dodirnite i zadržite da pomjerite vidžet."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Dvaput dodirnite i zadržite da pomjerite vidžet ili da koristite prilagođene radnje."</string>
     <string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"Više opcija"</string>
-    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Prikazuj sve vidžete"</string>
+    <string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"Prikaži sve vidžete"</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"Širina %1$d, visina %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Vidžet <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
diff --git a/res/values-cs/strings.xml b/res/values-cs/strings.xml
index 2ac7233..b9f0ba2 100644
--- a/res/values-cs/strings.xml
+++ b/res/values-cs/strings.xml
@@ -46,12 +46,12 @@
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"šířka %1$d, výška %2$d"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> widget"</string>
     <string name="widget_preview_name_and_dims_content_description" msgid="8489038126122831595">"Widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>, šířka %2$d, výška %3$d"</string>
-    <string name="add_item_request_drag_hint" msgid="8730547755622776606">"Pokud chcete widgetem pohybovat po ploše, podržte ho"</string>
+    <string name="add_item_request_drag_hint" msgid="8730547755622776606">"Pokud chcete widgetem pohybovat po ploše, podržte ho."</string>
     <string name="add_to_home_screen" msgid="9168649446635919791">"Přidat na plochu"</string>
     <string name="added_to_home_screen_accessibility_text" msgid="4451545765448884415">"Widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g> byl přidán na plochu"</string>
     <string name="suggested_widgets_header_title" msgid="1844314680798145222">"Návrhy"</string>
     <string name="productivity_widget_recommendation_category_label" msgid="3811812719618323750">"Nejdůležitější aplikace"</string>
-    <string name="news_widget_recommendation_category_label" msgid="6756167867113741310">"Zprávy a časopisy"</string>
+    <string name="news_widget_recommendation_category_label" msgid="6756167867113741310">"Noviny a časopisy"</string>
     <string name="entertainment_widget_recommendation_category_label" msgid="3973107268630717874">"Zábava"</string>
     <string name="social_widget_recommendation_category_label" msgid="689147679536384717">"Sociální sítě"</string>
     <string name="others_widget_recommendation_category_label" msgid="5555987036267226245">"Návrhy pro vás"</string>
@@ -127,7 +127,7 @@
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"Složka: <xliff:g id="NAME">%1$s</xliff:g>, počet položek: <xliff:g id="SIZE">%2$d</xliff:g> nebo více"</string>
     <string name="unnamed_folder" msgid="2420192029474044442">"Nepojmenovaná složka"</string>
     <string name="app_pair_name_format" msgid="8134106404716224054">"Dvojice aplikací: <xliff:g id="APP1">%1$s</xliff:g> a <xliff:g id="APP2">%2$s</xliff:g>"</string>
-    <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Tapeta a styl"</string>
+    <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Tapety a styl"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"Upravit plochu"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"Nastavení plochy"</string>
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Zakázáno administrátorem"</string>
diff --git a/res/values-fa/strings.xml b/res/values-fa/strings.xml
index c45d372..abaaa89 100644
--- a/res/values-fa/strings.xml
+++ b/res/values-fa/strings.xml
@@ -64,8 +64,8 @@
     <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="widgets_full_sheet_personal_tab" msgid="2743540105607120182">"ابزاره‌های شخصی"</string>
-    <string name="widgets_full_sheet_work_tab" msgid="3767150027110633765">"ابزاره‌های کاری"</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>
     <string name="widget_category_note_taking" msgid="3469689394504266039">"یادداشت‌برداری"</string>
     <string name="widget_cell_tap_to_show_add_button_label" msgid="4354194214317043581">"نشان دادن دکمه افزودن"</string>
diff --git a/res/values-gu/strings.xml b/res/values-gu/strings.xml
index 4925294..5a33983 100644
--- a/res/values-gu/strings.xml
+++ b/res/values-gu/strings.xml
@@ -60,7 +60,7 @@
     <string name="shortcuts_count" msgid="8471715556199592381">"{count,plural, =1{# શૉર્ટકટ}one{# શૉર્ટકટ}other{# શૉર્ટકટ}}"</string>
     <string name="widgets_and_shortcuts_count" msgid="7209136747878365116">"<xliff:g id="WIDGETS_COUNT">%1$s</xliff:g>, <xliff:g id="SHORTCUTS_COUNT">%2$s</xliff:g>"</string>
     <string name="widget_button_text" msgid="2880537293434387943">"વિજેટ"</string>
-    <string name="widgets_full_sheet_search_bar_hint" msgid="8484659090860596457">"શોધ"</string>
+    <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>
diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml
index 75e7cd2..3145133 100644
--- a/res/values-it/strings.xml
+++ b/res/values-it/strings.xml
@@ -46,7 +46,7 @@
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d di larghezza per %2$d di altezza"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"Widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
     <string name="widget_preview_name_and_dims_content_description" msgid="8489038126122831595">"Widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>, %2$d di larghezza per %3$d di altezza"</string>
-    <string name="add_item_request_drag_hint" msgid="8730547755622776606">"Tocca e tieni premuto il widget per spostarlo nella schermata Home"</string>
+    <string name="add_item_request_drag_hint" msgid="8730547755622776606">"Tieni premuto il widget per spostarlo nella schermata Home"</string>
     <string name="add_to_home_screen" msgid="9168649446635919791">"Aggiungi alla schermata Home"</string>
     <string name="added_to_home_screen_accessibility_text" msgid="4451545765448884415">"Widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g> aggiunto alla schermata Home"</string>
     <string name="suggested_widgets_header_title" msgid="1844314680798145222">"Suggerimenti"</string>
diff --git a/res/values-ky/strings.xml b/res/values-ky/strings.xml
index b6b5b66..b98b61d 100644
--- a/res/values-ky/strings.xml
+++ b/res/values-ky/strings.xml
@@ -46,7 +46,7 @@
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"Туурасы: %1$d, бийиктиги: %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">"Башкы экранга жылдыруу үчүн виджетти коё бербей басып туруңуз"</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>
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..3eb680f 100644
--- a/res/values-or/strings.xml
+++ b/res/values-or/strings.xml
@@ -60,10 +60,10 @@
     <string name="shortcuts_count" msgid="8471715556199592381">"{count,plural, =1{#ଟି ସର୍ଟକଟ୍}other{#ଟି ସର୍ଟକଟ୍}}"</string>
     <string name="widgets_and_shortcuts_count" msgid="7209136747878365116">"<xliff:g id="WIDGETS_COUNT">%1$s</xliff:g>, <xliff:g id="SHORTCUTS_COUNT">%2$s</xliff:g>"</string>
     <string name="widget_button_text" msgid="2880537293434387943">"ୱିଜେଟ୍‌"</string>
-    <string name="widgets_full_sheet_search_bar_hint" msgid="8484659090860596457">"ସନ୍ଧାନ କରନ୍ତୁ"</string>
+    <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>
@@ -164,7 +164,7 @@
     <string name="action_add_to_workspace" msgid="215894119683164916">"ହୋମ ସ୍କ୍ରିନରେ ଯୋଗ କରନ୍ତୁ"</string>
     <string name="action_move_here" msgid="2170188780612570250">"ଆଇଟମ୍‌କୁ ଏଠାକୁ ଘୁଞ୍ଚାନ୍ତୁ"</string>
     <string name="item_removed" msgid="851119963877842327">"ଆଇଟମକୁ କାଢ଼ି ଦିଆଯାଇଛି"</string>
-    <string name="undo" msgid="4151576204245173321">"ପୂର୍ବବତ କରନ୍ତୁ"</string>
+    <string name="undo" msgid="4151576204245173321">"ଅନଡୁ କରନ୍ତୁ"</string>
     <string name="action_move" msgid="4339390619886385032">"ଆଇଟମ୍‌ ଘୁଞ୍ଚାନ୍ତୁ"</string>
     <string name="move_to_empty_cell_description" msgid="5254852678218206889">"<xliff:g id="STRING">%3$s</xliff:g>ରେ ଧାଡି <xliff:g id="NUMBER_0">%1$s</xliff:g> ସ୍ତମ୍ଭ <xliff:g id="NUMBER_1">%2$s</xliff:g>କୁ ମୁଭ କରନ୍ତୁ"</string>
     <string name="move_to_position" msgid="6750008980455459790">"<xliff:g id="NUMBER">%1$s</xliff:g> ସ୍ଥିତିକୁ ନିଅନ୍ତୁ"</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-pt-rPT/strings.xml b/res/values-pt-rPT/strings.xml
index 701bb25..e016f20 100644
--- a/res/values-pt-rPT/strings.xml
+++ b/res/values-pt-rPT/strings.xml
@@ -64,7 +64,7 @@
     <string name="widgets_full_sheet_cancel_button_description" msgid="5766167035728653605">"Limpe o texto da caixa de pesquisa"</string>
     <string name="no_widgets_available" msgid="4337693382501046170">"Os widgets e os atalhos não estão disponíveis"</string>
     <string name="no_search_results" msgid="3787956167293097509">"Nenhum widget ou atalho encontrado"</string>
-    <string name="widgets_full_sheet_personal_tab" msgid="2743540105607120182">"Pessoais"</string>
+    <string name="widgets_full_sheet_personal_tab" msgid="2743540105607120182">"Pessoal"</string>
     <string name="widgets_full_sheet_work_tab" msgid="3767150027110633765">"Trabalho"</string>
     <string name="widget_category_conversations" msgid="8894438636213590446">"Conversas"</string>
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Tomar notas"</string>
diff --git a/res/values-sv/strings.xml b/res/values-sv/strings.xml
index ad5e281..d2bca52 100644
--- a/res/values-sv/strings.xml
+++ b/res/values-sv/strings.xml
@@ -64,8 +64,8 @@
     <string name="widgets_full_sheet_cancel_button_description" msgid="5766167035728653605">"Rensa texten från sökrutan"</string>
     <string name="no_widgets_available" msgid="4337693382501046170">"Widgetar och genvägar är inte tillgängliga"</string>
     <string name="no_search_results" msgid="3787956167293097509">"Inga widgetar eller genvägar hittades"</string>
-    <string name="widgets_full_sheet_personal_tab" msgid="2743540105607120182">"Privat"</string>
-    <string name="widgets_full_sheet_work_tab" msgid="3767150027110633765">"Arbete"</string>
+    <string name="widgets_full_sheet_personal_tab" msgid="2743540105607120182">"Personligt"</string>
+    <string name="widgets_full_sheet_work_tab" msgid="3767150027110633765">"Jobb"</string>
     <string name="widget_category_conversations" msgid="8894438636213590446">"Konversationer"</string>
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Anteckna"</string>
     <string name="widget_cell_tap_to_show_add_button_label" msgid="4354194214317043581">"Visa knappen Lägg till"</string>
diff --git a/res/values-te/strings.xml b/res/values-te/strings.xml
index b6563b0..4b25912 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>
@@ -64,8 +64,8 @@
     <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="widgets_full_sheet_personal_tab" msgid="2743540105607120182">"వ్యక్తిగత గ్యాడ్జెట్స్"</string>
-    <string name="widgets_full_sheet_work_tab" msgid="3767150027110633765">"ఆఫీస్"</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>
     <string name="widget_category_note_taking" msgid="3469689394504266039">"నోట్-టేకింగ్"</string>
     <string name="widget_cell_tap_to_show_add_button_label" msgid="4354194214317043581">"యాడ్‌ చేసే (జోడించే) బటన్‌ను చూపండి"</string>
diff --git a/res/values-tr/strings.xml b/res/values-tr/strings.xml
index cb47400..3fdae59 100644
--- a/res/values-tr/strings.xml
+++ b/res/values-tr/strings.xml
@@ -31,7 +31,7 @@
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Bölünmüş ekran"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$s uygulama bilgileri"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"%1$s ile ilgili kullanım ayarları"</string>
-    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Yeni Pencere"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Yeni pencere"</string>
     <string name="manage_windows_option_taskbar" msgid="2294109489960654212">"Pencereleri yönet"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Uygulama çiftini kaydedin"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
diff --git a/res/values-ur/strings.xml b/res/values-ur/strings.xml
index ed002ba..855a8c3 100644
--- a/res/values-ur/strings.xml
+++ b/res/values-ur/strings.xml
@@ -32,7 +32,7 @@
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"‏%1$s کے لیے ایپ کی معلومات"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"‏%1$s کیلئے استعمال کی ترتیبات"</string>
     <string name="new_window_option_taskbar" msgid="6448780542727767211">"نئی ونڈو"</string>
-    <string name="manage_windows_option_taskbar" msgid="2294109489960654212">"‏‫Windows کا نظم کریں"</string>
+    <string name="manage_windows_option_taskbar" msgid="2294109489960654212">"ونڈوز کا نظم کریں"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"ایپس کے جوڑے کو محفوظ کریں"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"ایپس کا یہ جوڑا اس آلے پر تعاون یافتہ نہیں ہے"</string>
diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml
index cecfedb..febc8c2 100644
--- a/res/values-zh-rCN/strings.xml
+++ b/res/values-zh-rCN/strings.xml
@@ -127,7 +127,7 @@
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"文件夹:<xliff:g id="NAME">%1$s</xliff:g>,<xliff:g id="SIZE">%2$d</xliff:g> 个或更多项目"</string>
     <string name="unnamed_folder" msgid="2420192029474044442">"未命名文件夹"</string>
     <string name="app_pair_name_format" msgid="8134106404716224054">"应用对:“<xliff:g id="APP1">%1$s</xliff:g>”和“<xliff:g id="APP2">%2$s</xliff:g>”"</string>
-    <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"壁纸与个性化"</string>
+    <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"壁纸与风格"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"修改主屏幕"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"主屏幕设置"</string>
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"已被您的管理员停用"</string>
diff --git a/res/values/config.xml b/res/values/config.xml
index 07f97bc..1a2ac9e 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -67,7 +67,6 @@
     <string name="main_process_initializer_class" translatable="false"></string>
     <string name="app_launch_tracker_class" translatable="false"></string>
     <string name="test_information_handler_class" translatable="false"></string>
-    <string name="model_delegate_class" translatable="false"></string>
     <string name="secondary_display_predictions_class" translatable="false"></string>
     <string name="widget_holder_factory_class" translatable="false"></string>
     <string name="taskbar_search_session_controller_class" translatable="false"></string>
@@ -75,8 +74,6 @@
     <string name="taskbar_view_callbacks_factory_class" translatable="false"></string>
     <string name="launcher_restore_event_logger_class" translatable="false"></string>
     <string name="taskbar_edu_tooltip_controller_class" translatable="false"></string>
-    <!--  Used for determining category of a widget presented in widget recommendations. -->
-    <string name="widget_recommendation_category_provider_class" translatable="false"></string>
 
     <!-- Default packages -->
     <string name="wallpaper_picker_package" translatable="false"></string>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index c3cb31d..7aa709d 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -307,6 +307,7 @@
 
     <!-- Folders -->
     <dimen name="page_indicator_dot_size">6dp</dimen>
+    <dimen name="page_indicator_gap_width">4dp</dimen>
     <dimen name="page_indicator_size">10dp</dimen>
 
 
diff --git a/res/values/id.xml b/res/values/id.xml
index 67692d8..78b8308 100644
--- a/res/values/id.xml
+++ b/res/values/id.xml
@@ -79,6 +79,7 @@
 
     <item type="id" name="saved_clip_children_tag_id" />
     <item type="id" name="saved_clip_to_padding_tag_id" />
+    <item type="id" name="perform_a11y_action_on_launcher_state_normal_tag" />
 
     <item type="id" name="saved_floating_widget_foreground" />
     <item type="id" name="saved_floating_widget_background" />
diff --git a/src/com/android/launcher3/AppFilter.java b/src/com/android/launcher3/AppFilter.java
index 3db456c..8ee7053 100644
--- a/src/com/android/launcher3/AppFilter.java
+++ b/src/com/android/launcher3/AppFilter.java
@@ -3,10 +3,14 @@
 import android.content.ComponentName;
 import android.content.Context;
 
+import com.android.launcher3.dagger.ApplicationContext;
+
 import java.util.Arrays;
 import java.util.Set;
 import java.util.stream.Collectors;
 
+import javax.inject.Inject;
+
 /**
  * Utility class to filter out components from various lists
  */
@@ -14,7 +18,8 @@
 
     private final Set<ComponentName> mFilteredComponents;
 
-    public AppFilter(Context context) {
+    @Inject
+    public AppFilter(@ApplicationContext Context context) {
         mFilteredComponents = Arrays.stream(
                 context.getResources().getStringArray(R.array.filtered_components))
                 .map(ComponentName::unflattenFromString)
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index fb847f9..bd42b2b 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -27,6 +27,7 @@
 import static com.android.launcher3.icons.BitmapInfo.FLAG_SKIP_USER_BADGE;
 import static com.android.launcher3.icons.BitmapInfo.FLAG_THEMED;
 import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
+import static com.android.launcher3.icons.IconNormalizer.ICON_VISIBLE_AREA_FACTOR;
 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_INCREMENTAL_DOWNLOAD_ACTIVE;
 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE;
 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_SHOW_DOWNLOAD_PROGRESS_MASK;
@@ -75,7 +76,6 @@
 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.icons.DotRenderer;
 import com.android.launcher3.icons.FastBitmapDrawable;
@@ -723,8 +723,7 @@
     protected void drawDotIfNecessary(Canvas canvas) {
         if (!mForceHideDot && (hasDot() || mDotParams.scale > 0)) {
             getIconBounds(mDotParams.iconBounds);
-            Utilities.scaleRectAboutCenter(mDotParams.iconBounds,
-                    IconShape.INSTANCE.get(getContext()).getNormalizationScale());
+            Utilities.scaleRectAboutCenter(mDotParams.iconBounds, ICON_VISIBLE_AREA_FACTOR);
             final int scrollX = getScrollX();
             final int scrollY = getScrollY();
             canvas.translate(scrollX, scrollY);
@@ -773,9 +772,7 @@
             return;
         }
         getIconBounds(mRunningAppIconBounds);
-        Utilities.scaleRectAboutCenter(
-                mRunningAppIconBounds,
-                IconShape.INSTANCE.get(getContext()).getNormalizationScale());
+        Utilities.scaleRectAboutCenter(mRunningAppIconBounds, ICON_VISIBLE_AREA_FACTOR);
 
         final boolean isMinimized = mRunningAppState == RunningAppState.MINIMIZED;
         final int indicatorTop = mRunningAppIconBounds.bottom + mRunningAppIndicatorTopMargin;
@@ -964,7 +961,8 @@
 
     @Override
     public void setTextColor(ColorStateList colors) {
-        mTextColor = shouldDrawAppContrastTile() ? PillColorProvider.getInstance(
+        mTextColor = (shouldDrawAppContrastTile() && !TextUtils.isEmpty(getText()))
+                ? PillColorProvider.getInstance(
                 getContext()).getAppTitleTextPaint().getColor()
                 : colors.getDefaultColor();
         mTextColorStateList = colors;
@@ -989,7 +987,7 @@
     public boolean shouldDrawAppContrastTile() {
         return mDisplay == DISPLAY_WORKSPACE && shouldTextBeVisible()
                 && PillColorProvider.getInstance(getContext()).isMatchaEnabled()
-                && enableContrastTiles() && !TextUtils.isEmpty(getText());
+                && enableContrastTiles();
     }
 
     public void setTextVisibility(boolean visible) {
diff --git a/src/com/android/launcher3/CellLayout.java b/src/com/android/launcher3/CellLayout.java
index 257f911..0ce966b 100644
--- a/src/com/android/launcher3/CellLayout.java
+++ b/src/com/android/launcher3/CellLayout.java
@@ -16,7 +16,6 @@
 
 package com.android.launcher3;
 
-import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.dragndrop.DraggableView.DRAGGABLE_ICON;
 import static com.android.launcher3.icons.IconNormalizer.ICON_VISIBLE_AREA_FACTOR;
 import static com.android.launcher3.util.MultiTranslateDelegate.INDEX_REORDER_PREVIEW_OFFSET;
@@ -71,7 +70,6 @@
 import com.android.launcher3.folder.PreviewBackground;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
-import com.android.launcher3.statemanager.StateManager;
 import com.android.launcher3.util.CellAndSpan;
 import com.android.launcher3.util.GridOccupancy;
 import com.android.launcher3.util.MSDLPlayerWrapper;
@@ -791,18 +789,8 @@
 
             // Whenever an app is added, if Accessibility service is enabled, focus on that app.
             if (mActivity instanceof Launcher) {
-                Launcher.cast(mActivity).getStateManager().addStateListener(
-                        new StateManager.StateListener<LauncherState>() {
-                            @Override
-                            public void onStateTransitionComplete(LauncherState finalState) {
-                                if (finalState == NORMAL) {
-                                    child.performAccessibilityAction(
-                                            AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null);
-                                    Launcher.cast(mActivity).getStateManager()
-                                            .removeStateListener(this);
-                                }
-                            }
-                        });
+                child.setTag(R.id.perform_a11y_action_on_launcher_state_normal_tag,
+                        AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
             }
 
             if (markCells) markCellsAsOccupiedForView(child);
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index 813d8f1..c85ca49 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,9 +53,8 @@
 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;
 import com.android.launcher3.responsive.CalculatedCellSpec;
 import com.android.launcher3.responsive.CalculatedHotseatSpec;
@@ -387,7 +387,8 @@
     }
 
     /** TODO: Once we fully migrate to staged split, remove "isMultiWindowMode" */
-    DeviceProfile(Context context, InvariantDeviceProfile inv, Info info, WindowBounds windowBounds,
+    DeviceProfile(Context context, InvariantDeviceProfile inv, Info info,
+            WindowManagerProxy wmProxy, ThemeManager themeManager, WindowBounds windowBounds,
             SparseArray<DotRenderer> dotRendererCache, boolean isMultiWindowMode,
             boolean transposeLayoutWithOrientation, boolean isMultiDisplay, boolean isGestureMode,
             @NonNull final ViewScaleProvider viewScaleProvider,
@@ -419,8 +420,10 @@
         isTablet = info.isTablet(windowBounds);
         isPhone = !isTablet;
         isTwoPanels = isTablet && isMultiDisplay;
-        isTaskbarPresent = (isTablet || (enableTinyTaskbar() && isGestureMode))
-                && WindowManagerProxy.INSTANCE.get(context).isTaskbarDrawnInProcess();
+        boolean taskbarOrBubbleBarOnPhones = enableTinyTaskbar()
+                || (enableBubbleBar() && enableBubbleBarOnPhones());
+        isTaskbarPresent = (isTablet || (taskbarOrBubbleBarOnPhones && isGestureMode))
+                && wmProxy.isTaskbarDrawnInProcess();
 
         // Some more constants.
         context = getContext(context, info, inv.isFixedLandscape
@@ -845,8 +848,8 @@
         dimensionOverrideProvider.accept(this);
 
         // This is done last, after iconSizePx is calculated above.
-        mDotRendererWorkSpace = createDotRenderer(context, iconSizePx, dotRendererCache);
-        mDotRendererAllApps = createDotRenderer(context, allAppsIconSizePx, dotRendererCache);
+        mDotRendererWorkSpace = createDotRenderer(themeManager, iconSizePx, dotRendererCache);
+        mDotRendererAllApps = createDotRenderer(themeManager, allAppsIconSizePx, dotRendererCache);
     }
 
     /**
@@ -868,12 +871,12 @@
     }
 
     private static DotRenderer createDotRenderer(
-            @NonNull Context context, 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.INSTANCE.get(context).getShape().getPath(DEFAULT_DOT_SIZE),
+                    themeManager.getIconShape().getPath(DEFAULT_DOT_SIZE),
                     DEFAULT_DOT_SIZE);
             cache.put(size, renderer);
         }
@@ -1090,7 +1093,7 @@
         dotRendererCache.put(iconSizePx, mDotRendererWorkSpace);
         dotRendererCache.put(allAppsIconSizePx, mDotRendererAllApps);
 
-        return new Builder(context, inv, mInfo)
+        return inv.newDPBuilder(context, mInfo)
                 .setWindowBounds(bounds)
                 .setIsMultiDisplay(isMultiDisplay)
                 .setMultiWindowMode(isMultiWindowMode)
@@ -1370,7 +1373,7 @@
         updateHotseatSizes(iconSizePx);
 
         // Folder icon
-        folderIconSizePx = IconNormalizer.getNormalizedCircleSize(iconSizePx);
+        folderIconSizePx = Math.round(iconSizePx * ICON_VISIBLE_AREA_FACTOR);
         folderIconOffsetYPx = (iconSizePx - folderIconSizePx) / 2;
 
         // Update widget padding:
@@ -2473,9 +2476,11 @@
     }
 
     public static class Builder {
-        private Context mContext;
-        private InvariantDeviceProfile mInv;
-        private Info mInfo;
+        private final Context mContext;
+        private final InvariantDeviceProfile mInv;
+        private final Info mInfo;
+        private final WindowManagerProxy mWMProxy;
+        private final ThemeManager mThemeManager;
 
         private WindowBounds mWindowBounds;
         private boolean mIsMultiDisplay;
@@ -2491,10 +2496,13 @@
 
         private boolean mIsTransientTaskbar;
 
-        public Builder(Context context, InvariantDeviceProfile inv, Info info) {
+        public Builder(Context context, InvariantDeviceProfile inv, Info info,
+                WindowManagerProxy wmProxy, ThemeManager themeManager) {
             mContext = context;
             mInv = inv;
             mInfo = info;
+            mWMProxy = wmProxy;
+            mThemeManager = themeManager;
             mIsTransientTaskbar = info.isTransientTaskbar();
         }
 
@@ -2575,7 +2583,8 @@
             if (mOverrideProvider == null) {
                 mOverrideProvider = DEFAULT_DIMENSION_PROVIDER;
             }
-            return new DeviceProfile(mContext, mInv, mInfo, mWindowBounds, mDotRendererCache,
+            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 56c2b8e..f189549 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -30,7 +30,6 @@
 import static com.android.launcher3.util.DisplayController.CHANGE_TASKBAR_PINNING;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 
-import android.annotation.TargetApi;
 import android.content.Context;
 import android.content.Intent;
 import android.content.res.Resources;
@@ -46,7 +45,6 @@
 import android.util.Log;
 import android.util.SparseArray;
 import android.util.Xml;
-import android.view.Display;
 
 import androidx.annotation.DimenRes;
 import androidx.annotation.IntDef;
@@ -56,18 +54,21 @@
 import androidx.core.content.res.ResourcesCompat;
 
 import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.dagger.ApplicationContext;
+import com.android.launcher3.dagger.LauncherAppComponent;
+import com.android.launcher3.dagger.LauncherAppSingleton;
+import com.android.launcher3.graphics.ThemeManager;
 import com.android.launcher3.icons.DotRenderer;
 import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.model.DeviceGridState;
 import com.android.launcher3.provider.RestoreDbTask;
 import com.android.launcher3.testing.shared.ResourceUtils;
+import com.android.launcher3.util.DaggerSingletonObject;
+import com.android.launcher3.util.DaggerSingletonTracker;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.DisplayController.Info;
-import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.util.Partner;
 import com.android.launcher3.util.ResourceHelper;
-import com.android.launcher3.util.RunnableList;
-import com.android.launcher3.util.SafeCloseable;
 import com.android.launcher3.util.SimpleBroadcastReceiver;
 import com.android.launcher3.util.WindowBounds;
 import com.android.launcher3.util.window.CachedDisplayInfo;
@@ -84,14 +85,18 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
+import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.stream.Collectors;
 
-public class InvariantDeviceProfile implements SafeCloseable {
+import javax.inject.Inject;
+
+@LauncherAppSingleton
+public class InvariantDeviceProfile {
 
     public static final String TAG = "IDP";
     // We do not need any synchronization for this variable as its only written on UI thread.
-    public static final MainThreadInitializedObject<InvariantDeviceProfile> INSTANCE =
-            new MainThreadInitializedObject<>(InvariantDeviceProfile::new);
+    public static final DaggerSingletonObject<InvariantDeviceProfile> INSTANCE =
+            new DaggerSingletonObject<>(LauncherAppComponent::getIDP);
 
     public static final String GRID_NAME_PREFS_KEY = "idp_grid_name";
     public static final String NON_FIXED_LANDSCAPE_GRID_NAME_PREFS_KEY =
@@ -129,7 +134,10 @@
     private static final String RES_GRID_NUM_COLUMNS = "grid_num_columns";
     private static final String RES_GRID_ICON_SIZE_DP = "grid_icon_size_dp";
 
-    private final RunnableList mCloseActions = new RunnableList();
+    private final DisplayController mDisplayController;
+    private final WindowManagerProxy mWMProxy;
+    private final LauncherPrefs mPrefs;
+    private final ThemeManager mThemeManager;
 
     /**
      * Number of icons per row and column in the workspace.
@@ -245,18 +253,24 @@
 
     public Point defaultWallpaperSize;
 
-    private final ArrayList<OnIDPChangeListener> mChangeListeners = new ArrayList<>();
+    private final List<OnIDPChangeListener> mChangeListeners = new CopyOnWriteArrayList<>();
 
-    @VisibleForTesting
-    public InvariantDeviceProfile() {
-    }
+    @Inject
+    InvariantDeviceProfile(
+            @ApplicationContext Context context,
+            LauncherPrefs prefs,
+            DisplayController dc,
+            WindowManagerProxy wmProxy,
+            ThemeManager themeManager,
+            DaggerSingletonTracker lifeCycle) {
+        mDisplayController = dc;
+        mWMProxy = wmProxy;
+        mPrefs = prefs;
+        mThemeManager = themeManager;
 
-    @TargetApi(23)
-    private InvariantDeviceProfile(Context context) {
-        String gridName = getCurrentGridName(context);
+        String gridName = prefs.get(GRID_NAME);
         initGrid(context, gridName);
 
-        DisplayController dc = DisplayController.INSTANCE.get(context);
         dc.setPriorityListener(
                 (displayContext, info, flags) -> {
                     if ((flags & (CHANGE_DENSITY | CHANGE_SUPPORTED_BOUNDS
@@ -265,126 +279,46 @@
                         onConfigChanged(displayContext);
                     }
                 });
-        mCloseActions.add(() -> dc.setPriorityListener(null));
+        lifeCycle.addCloseable(() -> dc.setPriorityListener(null));
 
         LauncherPrefChangeListener prefListener = key -> {
             if (FIXED_LANDSCAPE_MODE.getSharedPrefKey().equals(key)
-                    && isFixedLandscape != FIXED_LANDSCAPE_MODE.get(context)) {
+                    && isFixedLandscape != prefs.get(FIXED_LANDSCAPE_MODE)) {
                 Trace.beginSection("InvariantDeviceProfile#setFixedLandscape");
                 if (isFixedLandscape) {
-                    setCurrentGrid(
-                            context, LauncherPrefs.get(context).get(NON_FIXED_LANDSCAPE_GRID_NAME));
+                    setCurrentGrid(context, prefs.get(NON_FIXED_LANDSCAPE_GRID_NAME));
                 } else {
-                    LauncherPrefs.get(context)
-                            .put(NON_FIXED_LANDSCAPE_GRID_NAME, getCurrentGridName(context));
+                    prefs.put(NON_FIXED_LANDSCAPE_GRID_NAME, mPrefs.get(GRID_NAME));
                     onConfigChanged(context);
                 }
                 Trace.endSection();
             } else if (ENABLE_TWOLINE_ALLAPPS_TOGGLE.getSharedPrefKey().equals(key)
-                    && enableTwoLinesInAllApps != ENABLE_TWOLINE_ALLAPPS_TOGGLE.get(context)) {
+                    && enableTwoLinesInAllApps != prefs.get(ENABLE_TWOLINE_ALLAPPS_TOGGLE)) {
                 onConfigChanged(context);
             }
         };
-        LauncherPrefs prefs = LauncherPrefs.INSTANCE.get(context);
         prefs.addListener(prefListener, FIXED_LANDSCAPE_MODE, ENABLE_TWOLINE_ALLAPPS_TOGGLE);
-        mCloseActions.add(() -> prefs.removeListener(prefListener,
+        lifeCycle.addCloseable(() -> prefs.removeListener(prefListener,
                 FIXED_LANDSCAPE_MODE, ENABLE_TWOLINE_ALLAPPS_TOGGLE));
 
-        SimpleBroadcastReceiver localeReceiver = new SimpleBroadcastReceiver(
+        SimpleBroadcastReceiver localeReceiver = new SimpleBroadcastReceiver(context,
                 MAIN_EXECUTOR, i -> onConfigChanged(context));
-        localeReceiver.register(context, Intent.ACTION_LOCALE_CHANGED);
-        mCloseActions.add(() -> localeReceiver.unregisterReceiverSafely(context));
-    }
-
-    /**
-     * This constructor should NOT have any monitors by design.
-     */
-    public InvariantDeviceProfile(Context context, String gridName) {
-        String newName = initGrid(context, gridName);
-        if (newName == null || !newName.equals(gridName)) {
-            throw new IllegalArgumentException("Unknown grid name: " + gridName);
-        }
-    }
-
-    /**
-     * This constructor should NOT have any monitors by design.
-     */
-    public InvariantDeviceProfile(Context context, Display display) {
-        // Ensure that the main device profile is initialized
-        INSTANCE.get(context);
-        String gridName = getCurrentGridName(context);
-
-        // Get the display info based on default display and interpolate it to existing display
-        Info defaultInfo = DisplayController.INSTANCE.get(context).getInfo();
-        @DeviceType int defaultDeviceType = defaultInfo.getDeviceType();
-        DisplayOption defaultDisplayOption = invDistWeightedInterpolate(
-                defaultInfo,
-                getPredefinedDeviceProfiles(
-                        context,
-                        gridName,
-                        defaultInfo,
-                        /*allowDisabledGrid=*/false,
-                        FIXED_LANDSCAPE_MODE.get(context)
-                ),
-                defaultDeviceType);
-
-        Context displayContext = context.createDisplayContext(display);
-        Info myInfo = new Info(displayContext);
-        @DeviceType int deviceType = myInfo.getDeviceType();
-        DisplayOption myDisplayOption = invDistWeightedInterpolate(
-                myInfo,
-                getPredefinedDeviceProfiles(
-                        context,
-                        gridName,
-                        myInfo,
-                        /*allowDisabledGrid=*/false,
-                        FIXED_LANDSCAPE_MODE.get(context)
-                ),
-                deviceType);
-
-        DisplayOption result = new DisplayOption(defaultDisplayOption.grid)
-                .add(myDisplayOption);
-        result.iconSizes[INDEX_DEFAULT] =
-                defaultDisplayOption.iconSizes[INDEX_DEFAULT];
-        for (int i = 1; i < COUNT_SIZES; i++) {
-            result.iconSizes[i] = Math.min(
-                    defaultDisplayOption.iconSizes[i], myDisplayOption.iconSizes[i]);
-        }
-
-        System.arraycopy(defaultDisplayOption.minCellSize, 0, result.minCellSize, 0,
-                COUNT_SIZES);
-        System.arraycopy(defaultDisplayOption.borderSpaces, 0, result.borderSpaces, 0,
-                COUNT_SIZES);
-
-        initGrid(context, myInfo, result);
-    }
-
-    @Override
-    public void close() {
-        mCloseActions.executeAllAndDestroy();
-    }
-
-    public static String getCurrentGridName(Context context) {
-        return LauncherPrefs.get(context).get(GRID_NAME);
+        localeReceiver.register(Intent.ACTION_LOCALE_CHANGED);
+        lifeCycle.addCloseable(() -> localeReceiver.unregisterReceiverSafely());
     }
 
     private String initGrid(Context context, String gridName) {
-        FileLog.d(TAG, "Before initGrid:"
-                + "gridName:" + gridName
-                + ", dbFile:" + dbFile
-                + ", LauncherPrefs GRID_NAME:" + LauncherPrefs.get(context).get(GRID_NAME)
-                + ", LauncherPrefs DB_FILE:" + LauncherPrefs.get(context).get(DB_FILE));
-        Info displayInfo = DisplayController.INSTANCE.get(context).getInfo();
+        Info displayInfo = mDisplayController.getInfo();
         List<DisplayOption> allOptions = getPredefinedDeviceProfiles(
                 context,
                 gridName,
                 displayInfo,
-                RestoreDbTask.isPending(context),
-                FIXED_LANDSCAPE_MODE.get(context)
+                (RestoreDbTask.isPending(mPrefs) && !Flags.oneGridSpecs()),
+                mPrefs.get(FIXED_LANDSCAPE_MODE)
         );
 
         // Filter out options that don't have the same number of columns as the grid
-        DeviceGridState deviceGridState = new DeviceGridState(context);
+        DeviceGridState deviceGridState = new DeviceGridState(mPrefs);
         List<DisplayOption> allOptionsFilteredByColCount =
                 filterByColumnCount(allOptions, deviceGridState.getColumns());
 
@@ -395,15 +329,15 @@
                         displayInfo.getDeviceType());
 
         if (!displayOption.grid.name.equals(gridName)) {
-            LauncherPrefs.get(context).put(GRID_NAME, displayOption.grid.name);
+            mPrefs.put(GRID_NAME, displayOption.grid.name);
         }
 
         initGrid(context, displayInfo, displayOption);
         FileLog.d(TAG, "After initGrid:"
                 + "gridName:" + gridName
                 + ", dbFile:" + dbFile
-                + ", LauncherPrefs GRID_NAME:" + LauncherPrefs.get(context).get(GRID_NAME)
-                + ", LauncherPrefs DB_FILE:" + LauncherPrefs.get(context).get(DB_FILE));
+                + ", LauncherPrefs GRID_NAME:" + mPrefs.get(GRID_NAME)
+                + ", LauncherPrefs DB_FILE:" + mPrefs.get(DB_FILE));
         return displayOption.grid.name;
     }
 
@@ -420,18 +354,13 @@
      */
     @Deprecated
     public void reset(Context context) {
-        initGrid(context, getCurrentGridName(context));
-    }
-
-    @VisibleForTesting
-    public static String getDefaultGridName(Context context) {
-        return new InvariantDeviceProfile().initGrid(context, null);
+        initGrid(context, mPrefs.get(GRID_NAME));
     }
 
     private void initGrid(Context context, Info displayInfo, DisplayOption displayOption) {
         enableTwoLinesInAllApps = Flags.enableTwolineToggle()
                 && Utilities.isEnglishLanguage(context)
-                && ENABLE_TWOLINE_ALLAPPS_TOGGLE.get(context);
+                && mPrefs.get(ENABLE_TWOLINE_ALLAPPS_TOGGLE);
         mLocale = context.getResources().getConfiguration().locale.toString();
 
         DisplayMetrics metrics = context.getResources().getDisplayMetrics();
@@ -522,7 +451,7 @@
         defaultWallpaperSize = new Point(displayInfo.currentSize);
         SparseArray<DotRenderer> dotRendererCache = new SparseArray<>();
         for (WindowBounds bounds : displayInfo.supportedBounds) {
-            localSupportedProfiles.add(new DeviceProfile.Builder(context, this, displayInfo)
+            localSupportedProfiles.add(newDPBuilder(context, displayInfo)
                     .setIsMultiDisplay(deviceType == TYPE_MULTI_DISPLAY)
                     .setWindowBounds(bounds)
                     .setDotRendererCache(dotRendererCache)
@@ -561,6 +490,10 @@
                 });
     }
 
+    DeviceProfile.Builder newDPBuilder(Context context, Info info) {
+        return new DeviceProfile.Builder(context, this, info, mWMProxy, mThemeManager);
+    }
+
     public void addOnChangeListener(OnIDPChangeListener listener) {
         mChangeListeners.add(listener);
     }
@@ -573,8 +506,9 @@
      * Updates the current grid, this triggers a new IDP, reloads the database and triggers a grid
      * migration.
      */
+    @VisibleForTesting
     public void setCurrentGrid(Context context, String newGridName) {
-        LauncherPrefs.get(context).put(GRID_NAME, newGridName);
+        mPrefs.put(GRID_NAME, newGridName);
         MAIN_EXECUTOR.execute(() -> {
             Trace.beginSection("InvariantDeviceProfile#setCurrentGrid");
             onConfigChanged(context.getApplicationContext());
@@ -594,8 +528,7 @@
         Object[] oldState = toModelState();
 
         // Re-init grid
-        String gridName = getCurrentGridName(context);
-        initGrid(context, gridName);
+        initGrid(context, mPrefs.get(GRID_NAME));
 
         boolean modelPropsChanged = !Arrays.equals(oldState, toModelState());
         for (OnIDPChangeListener listener : mChangeListeners) {
@@ -912,11 +845,20 @@
         return out;
     }
 
-    public DeviceProfile getDeviceProfile(Context context) {
-        WindowManagerProxy windowManagerProxy = WindowManagerProxy.INSTANCE.get(context);
-        Rect bounds = windowManagerProxy.getCurrentBounds(context);
-        int rotation = windowManagerProxy.getRotation(context);
+    public DeviceProfile createDeviceProfileForSecondaryDisplay(Context displayContext) {
+        // Disable transpose layout and use multi-window mode so that the icons are scaled properly
+        return newDPBuilder(displayContext, new Info(displayContext))
+                .setIsMultiDisplay(false)
+                .setMultiWindowMode(true)
+                .setWindowBounds(mWMProxy.getRealBounds(
+                        displayContext, mWMProxy.getDisplayInfo(displayContext)))
+                .setTransposeLayoutWithOrientation(false)
+                .build();
+    }
 
+    public DeviceProfile getDeviceProfile(Context context) {
+        Rect bounds = mWMProxy.getCurrentBounds(context);
+        int rotation = mWMProxy.getRotation(context);
         return getBestMatch(bounds.width(), bounds.height(), rotation);
     }
 
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 315301a..3edba99 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -71,6 +71,7 @@
 import static com.android.launcher3.LauncherState.NO_SCALE;
 import static com.android.launcher3.LauncherState.SPRING_LOADED;
 import static com.android.launcher3.Utilities.postAsyncCallback;
+import static com.android.launcher3.anim.AnimatorListeners.forEndCallback;
 import static com.android.launcher3.config.FeatureFlags.FOLDABLE_SINGLE_PAGE;
 import static com.android.launcher3.config.FeatureFlags.MULTI_SELECT_EDIT_MODE;
 import static com.android.launcher3.logging.KeyboardStateManager.KeyboardState.HIDE;
@@ -182,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;
@@ -610,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() {
@@ -1466,7 +1467,10 @@
             }
 
             getModelWriter().addItemToDatabase(info, container, screenId, cellXY[0], cellXY[1]);
-            mWorkspace.addInScreen(view, info);
+            AnimatorSet anim = new AnimatorSet();
+            anim.addListener(forEndCallback(() ->
+                    view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED)));
+            bindInflatedItems(Collections.singletonList(Pair.create(info, view)), anim);
         } else {
             // Adding a shortcut to a Folder.
             FolderIcon folderIcon = findFolderIcon(container);
diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java
deleted file mode 100644
index e560a14..0000000
--- a/src/com/android/launcher3/LauncherAppState.java
+++ /dev/null
@@ -1,250 +0,0 @@
-/*
- * Copyright (C) 2013 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;
-
-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.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.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 =
-            new MainThreadInitializedObject<>(LauncherAppState::new);
-
-    private final Context mContext;
-    private final LauncherModel mModel;
-    private final LauncherIconProvider mIconProvider;
-    private final IconCache mIconCache;
-    private final InvariantDeviceProfile mInvariantDeviceProfile;
-    private boolean mIsSafeModeEnabled;
-
-    private final RunnableList mOnTerminateCallback = new RunnableList();
-
-    public static LauncherAppState getInstance(Context context) {
-        return INSTANCE.get(context);
-    }
-
-    public Context getContext() {
-        return mContext;
-    }
-
-    @SuppressWarnings("NewApi")
-    public LauncherAppState(Context context) {
-        this(context, LauncherFiles.APP_ICONS_DB);
-        Log.v(Launcher.TAG, "LauncherAppState initiated");
-        Preconditions.assertUIThread();
-
-        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(UI_HELPER_EXECUTOR, mModel::onBroadcastIntent);
-        modelChangeReceiver.register(
-                mContext,
-                ACTION_DEVICE_POLICY_RESOURCE_UPDATED);
-        if (BuildConfig.IS_STUDIO_BUILD) {
-            modelChangeReceiver.register(mContext, RECEIVER_EXPORTED, ACTION_FORCE_ROLOAD);
-        }
-        mOnTerminateCallback.add(() -> modelChangeReceiver.unregisterReceiverSafely(mContext));
-
-        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);
-    }
-
-    public LauncherAppState(Context context, @Nullable String iconCacheFileName) {
-        mContext = context;
-
-        mInvariantDeviceProfile = InvariantDeviceProfile.INSTANCE.get(context);
-        mIconProvider = new LauncherIconProvider(context);
-        mIconCache = new IconCache(mContext, mInvariantDeviceProfile,
-                iconCacheFileName, mIconProvider);
-        mModel = new LauncherModel(context, this, mIconCache,
-                WidgetsFilterDataProvider.Companion.newInstance(context), new AppFilter(mContext),
-                PackageManagerHelper.INSTANCE.get(context), iconCacheFileName != null);
-        mOnTerminateCallback.add(mIconCache::close);
-        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.
-     */
-    @Override
-    public void close() {
-        mOnTerminateCallback.executeAllAndDestroy();
-    }
-
-    public IconProvider getIconProvider() {
-        return mIconProvider;
-    }
-
-    public IconCache getIconCache() {
-        return mIconCache;
-    }
-
-    public LauncherModel getModel() {
-        return mModel;
-    }
-
-    public InvariantDeviceProfile getInvariantDeviceProfile() {
-        return mInvariantDeviceProfile;
-    }
-
-    public boolean isSafeModeEnabled() {
-        return mIsSafeModeEnabled;
-    }
-
-    /**
-     * Shorthand for {@link #getInvariantDeviceProfile()}
-     */
-    public static InvariantDeviceProfile getIDP(Context context) {
-        return InvariantDeviceProfile.INSTANCE.get(context);
-    }
-}
diff --git a/src/com/android/launcher3/LauncherAppState.kt b/src/com/android/launcher3/LauncherAppState.kt
new file mode 100644
index 0000000..ff84c3c
--- /dev/null
+++ b/src/com/android/launcher3/LauncherAppState.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2013 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
+
+import android.content.Context
+import com.android.launcher3.dagger.ApplicationContext
+import com.android.launcher3.icons.IconCache
+import com.android.launcher3.icons.LauncherIconProvider
+import com.android.launcher3.util.DaggerSingletonObject
+import javax.inject.Inject
+import javax.inject.Named
+
+/** A collection of common dependencies used across Launcher */
+@Deprecated("Inject the specific targets directly instead of using LauncherAppState")
+data class LauncherAppState
+@Inject
+constructor(
+    @ApplicationContext val context: Context,
+    val iconProvider: LauncherIconProvider,
+    val iconCache: IconCache,
+    val model: LauncherModel,
+    val invariantDeviceProfile: InvariantDeviceProfile,
+    @Named("SAFE_MODE") val isSafeModeEnabled: Boolean,
+) {
+
+    companion object {
+
+        @JvmField var INSTANCE = DaggerSingletonObject { it.launcherAppState }
+
+        @JvmStatic fun getInstance(context: Context) = INSTANCE[context]
+
+        /** Shorthand for [.getInvariantDeviceProfile] */
+        @JvmStatic fun getIDP(context: Context) = InvariantDeviceProfile.INSTANCE[context]
+    }
+}
diff --git a/src/com/android/launcher3/LauncherApplication.java b/src/com/android/launcher3/LauncherApplication.java
index 678901b..03eaeea 100644
--- a/src/com/android/launcher3/LauncherApplication.java
+++ b/src/com/android/launcher3/LauncherApplication.java
@@ -20,6 +20,7 @@
 import com.android.launcher3.dagger.DaggerLauncherAppComponent;
 import com.android.launcher3.dagger.LauncherAppComponent;
 import com.android.launcher3.dagger.LauncherBaseAppComponent;
+import com.android.launcher3.util.TraceHelper;
 
 /**
  * Main application class for Launcher
@@ -41,7 +42,8 @@
                 if (mAppComponent == null) {
                     // Initialize the dagger component on demand as content providers can get
                     // accessed before the Launcher application (b/36917845#comment4)
-                    initDaggerComponent(DaggerLauncherAppComponent.builder());
+                    initDaggerComponent(DaggerLauncherAppComponent.builder()
+                            .iconsDbName(LauncherFiles.APP_ICONS_DB));
                 }
             }
         }
@@ -55,7 +57,11 @@
     /**
      * Init with the desired dagger component.
      */
-    public void initDaggerComponent(LauncherAppComponent.Builder componentBuilder) {
-        mAppComponent = componentBuilder.appContext(this).build();
+    public void initDaggerComponent(LauncherBaseAppComponent.Builder componentBuilder) {
+        mAppComponent = componentBuilder
+                .appContext(this)
+                .setSafeModeEnabled(TraceHelper.allowIpcs(
+                        "isSafeMode", () -> getPackageManager().isSafeMode()))
+                .build();
     }
 }
diff --git a/src/com/android/launcher3/LauncherModel.kt b/src/com/android/launcher3/LauncherModel.kt
index 185629b..892a218 100644
--- a/src/com/android/launcher3/LauncherModel.kt
+++ b/src/com/android/launcher3/LauncherModel.kt
@@ -15,16 +15,16 @@
  */
 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
+import com.android.launcher3.dagger.ApplicationContext
+import com.android.launcher3.dagger.LauncherAppSingleton
 import com.android.launcher3.icons.IconCache
 import com.android.launcher3.model.AddWorkspaceItemsTask
 import com.android.launcher3.model.AllAppsList
@@ -35,6 +35,7 @@
 import com.android.launcher3.model.LoaderTask
 import com.android.launcher3.model.ModelDbController
 import com.android.launcher3.model.ModelDelegate
+import com.android.launcher3.model.ModelInitializer
 import com.android.launcher3.model.ModelLauncherCallbacks
 import com.android.launcher3.model.ModelTaskController
 import com.android.launcher3.model.ModelWriter
@@ -47,32 +48,42 @@
 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.DaggerSingletonTracker
 import com.android.launcher3.util.Executors.MAIN_EXECUTOR
 import com.android.launcher3.util.Executors.MODEL_EXECUTOR
-import com.android.launcher3.util.PackageManagerHelper
 import com.android.launcher3.util.PackageUserKey
 import com.android.launcher3.util.Preconditions
 import java.io.FileDescriptor
 import java.io.PrintWriter
 import java.util.concurrent.CancellationException
 import java.util.function.Consumer
+import javax.inject.Inject
+import javax.inject.Named
+import javax.inject.Provider
 
 /**
  * Maintains in-memory state of the Launcher. It is expected that there should be only one
  * LauncherModel object held in a static. Also provide APIs for updating the database state for the
  * Launcher.
  */
-class LauncherModel(
-    private val context: Context,
-    private val mApp: LauncherAppState,
+@LauncherAppSingleton
+class LauncherModel
+@Inject
+constructor(
+    @ApplicationContext private val context: Context,
+    private val appProvider: Provider<LauncherAppState>,
     private val iconCache: IconCache,
-    private val widgetsFilterDataProvider: WidgetsFilterDataProvider,
+    private val prefs: LauncherPrefs,
+    private val installQueue: ItemInstallQueue,
     appFilter: AppFilter,
-    mPmHelper: PackageManagerHelper,
-    isPrimaryInstance: Boolean,
+    @Named("ICONS_DB") dbFileName: String?,
+    initializer: ModelInitializer,
+    lifecycle: DaggerSingletonTracker,
+    val modelDelegate: ModelDelegate,
 ) {
 
+    private val widgetsFilterDataProvider = WidgetsFilterDataProvider.newInstance(context)
+
     private val mCallbacksList = ArrayList<BgDataModel.Callbacks>(1)
 
     // < only access in worker thread >
@@ -84,16 +95,6 @@
      */
     private val mBgDataModel = BgDataModel()
 
-    val modelDelegate: ModelDelegate =
-        ModelDelegate.newInstance(
-            context,
-            mApp,
-            mPmHelper,
-            mBgAllAppsList,
-            mBgDataModel,
-            isPrimaryInstance,
-        )
-
     val modelDbController = ModelDbController(context)
 
     private val mLock = Any()
@@ -128,6 +129,14 @@
         }
     }
 
+    init {
+        if (!dbFileName.isNullOrEmpty()) {
+            initializer.initialize(this)
+        }
+        lifecycle.addCloseable { destroy() }
+        modelDelegate.init(this, mBgAllAppsList, mBgDataModel)
+    }
+
     fun newModelCallbacks() = ModelLauncherCallbacks(this::enqueueModelUpdateTask)
 
     /** Adds the provided items to the workspace. */
@@ -140,7 +149,7 @@
         verifyChanges: Boolean,
         cellPosMapper: CellPosMapper?,
         owner: BgDataModel.Callbacks?,
-    ) = ModelWriter(mApp.context, this, mBgDataModel, verifyChanges, cellPosMapper, owner)
+    ) = ModelWriter(context, this, mBgDataModel, verifyChanges, cellPosMapper, owner)
 
     /** Returns the [WidgetsFilterDataProvider] that manages widget filters. */
     fun getWidgetsFilterDataProvider(): WidgetsFilterDataProvider {
@@ -173,15 +182,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))
     }
 
     /**
@@ -212,7 +214,7 @@
             UserCache.ACTION_PROFILE_UNLOCKED ->
                 enqueueModelUpdateTask(UserLockStateChangedTask(user, true))
             Intent.ACTION_MANAGED_PROFILE_REMOVED -> {
-                LauncherPrefs.get(mApp.context).put(LauncherPrefs.WORK_EDU_STEP, 0)
+                prefs.put(LauncherPrefs.WORK_EDU_STEP, 0)
                 forceReload()
             }
             UserCache.ACTION_PROFILE_ADDED,
@@ -243,6 +245,13 @@
         rebindCallbacks()
     }
 
+    /** Reloads the model if it is already in use */
+    fun reloadIfActive() {
+        val wasActive: Boolean
+        synchronized(mLock) { wasActive = mModelLoaded || stopLoader() }
+        if (wasActive) forceReload()
+    }
+
     /** Rebinds all existing callbacks with already loaded model */
     fun rebindCallbacks() {
         if (hasCallbacks()) {
@@ -290,7 +299,7 @@
 
     private fun startLoader(newCallbacks: Array<BgDataModel.Callbacks>): Boolean {
         // Enable queue before starting loader. It will get disabled in Launcher#finishBindingItems
-        ItemInstallQueue.INSTANCE.get(context).pauseModelPush(ItemInstallQueue.FLAG_LOADER_RUNNING)
+        installQueue.pauseModelPush(ItemInstallQueue.FLAG_LOADER_RUNNING)
         synchronized(mLock) {
             // If there is already one running, tell it to stop.
             val wasRunning = stopLoader()
@@ -302,7 +311,12 @@
                 callbacksList.forEach { MAIN_EXECUTOR.execute(it::clearPendingBinds) }
 
                 val launcherBinder =
-                    BaseLauncherBinder(mApp, mBgDataModel, mBgAllAppsList, callbacksList)
+                    BaseLauncherBinder(
+                        appProvider.get(),
+                        mBgDataModel,
+                        mBgAllAppsList,
+                        callbacksList,
+                    )
                 if (bindDirectly) {
                     // Divide the set of loaded items into those that we are binding synchronously,
                     // and everything else that is to be bound normally (asynchronously).
@@ -314,19 +328,20 @@
                     launcherBinder.bindWidgets()
                     return true
                 } else {
-                    mLoaderTask =
+                    val task =
                         LoaderTask(
-                            mApp,
+                            appProvider.get(),
                             mBgAllAppsList,
                             mBgDataModel,
                             this.modelDelegate,
                             launcherBinder,
                             widgetsFilterDataProvider,
                         )
+                    mLoaderTask = task
 
                     // Always post the loader task, instead of running directly
                     // (even on same thread) so that we exit any nested synchronized blocks
-                    MODEL_EXECUTOR.post(mLoaderTask)
+                    MODEL_EXECUTOR.post(task)
                 }
             }
         }
@@ -422,7 +437,7 @@
     /** Called when the labels for the widgets has updated in the icon cache. */
     fun onWidgetLabelsUpdated(updatedPackages: HashSet<String?>, user: UserHandle) {
         enqueueModelUpdateTask { taskController, dataModel, _ ->
-            dataModel.widgetsModel.onPackageIconsUpdated(updatedPackages, user, mApp)
+            dataModel.widgetsModel.onPackageIconsUpdated(updatedPackages, user, appProvider.get())
             taskController.bindUpdatedWidgets(dataModel)
         }
     }
@@ -445,7 +460,13 @@
                 return@execute
             }
             task.execute(
-                ModelTaskController(mApp, mBgDataModel, mBgAllAppsList, this, MAIN_EXECUTOR),
+                ModelTaskController(
+                    appProvider.get(),
+                    mBgDataModel,
+                    mBgAllAppsList,
+                    this,
+                    MAIN_EXECUTOR,
+                ),
                 mBgDataModel,
                 mBgAllAppsList,
             )
@@ -506,8 +527,6 @@
         }
 
     companion object {
-        private const val DEBUG_RECEIVER = false
-
         const val TAG = "Launcher.Model"
     }
 }
diff --git a/src/com/android/launcher3/LauncherProvider.java b/src/com/android/launcher3/LauncherProvider.java
index a526b89..03ecf14 100644
--- a/src/com/android/launcher3/LauncherProvider.java
+++ b/src/com/android/launcher3/LauncherProvider.java
@@ -61,11 +61,10 @@
      */
     @Override
     public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
-        LauncherAppState.INSTANCE.executeIfCreated(appState -> {
-            if (appState.getModel().isModelLoaded()) {
-                appState.getModel().dumpState("", fd, writer, args);
-            }
-        });
+        LauncherModel model = LauncherAppState.INSTANCE.get(getContext()).getModel();
+        if (model.isModelLoaded()) {
+            model.dumpState("", fd, writer, args);
+        }
     }
 
     @Override
diff --git a/src/com/android/launcher3/LauncherState.java b/src/com/android/launcher3/LauncherState.java
index 8c6555e..78ad04b 100644
--- a/src/com/android/launcher3/LauncherState.java
+++ b/src/com/android/launcher3/LauncherState.java
@@ -70,6 +70,7 @@
     public static final int WORKSPACE_PAGE_INDICATOR = 1 << 5;
     public static final int SPLIT_PLACHOLDER_VIEW = 1 << 6;
     public static final int FLOATING_SEARCH_BAR = 1 << 7;
+    public static final int ADD_DESK_BUTTON = 1 << 8;
 
     // Flag indicating workspace has multiple pages visible.
     public static final int FLAG_MULTI_PAGE = BaseState.getFlag(0);
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/PagedView.java b/src/com/android/launcher3/PagedView.java
index 5072e37..d6abb56 100644
--- a/src/com/android/launcher3/PagedView.java
+++ b/src/com/android/launcher3/PagedView.java
@@ -17,6 +17,7 @@
 package com.android.launcher3;
 
 import static com.android.app.animation.Interpolators.SCROLL;
+import static com.android.launcher3.RemoveAnimationSettingsTracker.WINDOW_ANIMATION_SCALE_URI;
 import static com.android.launcher3.compat.AccessibilityManagerCompat.isAccessibilityEnabled;
 import static com.android.launcher3.compat.AccessibilityManagerCompat.isObservedEventType;
 import static com.android.launcher3.testing.shared.TestProtocol.SCROLL_FINISHED_MESSAGE;
@@ -33,7 +34,6 @@
 import android.graphics.Canvas;
 import android.graphics.Rect;
 import android.os.Bundle;
-import android.provider.Settings;
 import android.util.AttributeSet;
 import android.util.Log;
 import android.view.InputDevice;
@@ -1756,8 +1756,8 @@
         }
 
         if (FeatureFlags.IS_STUDIO_BUILD && !Utilities.isRunningInTestHarness()) {
-            duration *= Settings.Global.getFloat(getContext().getContentResolver(),
-                    Settings.Global.WINDOW_ANIMATION_SCALE, 1);
+            duration *= RemoveAnimationSettingsTracker.INSTANCE.get(getContext()).getValue(
+                    WINDOW_ANIMATION_SCALE_URI);
         }
 
         whichPage = validateNewPage(whichPage);
diff --git a/src/com/android/launcher3/RemoveAnimationSettingsTracker.kt b/src/com/android/launcher3/RemoveAnimationSettingsTracker.kt
new file mode 100644
index 0000000..dbc04f1
--- /dev/null
+++ b/src/com/android/launcher3/RemoveAnimationSettingsTracker.kt
@@ -0,0 +1,110 @@
+/*
+ * 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
+
+import android.content.Context
+import android.database.ContentObserver
+import android.net.Uri
+import android.os.Handler
+import android.os.Looper
+import android.provider.Settings
+import android.provider.Settings.Global.ANIMATOR_DURATION_SCALE
+import android.provider.Settings.Global.TRANSITION_ANIMATION_SCALE
+import android.provider.Settings.Global.WINDOW_ANIMATION_SCALE
+import com.android.launcher3.dagger.ApplicationContext
+import com.android.launcher3.dagger.LauncherAppComponent
+import com.android.launcher3.dagger.LauncherAppSingleton
+import com.android.launcher3.util.DaggerSingletonObject
+import com.android.launcher3.util.DaggerSingletonTracker
+import com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR
+import java.util.concurrent.ConcurrentHashMap
+import javax.inject.Inject
+
+/** Tracker Class for when user turns on/off remove animation setting. */
+@LauncherAppSingleton
+class RemoveAnimationSettingsTracker
+@Inject
+constructor(@ApplicationContext val context: Context, tracker: DaggerSingletonTracker) :
+    ContentObserver(Handler(Looper.getMainLooper())) {
+
+    private val contentResolver = context.contentResolver
+
+    /** Caches the last seen value for registered keys. */
+    private val cache: MutableMap<Uri, Float> = ConcurrentHashMap()
+
+    init {
+        UI_HELPER_EXECUTOR.execute {
+            contentResolver.registerContentObserver(WINDOW_ANIMATION_SCALE_URI, false, this)
+            contentResolver.registerContentObserver(TRANSITION_ANIMATION_SCALE_URI, false, this)
+            contentResolver.registerContentObserver(ANIMATOR_DURATION_SCALE_URI, false, this)
+        }
+
+        tracker.addCloseable {
+            UI_HELPER_EXECUTOR.execute { contentResolver.unregisterContentObserver(this) }
+        }
+    }
+
+    /**
+     * Returns the value for this classes key from the cache. If not in cache, will call
+     * [updateValue] to fetch.
+     */
+    fun getValue(uri: Uri): Float {
+        return getValue(uri, 1f)
+    }
+
+    /**
+     * Returns the value for this classes key from the cache. If not in cache, will call
+     * [getValueFromSettingsGlobal] to fetch.
+     */
+    private fun getValue(uri: Uri, defaultValue: Float): Float {
+        return cache.computeIfAbsent(uri) { getValueFromSettingsGlobal(uri, defaultValue) }
+    }
+
+    /** Returns if user has opted into having no animation on their device. */
+    fun isRemoveAnimationEnabled(): Boolean {
+        return getValue(WINDOW_ANIMATION_SCALE_URI) == 0f &&
+            getValue(TRANSITION_ANIMATION_SCALE_URI) == 0f &&
+            getValue(ANIMATOR_DURATION_SCALE_URI) == 0f
+    }
+
+    override fun onChange(selfChange: Boolean, uri: Uri?) {
+        if (uri == null) return
+        updateValue(uri)
+    }
+
+    private fun getValueFromSettingsGlobal(uri: Uri, defaultValue: Float = 1f): Float {
+        return Settings.Global.getFloat(contentResolver, uri.lastPathSegment, defaultValue)
+    }
+
+    private fun updateValue(uri: Uri, defaultValue: Float = 1f) {
+        val newValue = getValueFromSettingsGlobal(uri, defaultValue)
+        cache[uri] = newValue
+    }
+
+    companion object {
+        @JvmField
+        val INSTANCE =
+            DaggerSingletonObject(LauncherAppComponent::getRemoveAnimationSettingsTracker)
+        @JvmField
+        val WINDOW_ANIMATION_SCALE_URI: Uri = Settings.Global.getUriFor(WINDOW_ANIMATION_SCALE)
+        @JvmField
+        val TRANSITION_ANIMATION_SCALE_URI: Uri =
+            Settings.Global.getUriFor(TRANSITION_ANIMATION_SCALE)
+        @JvmField
+        val ANIMATOR_DURATION_SCALE_URI: Uri = Settings.Global.getUriFor(ANIMATOR_DURATION_SCALE)
+    }
+}
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index b41a425..94ff441 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -53,6 +53,7 @@
 import android.graphics.PointF;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
+import android.os.Bundle;
 import android.os.Parcelable;
 import android.util.AttributeSet;
 import android.util.Log;
@@ -80,8 +81,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;
@@ -299,6 +300,17 @@
     private final StatsLogManager mStatsLogManager;
 
     private final MSDLPlayerWrapper mMSDLPlayerWrapper;
+
+    private final StateManager.StateListener<LauncherState> mAccessibilityDropListener =
+            new StateListener<>() {
+                @Override
+                public void onStateTransitionComplete(LauncherState finalState) {
+                    if (finalState == NORMAL) {
+                        performAccessibilityActionOnViewTree(Workspace.this);
+                    }
+                }
+            };
+
     @Nullable
     private DragController.DragListener mAccessibilityDragListener;
 
@@ -1454,11 +1466,13 @@
         super.onAttachedToWindow();
         mWallpaperOffset.setWindowToken(getWindowToken());
         computeScroll();
+        mLauncher.getStateManager().addStateListener(mAccessibilityDropListener);
     }
 
     protected void onDetachedFromWindow() {
         super.onDetachedFromWindow();
         mWallpaperOffset.setWindowToken(null);
+        mLauncher.getStateManager().removeStateListener(mAccessibilityDropListener);
     }
 
     @Override
@@ -2239,23 +2253,15 @@
                 // the order of operations in this method related to the StateListener below, please
                 // test that accessibility moves retain focus after accessibility dropping an item.
                 // Accessibility focus must be requested after launcher is back to a normal state
-                mLauncher.getStateManager().addStateListener(new StateListener<LauncherState>() {
-                    @Override
-                    public void onStateTransitionComplete(LauncherState finalState) {
-                        if (finalState == NORMAL) {
-                            cell.performAccessibilityAction(
-                                    AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null);
-                            mLauncher.getStateManager().removeStateListener(this);
-                        }
-                    }
-                });
+                cell.setTag(R.id.perform_a11y_action_on_launcher_state_normal_tag,
+                        AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
             }
         }
 
         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
@@ -3580,4 +3586,22 @@
             onEndStateTransition();
         }
     }
+
+    /**
+     * Recursively check view tag {@link R.id.perform_a11y_action_on_launcher_state_normal_tag} and
+     * call {@link View#performAccessibilityAction(int, Bundle)} on view tree. The tag is cleared
+     * after this call.
+     */
+    private static void performAccessibilityActionOnViewTree(View view) {
+        Object tag = view.getTag(R.id.perform_a11y_action_on_launcher_state_normal_tag);
+        if (tag instanceof Integer) {
+            view.performAccessibilityAction((int) tag, null);
+            view.setTag(R.id.perform_a11y_action_on_launcher_state_normal_tag, null);
+        }
+        if (view instanceof ViewGroup viewgroup) {
+            for (int i = 0; i < viewgroup.getChildCount(); i++) {
+                performAccessibilityActionOnViewTree(viewgroup.getChildAt(i));
+            }
+        }
+    }
 }
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 7bd7c3e..150761f 100644
--- a/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
+++ b/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
@@ -18,9 +18,15 @@
 
 import android.content.Context;
 
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherPrefs;
-import com.android.launcher3.graphics.IconShape;
+import com.android.launcher3.RemoveAnimationSettingsTracker;
+import com.android.launcher3.graphics.GridCustomizationsProxy;
 import com.android.launcher3.graphics.ThemeManager;
+import com.android.launcher3.icons.LauncherIcons.IconPool;
 import com.android.launcher3.model.ItemInstallQueue;
 import com.android.launcher3.pm.InstallSessionHelper;
 import com.android.launcher3.pm.UserCache;
@@ -42,6 +48,8 @@
 
 import dagger.BindsInstance;
 
+import javax.inject.Named;
+
 /**
  * Launcher base component for Dagger injection.
  *
@@ -55,7 +63,6 @@
     ApiWrapper getApiWrapper();
     CustomWidgetManager getCustomWidgetManager();
     DynamicResource getDynamicResource();
-    IconShape getIconShape();
     InstallSessionHelper getInstallSessionHelper();
     ItemInstallQueue getItemInstallQueue();
     RefreshRateTracker getRefreshRateTracker();
@@ -72,10 +79,17 @@
     DisplayController getDisplayController();
     WallpaperColorHints getWallpaperColorHints();
     LockedUserState getLockedUserState();
+    InvariantDeviceProfile getIDP();
+    IconPool getIconPool();
+    RemoveAnimationSettingsTracker getRemoveAnimationSettingsTracker();
+    LauncherAppState getLauncherAppState();
+    GridCustomizationsProxy getGridCustomizationsProxy();
 
     /** Builder for LauncherBaseAppComponent. */
     interface Builder {
         @BindsInstance Builder appContext(@ApplicationContext Context context);
+        @BindsInstance Builder iconsDbName(@Nullable @Named("ICONS_DB") String dbFileName);
+        @BindsInstance Builder setSafeModeEnabled(@Named("SAFE_MODE") boolean safeModeEnabled);
         LauncherBaseAppComponent build();
     }
 }
diff --git a/src/com/android/launcher3/dagger/LauncherComponentProvider.kt b/src/com/android/launcher3/dagger/LauncherComponentProvider.kt
index 5015e54..6199149 100644
--- a/src/com/android/launcher3/dagger/LauncherComponentProvider.kt
+++ b/src/com/android/launcher3/dagger/LauncherComponentProvider.kt
@@ -39,14 +39,20 @@
 
         // Create a new component
         return Holder(
-                DaggerLauncherAppComponent.builder().appContext(app).build()
-                    as LauncherAppComponent,
+                DaggerLauncherAppComponent.builder()
+                    .appContext(app)
+                    .setSafeModeEnabled(true)
+                    .build() as LauncherAppComponent,
                 existingFilter,
             )
             .apply { inflater.filter = this }
             .component
     }
 
+    /** Extension method easily access LauncherAppComponent */
+    val Context.appComponent: LauncherAppComponent
+        get() = get(this)
+
     private data class Holder(
         val component: LauncherAppComponent,
         private val filter: LayoutInflater.Filter?,
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..981b78b 100644
--- a/src/com/android/launcher3/dragndrop/DragView.java
+++ b/src/com/android/launcher3/dragndrop/DragView.java
@@ -60,9 +60,9 @@
 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.icons.IconNormalizer;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.util.RunnableList;
 import com.android.launcher3.views.ActivityContext;
@@ -246,10 +246,12 @@
     public void setItemInfo(final ItemInfo info) {
         // Load the adaptive icon on a background thread and add the view in ui thread.
         MODEL_EXECUTOR.getHandler().postAtFrontOfQueue(() -> {
+            ThemeManager themeManager = ThemeManager.INSTANCE.get(getContext());
             int w = mWidth;
             int h = mHeight;
             Pair<AdaptiveIconDrawable, Drawable> fullDrawable = Utilities.getFullDrawable(
-                    mActivity, info, w, h, true /* shouldThemeIcon */);
+                    mActivity, info, w, h,
+                    themeManager.isIconThemeEnabled());
             if (fullDrawable != null) {
                 AdaptiveIconDrawable adaptiveIcon = fullDrawable.first;
                 int blurMargin = (int) mActivity.getResources()
@@ -261,12 +263,7 @@
                 // be scaled down due to icon normalization.
                 mBadge = fullDrawable.second;
                 FastBitmapDrawable.setBadgeBounds(mBadge, bounds);
-
-                try (LauncherIcons li = LauncherIcons.obtain(mActivity)) {
-                    // Since we just want the scale, avoid heavy drawing operations
-                    Utilities.scaleRectAboutCenter(bounds, li.getNormalizer().getScale(
-                            new AdaptiveIconDrawable(new ColorDrawable(Color.BLACK), null)));
-                }
+                Utilities.scaleRectAboutCenter(bounds, IconNormalizer.ICON_VISIBLE_AREA_FACTOR);
 
                 // Shrink very tiny bit so that the clip path is smaller than the original bitmap
                 // that has anti aliased edges and shadows.
@@ -274,9 +271,8 @@
                 Utilities.scaleRectAboutCenter(shrunkBounds, 0.98f);
                 adaptiveIcon.setBounds(shrunkBounds);
 
-                IconShape iconShape = IconShape.INSTANCE.get(getContext());
                 final Path mask = (adaptiveIcon instanceof FolderAdaptiveIcon
-                        ? iconShape.getFolderShape() : iconShape.getShape())
+                        ? themeManager.getFolderShape() : themeManager.getIconShape())
                         .getPath(shrunkBounds);
 
                 mTranslateX = new SpringFloatValue(DragView.this,
@@ -567,7 +563,7 @@
         return mContentViewParent;
     }
 
-    /** Return true if {@link mContent} is a {@link AppWidgetHostView}. */
+    /** Return true if {@link #mContent} is a {@link AppWidgetHostView}. */
     public boolean containsAppWidgetHostView() {
         return mContent instanceof AppWidgetHostView;
     }
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/GridCustomizationsProvider.java b/src/com/android/launcher3/graphics/GridCustomizationsProxy.java
similarity index 82%
rename from src/com/android/launcher3/graphics/GridCustomizationsProvider.java
rename to src/com/android/launcher3/graphics/GridCustomizationsProxy.java
index 12c65c7..70b9f46 100644
--- a/src/com/android/launcher3/graphics/GridCustomizationsProvider.java
+++ b/src/com/android/launcher3/graphics/GridCustomizationsProxy.java
@@ -22,14 +22,11 @@
 
 import static java.util.Objects.requireNonNullElse;
 
-import android.content.ContentProvider;
 import android.content.ContentValues;
 import android.content.Context;
-import android.content.pm.PackageManager;
 import android.database.Cursor;
 import android.database.MatrixCursor;
 import android.net.Uri;
-import android.os.Binder;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder.DeathRecipient;
@@ -45,15 +42,20 @@
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherModel;
 import com.android.launcher3.LauncherPrefs;
+import com.android.launcher3.dagger.ApplicationContext;
+import com.android.launcher3.dagger.LauncherAppSingleton;
 import com.android.launcher3.model.BgDataModel;
 import com.android.launcher3.shapes.IconShapeModel;
 import com.android.launcher3.shapes.ShapesProvider;
+import com.android.launcher3.util.ContentProviderProxy.ProxyProvider;
+import com.android.launcher3.util.DaggerSingletonTracker;
 import com.android.launcher3.util.Executors;
 import com.android.launcher3.util.Preconditions;
 import com.android.launcher3.util.RunnableList;
 import com.android.systemui.shared.Flags;
 
 import java.lang.ref.WeakReference;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
@@ -62,6 +64,8 @@
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ExecutionException;
 
+import javax.inject.Inject;
+
 /**
  * Exposes various launcher grid options and allows the caller to change them.
  * APIs:
@@ -76,7 +80,7 @@
  *          rows: number of rows in the grid
  *          cols: number of columns in the grid
  *          preview_count: number of previews available for this grid option. The preview uri
- *                         looks like /preview/<grid-name>/<preview index starting with 0>
+ *                         looks like /preview/[grid-name]/[preview index starting with 0]
  *          is_default: true if this grid option is currently set to the system
  *
  *     /get_preview: Open a file stream for the grid preview
@@ -85,7 +89,8 @@
  *          shape_key: key of the shape to apply
  *          name: key of the grid to apply
  */
-public class GridCustomizationsProvider extends ContentProvider {
+@LauncherAppSingleton
+public class GridCustomizationsProxy implements ProxyProvider {
 
     private static final String TAG = "GridCustomizationsProvider";
 
@@ -126,23 +131,35 @@
     private static final int MESSAGE_ID_UPDATE_GRID = 7414;
     private static final int MESSAGE_ID_UPDATE_COLOR = 856;
 
-    private static final String DEFAULT_SHAPE_KEY = "circle";
-
     // Set of all active previews used to track duplicate memory allocations
     private final Set<PreviewLifecycleObserver> mActivePreviews =
             Collections.newSetFromMap(new ConcurrentHashMap<>());
 
-    @Override
-    public boolean onCreate() {
-        return true;
+    private final Context mContext;
+    private final ThemeManager mThemeManager;
+    private final LauncherPrefs mPrefs;
+    private final InvariantDeviceProfile mIdp;
+
+    @Inject
+    GridCustomizationsProxy(
+            @ApplicationContext Context context,
+            ThemeManager themeManager,
+            LauncherPrefs prefs,
+            InvariantDeviceProfile idp,
+            DaggerSingletonTracker lifeCycle
+    ) {
+        mContext = context;
+        mThemeManager = themeManager;
+        mPrefs = prefs;
+        mIdp = idp;
+        lifeCycle.addCloseable(() -> mActivePreviews.forEach(PreviewLifecycleObserver::binderDied));
     }
 
     @Override
     public Cursor query(Uri uri, String[] projection, String selection,
             String[] selectionArgs, String sortOrder) {
-        Context context = getContext();
         String path = uri.getPath();
-        if (context == null || path == null) {
+        if (path == null) {
             return null;
         }
 
@@ -151,20 +168,19 @@
                 if (Flags.newCustomizationPickerUi()) {
                     MatrixCursor cursor = new MatrixCursor(new String[]{
                             KEY_SHAPE_KEY, KEY_SHAPE_TITLE, KEY_PATH, KEY_IS_DEFAULT});
-                    String currentShapePath =
-                            ThemeManager.INSTANCE.get(context).getIconState().getIconMask();
-                    Optional<IconShapeModel> selectedShape = ShapesProvider.INSTANCE.getIconShapes()
-                            .values()
-                            .stream()
-                            .filter(shape -> shape.getPathString().equals(currentShapePath))
-                            .findFirst();
+                    String currentShapePath = mThemeManager.getIconState().getIconMask();
+                    Optional<IconShapeModel> selectedShape = Arrays.stream(
+                            ShapesProvider.INSTANCE.getIconShapes()).filter(
+                                    shape -> shape.getPathString().equals(currentShapePath)
+                    ).findFirst();
                     // Handle default for when current shape doesn't match new shapes.
                     if (selectedShape.isEmpty()) {
-                        selectedShape = Optional.ofNullable(ShapesProvider.INSTANCE.getIconShapes()
-                                .get(DEFAULT_SHAPE_KEY));
+                        selectedShape = Optional.of(Arrays.stream(
+                                ShapesProvider.INSTANCE.getIconShapes()
+                        ).findFirst().get());
                     }
 
-                    for (IconShapeModel shape : ShapesProvider.INSTANCE.getIconShapes().values()) {
+                    for (IconShapeModel shape : ShapesProvider.INSTANCE.getIconShapes()) {
                         cursor.newRow()
                                 .add(KEY_SHAPE_KEY, shape.getKey())
                                 .add(KEY_SHAPE_TITLE, shape.getTitle())
@@ -180,8 +196,7 @@
                 MatrixCursor cursor = new MatrixCursor(new String[]{
                         KEY_NAME, KEY_GRID_TITLE, KEY_ROWS, KEY_COLS, KEY_PREVIEW_COUNT,
                         KEY_IS_DEFAULT, KEY_GRID_ICON_ID});
-                InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(getContext());
-                List<GridOption> gridOptionList = idp.parseAllGridOptions(getContext());
+                List<GridOption> gridOptionList = mIdp.parseAllGridOptions(mContext);
                 if (com.android.launcher3.Flags.oneGridSpecs()) {
                     gridOptionList.sort(Comparator
                             .comparingInt((GridOption option) -> option.numColumns)
@@ -194,8 +209,8 @@
                             .add(KEY_ROWS, gridOption.numRows)
                             .add(KEY_COLS, gridOption.numColumns)
                             .add(KEY_PREVIEW_COUNT, 1)
-                            .add(KEY_IS_DEFAULT, idp.numColumns == gridOption.numColumns
-                                    && idp.numRows == gridOption.numRows)
+                            .add(KEY_IS_DEFAULT, mIdp.numColumns == gridOption.numColumns
+                                    && mIdp.numRows == gridOption.numRows)
                             .add(KEY_GRID_ICON_ID, gridOption.gridIconId);
                 }
                 return cursor;
@@ -203,8 +218,7 @@
             case GET_ICON_THEMED:
             case ICON_THEMED: {
                 MatrixCursor cursor = new MatrixCursor(new String[]{BOOLEAN_VALUE});
-                cursor.newRow().add(BOOLEAN_VALUE,
-                        ThemeManager.INSTANCE.get(getContext()).isMonoThemeEnabled() ? 1 : 0);
+                cursor.newRow().add(BOOLEAN_VALUE, mThemeManager.isMonoThemeEnabled() ? 1 : 0);
                 return cursor;
             }
             default:
@@ -213,38 +227,21 @@
     }
 
     @Override
-    public String getType(Uri uri) {
-        return "vnd.android.cursor.dir/launcher_grid";
-    }
-
-    @Override
-    public Uri insert(Uri uri, ContentValues initialValues) {
-        return null;
-    }
-
-    @Override
-    public int delete(Uri uri, String selection, String[] selectionArgs) {
-        return 0;
-    }
-
-    @Override
     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
         String path = uri.getPath();
-        Context context = getContext();
-        if (path == null || context == null) {
+        if (path == null) {
             return 0;
         }
         switch (path) {
             case KEY_DEFAULT_GRID: {
                 if (Flags.newCustomizationPickerUi()) {
-                    LauncherPrefs.INSTANCE.get(context).put(PREF_ICON_SHAPE,
+                    mPrefs.put(PREF_ICON_SHAPE,
                             requireNonNullElse(values.getAsString(KEY_SHAPE_KEY), ""));
                 }
                 String gridName = values.getAsString(KEY_NAME);
-                InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(context);
                 // Verify that this is a valid grid option
                 GridOption match = null;
-                for (GridOption option : idp.parseAllGridOptions(context)) {
+                for (GridOption option : mIdp.parseAllGridOptions(mContext)) {
                     String name = option.name;
                     if (name != null && name.equals(gridName)) {
                         match = option;
@@ -255,23 +252,22 @@
                     return 0;
                 }
 
-                idp.setCurrentGrid(context, gridName);
+                mIdp.setCurrentGrid(mContext, gridName);
                 if (Flags.newCustomizationPickerUi()) {
                     try {
                         // Wait for device profile to be fully reloaded and applied to the launcher
-                        loadModelSync(context);
+                        loadModelSync(mContext);
                     } catch (ExecutionException | InterruptedException e) {
                         Log.e(TAG, "Fail to load model", e);
                     }
                 }
-                context.getContentResolver().notifyChange(uri, null);
+                mContext.getContentResolver().notifyChange(uri, null);
                 return 1;
             }
             case ICON_THEMED:
             case SET_ICON_THEMED: {
-                ThemeManager.INSTANCE.get(context)
-                        .setMonoThemeEnabled(values.getAsBoolean(BOOLEAN_VALUE));
-                context.getContentResolver().notifyChange(uri, null);
+                mThemeManager.setMonoThemeEnabled(values.getAsBoolean(BOOLEAN_VALUE));
+                mContext.getContentResolver().notifyChange(uri, null);
                 return 1;
             }
             default:
@@ -298,17 +294,6 @@
 
     @Override
     public Bundle call(@NonNull String method, String arg, Bundle extras) {
-        Context context = getContext();
-        if (context == null) {
-            return null;
-        }
-
-        if (context.checkPermission("android.permission.BIND_WALLPAPER",
-                Binder.getCallingPid(), Binder.getCallingUid())
-                != PackageManager.PERMISSION_GRANTED) {
-            return null;
-        }
-
         if (METHOD_GET_PREVIEW.equals(method)) {
             return getPreview(extras);
         } else {
@@ -317,14 +302,10 @@
     }
 
     private synchronized Bundle getPreview(Bundle request) {
-        Context context = getContext();
-        if (context == null) {
-            return null;
-        }
         RunnableList lifeCycleTracker = new RunnableList();
         try {
             PreviewSurfaceRenderer renderer = new PreviewSurfaceRenderer(
-                    getContext(), lifeCycleTracker, request);
+                    mContext, lifeCycleTracker, request);
             PreviewLifecycleObserver observer =
                     new PreviewLifecycleObserver(lifeCycleTracker, renderer);
 
@@ -387,7 +368,9 @@
                     if (Flags.newCustomizationPickerUi()
                             && com.android.launcher3.Flags.enableLauncherIconShapes()) {
                         String shapeKey = message.getData().getString(KEY_SHAPE_KEY);
-                        renderer.updateShape(shapeKey);
+                        if (!TextUtils.isEmpty(shapeKey)) {
+                            renderer.updateShape(shapeKey);
+                        }
                     }
                     break;
                 case MESSAGE_ID_UPDATE_GRID:
diff --git a/src/com/android/launcher3/graphics/LauncherCustomizationProvider.kt b/src/com/android/launcher3/graphics/LauncherCustomizationProvider.kt
new file mode 100644
index 0000000..c949e2e
--- /dev/null
+++ b/src/com/android/launcher3/graphics/LauncherCustomizationProvider.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.launcher3.graphics
+
+import android.content.Context
+import android.net.Uri
+import com.android.launcher3.dagger.LauncherComponentProvider.appComponent
+import com.android.launcher3.util.ContentProviderProxy
+
+/** Provider for various Launcher customizations exposed via a ContentProvider API */
+class LauncherCustomizationProvider : ContentProviderProxy() {
+
+    override fun getProxy(ctx: Context): ProxyProvider? = ctx.appComponent.gridCustomizationsProxy
+
+    override fun getType(uri: Uri) = "vnd.android.cursor.dir/launcher_grid"
+}
diff --git a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
index 911064c..740b87b 100644
--- a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
+++ b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
@@ -24,9 +24,11 @@
 import static com.android.launcher3.BubbleTextView.DISPLAY_WORKSPACE;
 import static com.android.launcher3.DeviceProfile.DEFAULT_SCALE;
 import static com.android.launcher3.Hotseat.ALPHA_CHANNEL_PREVIEW_RENDERER;
+import static com.android.launcher3.LauncherPrefs.GRID_NAME;
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT;
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
 import static com.android.launcher3.Utilities.SHOULD_SHOW_FIRST_PAGE_WIDGET;
+import static com.android.launcher3.graphics.ThemeManager.PREF_ICON_SHAPE;
 import static com.android.launcher3.model.ModelUtils.currentScreenContentFilter;
 
 import android.app.Fragment;
@@ -66,7 +68,6 @@
 import com.android.launcher3.Hotseat;
 import com.android.launcher3.InsettableFrameLayout;
 import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherPrefs;
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.ProxyPrefs;
@@ -137,16 +138,15 @@
 
         private final String mPrefName;
 
-        public PreviewContext(Context base, InvariantDeviceProfile idp) {
+        public PreviewContext(Context base, String gridName, String shapeKey) {
             super(base);
             mPrefName = "preview-" + UUID.randomUUID().toString();
-            initDaggerComponent(DaggerLauncherPreviewRenderer_PreviewAppComponent.builder()
-                    .bindPrefs(new ProxyPrefs(
-                            this, getSharedPreferences(mPrefName, MODE_PRIVATE))));
-
-            putObject(InvariantDeviceProfile.INSTANCE, idp);
-            putObject(LauncherAppState.INSTANCE,
-                    new LauncherAppState(this, null /* iconCacheFileName */));
+            LauncherPrefs prefs =
+                    new ProxyPrefs(this, getSharedPreferences(mPrefName, MODE_PRIVATE));
+            prefs.put(GRID_NAME, gridName);
+            prefs.put(PREF_ICON_SHAPE, shapeKey);
+            initDaggerComponent(
+                    DaggerLauncherPreviewRenderer_PreviewAppComponent.builder().bindPrefs(prefs));
         }
 
         @Override
@@ -192,8 +192,8 @@
                 this::getAppWidgetScale).build();
         if (context instanceof PreviewContext) {
             Context tempContext = ((PreviewContext) context).getBaseContext();
-            mDpOrig = new InvariantDeviceProfile(tempContext, InvariantDeviceProfile
-                    .getCurrentGridName(tempContext)).getDeviceProfile(tempContext)
+            mDpOrig = InvariantDeviceProfile.INSTANCE.get(tempContext)
+                    .getDeviceProfile(tempContext)
                     .copy(tempContext);
         } else {
             mDpOrig = mDp;
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/PreviewSurfaceRenderer.java b/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
index 7a60814..8af18f5 100644
--- a/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
+++ b/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
@@ -20,6 +20,7 @@
 import static android.content.res.Configuration.UI_MODE_NIGHT_YES;
 import static android.view.Display.DEFAULT_DISPLAY;
 
+import static com.android.launcher3.LauncherPrefs.GRID_NAME;
 import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME;
 import static com.android.launcher3.graphics.ThemeManager.PREF_ICON_SHAPE;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
@@ -39,6 +40,7 @@
 import android.util.SparseIntArray;
 import android.view.ContextThemeWrapper;
 import android.view.Display;
+import android.view.Surface;
 import android.view.SurfaceControlViewHost;
 import android.view.SurfaceControlViewHost.SurfacePackage;
 import android.view.View;
@@ -60,10 +62,8 @@
 import com.android.launcher3.model.BaseLauncherBinder;
 import com.android.launcher3.model.BgDataModel;
 import com.android.launcher3.model.BgDataModel.Callbacks;
-import com.android.launcher3.model.GridSizeMigrationDBController;
 import com.android.launcher3.model.LoaderTask;
 import com.android.launcher3.model.ModelDbController;
-import com.android.launcher3.provider.LauncherDbUtils;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.RunnableList;
 import com.android.launcher3.util.Themes;
@@ -72,7 +72,6 @@
 
 import java.util.ArrayList;
 import java.util.Map;
-import java.util.Objects;
 import java.util.concurrent.TimeUnit;
 
 /** Render preview using surface view. */
@@ -117,13 +116,14 @@
         mGridName = bundle.getString("name");
         bundle.remove("name");
         if (mGridName == null) {
-            mGridName = InvariantDeviceProfile.getCurrentGridName(context);
+            mGridName = LauncherPrefs.get(context).get(GRID_NAME);
         }
+        mShapeKey = LauncherPrefs.get(context).get(PREF_ICON_SHAPE);
         mWallpaperColors = bundle.getParcelable(KEY_COLORS);
         if (Flags.newCustomizationPickerUi()) {
             updateColorOverrides(bundle);
         }
-        mHideQsb = bundle.getBoolean(GridCustomizationsProvider.KEY_HIDE_BOTTOM_ROW);
+        mHideQsb = bundle.getBoolean(GridCustomizationsProxy.KEY_HIDE_BOTTOM_ROW);
 
         mHostToken = bundle.getBinder(KEY_HOST_TOKEN);
         mWidth = bundle.getInt(KEY_VIEW_WIDTH);
@@ -224,8 +224,8 @@
      *
      * @param shapeKey key for the IconShape model
      */
-    public void updateShape(@Nullable String shapeKey) {
-        if (Objects.equals(mShapeKey, shapeKey)) {
+    public void updateShape(String shapeKey) {
+        if (shapeKey.equals(mShapeKey)) {
             Log.w(TAG, "Preview shape already set, skipping. shape=" + mShapeKey);
             return;
         }
@@ -286,6 +286,20 @@
             }
             context = context.createConfigurationContext(configuration);
         }
+        if (InvariantDeviceProfile.INSTANCE.get(context).isFixedLandscape) {
+            Configuration configuration = new Configuration(
+                    context.getResources().getConfiguration()
+            );
+            int width = configuration.screenWidthDp;
+            int height = configuration.screenHeightDp;
+            if (configuration.screenHeightDp > configuration.screenWidthDp) {
+                configuration.screenWidthDp = height;
+                configuration.screenHeightDp = width;
+                configuration.orientation = Surface.ROTATION_90;
+            }
+            context = context.createConfigurationContext(configuration);
+        }
+
         if (Flags.newCustomizationPickerUi()) {
             if (mPreviewColorOverride != null) {
                 LocalColorExtractor.newInstance(context)
@@ -316,24 +330,11 @@
     @WorkerThread
     private void loadModelData() {
         final Context inflationContext = getPreviewContext();
-        final InvariantDeviceProfile idp = new InvariantDeviceProfile(inflationContext, mGridName);
-        if (GridSizeMigrationDBController.needsToMigrate(inflationContext, idp)
-                || mShapeKey != null) {
+        if (!mGridName.equals(LauncherPrefs.INSTANCE.get(mContext).get(GRID_NAME))
+                || !mShapeKey.equals(LauncherPrefs.INSTANCE.get(mContext).get(PREF_ICON_SHAPE))) {
             // Start the migration
-            PreviewContext previewContext = new PreviewContext(inflationContext, idp);
-            if (mShapeKey != null) {
-                LauncherPrefs.INSTANCE.get(previewContext).put(PREF_ICON_SHAPE, mShapeKey);
-            }
-            // Copy existing data to preview DB
-            LauncherDbUtils.copyTable(LauncherAppState.getInstance(mContext)
-                            .getModel().getModelDbController().getDb(),
-                    TABLE_NAME,
-                    LauncherAppState.getInstance(previewContext)
-                            .getModel().getModelDbController().getDb(),
-                    TABLE_NAME,
-                    mContext);
-            LauncherAppState.getInstance(previewContext)
-                    .getModel().getModelDbController().clearEmptyDbFlag();
+            PreviewContext previewContext =
+                    new PreviewContext(inflationContext, mGridName, mShapeKey);
 
             BgDataModel bgModel = new BgDataModel();
             new LoaderTask(
@@ -348,6 +349,7 @@
 
                 @Override
                 public void run() {
+                    InvariantDeviceProfile idp = LauncherAppState.getIDP(previewContext);
                     DeviceProfile deviceProfile = idp.getDeviceProfile(previewContext);
                     String query =
                             LauncherSettings.Favorites.SCREEN + " = " + Workspace.FIRST_SCREEN_ID
@@ -371,7 +373,7 @@
             LauncherAppState.getInstance(inflationContext).getModel().loadAsync(dataModel -> {
                 if (dataModel != null) {
                     MAIN_EXECUTOR.execute(() -> renderView(inflationContext, dataModel, null,
-                            null, idp));
+                            null, LauncherAppState.getIDP(inflationContext)));
                 } else {
                     Log.e(TAG, "Model loading failed");
                 }
@@ -396,15 +398,28 @@
         }
         renderer.hideBottomRow(mHideQsb);
         View view = renderer.getRenderedView(dataModel, widgetProviderInfoMap);
-        // This aspect scales the view to fit in the surface and centers it
-        final float scale = Math.min(mWidth / (float) view.getMeasuredWidth(),
-                mHeight / (float) view.getMeasuredHeight());
-        view.setScaleX(scale);
-        view.setScaleY(scale);
+
         view.setPivotX(0);
         view.setPivotY(0);
-        view.setTranslationX((mWidth - scale * view.getWidth()) / 2);
-        view.setTranslationY((mHeight - scale * view.getHeight()) / 2);
+        if (idp.isFixedLandscape) {
+            final float scale = Math.min(mHeight / (float) view.getMeasuredWidth(),
+                    mWidth / (float) view.getMeasuredHeight());
+            view.setScaleX(scale);
+            view.setScaleY(scale);
+            view.setRotation(90);
+            view.setTranslationX((mHeight - scale * view.getWidth()) / 2 + mWidth);
+            view.setTranslationY((mWidth - scale * view.getHeight()) / 2);
+        } else {
+            // This aspect scales the view to fit in the surface and centers it
+            final float scale = Math.min(mWidth / (float) view.getMeasuredWidth(),
+                    mHeight / (float) view.getMeasuredHeight());
+            view.setScaleX(scale);
+            view.setScaleY(scale);
+            view.setTranslationX((mWidth - scale * view.getWidth()) / 2);
+            view.setTranslationY((mHeight - scale * view.getHeight()) / 2);
+        }
+
+
         if (!Flags.newCustomizationPickerUi()) {
             view.setAlpha(0);
             view.animate().alpha(1)
diff --git a/src/com/android/launcher3/graphics/IconShape.kt b/src/com/android/launcher3/graphics/ShapeDelegate.kt
similarity index 80%
rename from src/com/android/launcher3/graphics/IconShape.kt
rename to src/com/android/launcher3/graphics/ShapeDelegate.kt
index eac3440..9033eac 100644
--- a/src/com/android/launcher3/graphics/IconShape.kt
+++ b/src/com/android/launcher3/graphics/ShapeDelegate.kt
@@ -42,78 +42,37 @@
 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 +138,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 +251,6 @@
     }
 
     companion object {
-        @JvmField var INSTANCE = DaggerSingletonObject(LauncherAppComponent::getIconShape)
 
         const val TAG = "IconShape"
         const val DEFAULT_PATH_SIZE = 100f
@@ -312,7 +275,6 @@
             }
         }
 
-        @VisibleForTesting
         fun pickBestShape(shapeStr: String): ShapeDelegate {
             val baseShape =
                 if (shapeStr.isNotEmpty()) {
@@ -332,7 +294,6 @@
             return pickBestShape(baseShape, shapeStr)
         }
 
-        @VisibleForTesting
         fun pickBestShape(baseShape: Path, shapeStr: String): ShapeDelegate {
             val calcAreaDiff = areaDiffCalculator(baseShape)
 
diff --git a/src/com/android/launcher3/graphics/ThemeManager.kt b/src/com/android/launcher3/graphics/ThemeManager.kt
index 242220a..ebb7ea0 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(MAIN_EXECUTOR) { verifyIconState() }
-        receiver.registerPkgActions(context, "android", ACTION_OVERLAY_CHANGED)
+        val receiver = SimpleBroadcastReceiver(context, MAIN_EXECUTOR) { verifyIconState() }
+        receiver.registerPkgActions("android", ACTION_OVERLAY_CHANGED)
 
+        val keys = (iconControllerFactory.prefKeys + PREF_ICON_SHAPE)
+
+        val keysArray = keys.toTypedArray()
+        val prefKeySet = keys.map { it.sharedPrefKey }
         val prefListener = LauncherPrefChangeListener { key ->
-            when (key) {
-                KEY_THEMED_ICONS,
-                KEY_ICON_SHAPE -> verifyIconState()
-            }
+            if (prefKeySet.contains(key)) verifyIconState()
         }
-        prefs.addListener(prefListener, THEMED_ICONS, PREF_ICON_SHAPE)
-
+        prefs.addListener(prefListener, *keysArray)
         lifecycle.addCloseable {
-            receiver.unregisterReceiverSafely(context)
-            prefs.removeListener(prefListener)
+            receiver.unregisterReceiverSafely()
+            prefs.removeListener(prefListener, *keysArray)
         }
     }
 
-    protected fun verifyIconState() {
-        val newState = parseIconState()
+    private fun verifyIconState() {
+        val newState = parseIconState(iconState)
         if (newState == iconState) return
         iconState = newState
 
@@ -91,10 +100,10 @@
 
     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 }
+                ShapesProvider.iconShapes.firstOrNull { it.key == shapeOverride }
             }
         val iconMask =
             when {
@@ -102,22 +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"
     }
@@ -127,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/CacheableShortcutInfo.kt b/src/com/android/launcher3/icons/CacheableShortcutInfo.kt
index 225e12f..50dd146 100644
--- a/src/com/android/launcher3/icons/CacheableShortcutInfo.kt
+++ b/src/com/android/launcher3/icons/CacheableShortcutInfo.kt
@@ -30,6 +30,7 @@
 import com.android.launcher3.icons.cache.BaseIconCache
 import com.android.launcher3.icons.cache.CachingLogic
 import com.android.launcher3.shortcuts.ShortcutKey
+import com.android.launcher3.util.ApiWrapper
 import com.android.launcher3.util.ApplicationInfoWrapper
 import com.android.launcher3.util.PackageUserKey
 import com.android.launcher3.util.Themes
@@ -114,7 +115,15 @@
                         d,
                         IconOptions()
                             .setExtractedColor(Themes.getColorAccent(context))
-                            .setSourceHint(getSourceHint(info, cache)),
+                            .setSourceHint(
+                                getSourceHint(info, cache)
+                                    .copy(
+                                        isFileDrawable =
+                                            ApiWrapper.INSTANCE[context].isFileDrawable(
+                                                info.shortcutInfo
+                                            )
+                                    )
+                            ),
                     )
                 } ?: BitmapInfo.LOW_RES_INFO
         }
diff --git a/src/com/android/launcher3/icons/IconCache.java b/src/com/android/launcher3/icons/IconCache.java
index 9f99e8f..119a6b1 100644
--- a/src/com/android/launcher3/icons/IconCache.java
+++ b/src/com/android/launcher3/icons/IconCache.java
@@ -20,6 +20,7 @@
 import static com.android.launcher3.icons.cache.CacheLookupFlag.DEFAULT_LOOKUP_FLAG;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+import static com.android.launcher3.util.LooperExecutor.CALLER_ICON_CACHE;
 import static com.android.launcher3.widget.WidgetSections.NO_CATEGORY;
 
 import static java.util.stream.Collectors.groupingBy;
@@ -35,7 +36,6 @@
 import android.database.Cursor;
 import android.database.sqlite.SQLiteException;
 import android.os.Looper;
-import android.os.Process;
 import android.os.Trace;
 import android.os.UserHandle;
 import android.text.TextUtils;
@@ -51,6 +51,8 @@
 import com.android.launcher3.Flags;
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.dagger.ApplicationContext;
+import com.android.launcher3.dagger.LauncherAppSingleton;
 import com.android.launcher3.icons.cache.BaseIconCache;
 import com.android.launcher3.icons.cache.CacheLookupFlag;
 import com.android.launcher3.icons.cache.CachedObject;
@@ -66,6 +68,7 @@
 import com.android.launcher3.pm.UserCache;
 import com.android.launcher3.util.CancellableTask;
 import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.DaggerSingletonTracker;
 import com.android.launcher3.util.InstantAppResolver;
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.widget.WidgetSections;
@@ -79,9 +82,13 @@
 import java.util.function.Supplier;
 import java.util.stream.Stream;
 
+import javax.inject.Inject;
+import javax.inject.Named;
+
 /**
  * Cache of application icons.  Icons can be made from any thread.
  */
+@LauncherAppSingleton
 public class IconCache extends BaseIconCache {
 
     // Shortcut extra which can point to a packageName and can be used to indicate an alternate
@@ -96,24 +103,39 @@
 
     private final LauncherApps mLauncherApps;
     private final UserCache mUserManager;
+    private final InstallSessionHelper mInstallSessionHelper;
     private final InstantAppResolver mInstantAppResolver;
     private final CancellableTask mCancelledTask;
+    private final LauncherIcons.IconPool mIconPool;
 
     private final SparseArray<BitmapInfo> mWidgetCategoryBitmapInfos;
 
     private int mPendingIconRequestCount = 0;
 
-    public IconCache(Context context, InvariantDeviceProfile idp, String dbFileName,
-            IconProvider iconProvider) {
+    @Inject
+    public IconCache(
+            @ApplicationContext Context context,
+            InvariantDeviceProfile idp,
+            @Nullable @Named("ICONS_DB") String dbFileName,
+            UserCache userCache,
+            LauncherIconProvider iconProvider,
+            InstallSessionHelper installSessionHelper,
+            LauncherIcons.IconPool iconPool,
+            DaggerSingletonTracker lifecycle) {
         super(context, dbFileName, MODEL_EXECUTOR.getLooper(),
                 idp.fillResIconDpi, idp.iconBitmapSize, true /* inMemoryCache */, iconProvider);
         mLauncherApps = context.getSystemService(LauncherApps.class);
-        mUserManager = UserCache.INSTANCE.get(context);
+        mUserManager = userCache;
+        mInstallSessionHelper = installSessionHelper;
+        mIconPool = iconPool;
+
         mInstantAppResolver = InstantAppResolver.newInstance(context);
         mWidgetCategoryBitmapInfos = new SparseArray<>();
 
         mCancelledTask = new CancellableTask(() -> null, MAIN_EXECUTOR, c -> { });
         mCancelledTask.cancel();
+
+        lifecycle.addCloseable(this::close);
     }
 
     @Override
@@ -129,7 +151,7 @@
     @NonNull
     @Override
     public BaseIconFactory getIconFactory() {
-        return LauncherIcons.obtain(context);
+        return mIconPool.obtain();
     }
 
     /**
@@ -182,7 +204,7 @@
         Runnable endRunnable;
         if (Looper.myLooper() == Looper.getMainLooper()) {
             if (mPendingIconRequestCount <= 0) {
-                MODEL_EXECUTOR.setThreadPriority(Process.THREAD_PRIORITY_FOREGROUND);
+                MODEL_EXECUTOR.elevatePriority(CALLER_ICON_CACHE);
             }
             mPendingIconRequestCount++;
             endRunnable = this::onIconRequestEnd;
@@ -199,7 +221,7 @@
     private void onIconRequestEnd() {
         mPendingIconRequestCount--;
         if (mPendingIconRequestCount <= 0) {
-            MODEL_EXECUTOR.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+            MODEL_EXECUTOR.restorePriority(CALLER_ICON_CACHE);
         }
     }
 
@@ -279,8 +301,7 @@
         String override = shortcutInfo.getExtras() == null ? null
                 : shortcutInfo.getExtras().getString(EXTRA_SHORTCUT_BADGE_OVERRIDE_PACKAGE);
         if (!TextUtils.isEmpty(override)
-                && InstallSessionHelper.INSTANCE.get(context)
-                .isTrustedPackage(pkg, shortcutInfo.getUserHandle())) {
+                && mInstallSessionHelper.isTrustedPackage(pkg, shortcutInfo.getUserHandle())) {
             pkg = override;
         } else {
             // Try component based badge before trying the normal package badge
@@ -536,7 +557,7 @@
             return;
         }
 
-        try (LauncherIcons li = LauncherIcons.obtain(context)) {
+        try (LauncherIcons li = mIconPool.obtain()) {
             final BitmapInfo tempBitmap = li.createBadgedIconBitmap(
                     context.getDrawable(widgetSection.mSectionDrawable),
                     new BaseIconFactory.IconOptions());
diff --git a/src/com/android/launcher3/icons/LauncherIconProvider.java b/src/com/android/launcher3/icons/LauncherIconProvider.java
index 482360c..7241198 100644
--- a/src/com/android/launcher3/icons/LauncherIconProvider.java
+++ b/src/com/android/launcher3/icons/LauncherIconProvider.java
@@ -30,7 +30,9 @@
 
 import com.android.launcher3.R;
 import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.graphics.IconShape;
+import com.android.launcher3.dagger.ApplicationContext;
+import com.android.launcher3.dagger.LauncherAppSingleton;
+import com.android.launcher3.graphics.ShapeDelegate;
 import com.android.launcher3.graphics.ThemeManager;
 import com.android.launcher3.util.ApiWrapper;
 
@@ -39,9 +41,12 @@
 import java.util.Collections;
 import java.util.Map;
 
+import javax.inject.Inject;
+
 /**
  * Extension of {@link IconProvider} with support for overriding theme icons
  */
+@LauncherAppSingleton
 public class LauncherIconProvider extends IconProvider {
 
     private static final String TAG_ICON = "icon";
@@ -54,13 +59,17 @@
     private Map<String, ThemeData> mThemedIconMap;
 
     private final ApiWrapper mApiWrapper;
-    private final IconShape mIconShape;
+    private final ThemeManager mThemeManager;
 
-    public LauncherIconProvider(Context context) {
+    @Inject
+    public LauncherIconProvider(
+            @ApplicationContext Context context,
+            ThemeManager themeManager,
+            ApiWrapper apiWrapper) {
         super(context);
-        setIconThemeSupported(ThemeManager.INSTANCE.get(context).isMonoThemeEnabled());
-        mApiWrapper = ApiWrapper.INSTANCE.get(context);
-        mIconShape = IconShape.INSTANCE.get(context);
+        mThemeManager = themeManager;
+        mApiWrapper = apiWrapper;
+        setIconThemeSupported(mThemeManager.isMonoThemeEnabled());
     }
 
     /**
@@ -79,7 +88,7 @@
     @Override
     public void updateSystemState() {
         super.updateSystemState();
-        mSystemState += "," + ThemeManager.INSTANCE.get(mContext).getIconState().toUniqueId();
+        mSystemState += "," + mThemeManager.getIconState().toUniqueId();
     }
 
     @Override
@@ -91,7 +100,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.java b/src/com/android/launcher3/icons/LauncherIcons.java
deleted file mode 100644
index 5c6debe..0000000
--- a/src/com/android/launcher3/icons/LauncherIcons.java
+++ /dev/null
@@ -1,164 +0,0 @@
-/*
- * Copyright (C) 2016 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.icons;
-
-import static android.graphics.Color.BLACK;
-
-import static com.android.launcher3.graphics.ThemeManager.PREF_ICON_SHAPE;
-
-import android.content.Context;
-import android.graphics.Canvas;
-import android.graphics.Path;
-import android.graphics.Rect;
-import android.graphics.drawable.AdaptiveIconDrawable;
-import android.os.UserHandle;
-
-import androidx.annotation.NonNull;
-
-import com.android.launcher3.Flags;
-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.pm.UserCache;
-import com.android.launcher3.util.MainThreadInitializedObject;
-import com.android.launcher3.util.SafeCloseable;
-import com.android.launcher3.util.UserIconInfo;
-
-import java.util.concurrent.ConcurrentLinkedQueue;
-
-/**
- * Wrapper class to provide access to {@link BaseIconFactory} and also to provide pool of this class
- * that are threadsafe.
- */
-public class LauncherIcons extends BaseIconFactory implements AutoCloseable {
-
-    private static final float SEVEN_SIDED_COOKIE_SCALE = 72f / 80f;
-    private static final float FOUR_SIDED_COOKIE_SCALE = 72f / 83.4f;
-    private static final float VERY_SUNNY_SCALE = 72f / 92f;
-    private static final float DEFAULT_ICON_SCALE = 1f;
-
-
-    private static final MainThreadInitializedObject<Pool> POOL =
-            new MainThreadInitializedObject<>(Pool::new);
-
-    /**
-     * Return a new Message instance from the global pool. Allows us to
-     * avoid allocating new objects in many cases.
-     */
-    public static LauncherIcons obtain(Context context) {
-        return POOL.get(context).obtain();
-    }
-
-    public static void clearPool(Context context) {
-        POOL.get(context).close();
-    }
-
-    private final ConcurrentLinkedQueue<LauncherIcons> mPool;
-
-    protected LauncherIcons(Context context, int fillResIconDpi, int iconBitmapSize,
-            ConcurrentLinkedQueue<LauncherIcons> pool) {
-        super(context, fillResIconDpi, iconBitmapSize);
-        mThemeController = ThemeManager.INSTANCE.get(context).getThemeController();
-        mPool = pool;
-    }
-
-    /**
-     * Recycles a LauncherIcons that may be in-use.
-     */
-    public void recycle() {
-        clear();
-        mPool.add(this);
-    }
-
-    @NonNull
-    @Override
-    protected UserIconInfo getUserInfo(@NonNull UserHandle user) {
-        return UserCache.INSTANCE.get(mContext).getUserInfo(user);
-    }
-
-    @NonNull
-    @Override
-    public Path getShapePath(AdaptiveIconDrawable drawable, Rect iconBounds) {
-        if (!Flags.enableLauncherIconShapes()) return drawable.getIconMask();
-        return IconShape.INSTANCE.get(mContext).getShape().getPath(iconBounds);
-    }
-
-    @Override
-    protected void drawAdaptiveIcon(
-            @NonNull Canvas canvas,
-            @NonNull AdaptiveIconDrawable drawable,
-            @NonNull Path overridePath
-    ) {
-        if (!Flags.enableLauncherIconShapes()) {
-            super.drawAdaptiveIcon(canvas, drawable, overridePath);
-            return;
-        }
-        String shapeKey = LauncherPrefs.get(mContext).get(PREF_ICON_SHAPE);
-        float iconScale = switch (shapeKey) {
-            case "seven_sided_cookie" -> SEVEN_SIDED_COOKIE_SCALE;
-            case "four_sided_cookie" -> FOUR_SIDED_COOKIE_SCALE;
-            case "sunny" -> VERY_SUNNY_SCALE;
-            default -> DEFAULT_ICON_SCALE;
-        };
-        canvas.clipPath(overridePath);
-        canvas.drawColor(BLACK);
-        canvas.save();
-        canvas.scale(iconScale, iconScale, canvas.getWidth() / 2f, canvas.getHeight() / 2f);
-        if (drawable.getBackground() != null) {
-            drawable.getBackground().draw(canvas);
-        }
-        if (drawable.getForeground() != null) {
-            drawable.getForeground().draw(canvas);
-        }
-        canvas.restore();
-    }
-
-    @Override
-    public void close() {
-        recycle();
-    }
-
-    private static class Pool implements SafeCloseable {
-
-        private final Context mContext;
-
-        @NonNull
-        private ConcurrentLinkedQueue<LauncherIcons> mPool = new ConcurrentLinkedQueue<>();
-
-        private Pool(Context context) {
-            mContext = context;
-        }
-
-        public LauncherIcons obtain() {
-            ConcurrentLinkedQueue<LauncherIcons> pool = mPool;
-            LauncherIcons m = pool.poll();
-
-            if (m == null) {
-                InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(mContext);
-                return new LauncherIcons(mContext, idp.fillResIconDpi, idp.iconBitmapSize, pool);
-            } else {
-                return m;
-            }
-        }
-
-        @Override
-        public void close() {
-            mPool = new ConcurrentLinkedQueue<>();
-        }
-    }
-}
diff --git a/src/com/android/launcher3/icons/LauncherIcons.kt b/src/com/android/launcher3/icons/LauncherIcons.kt
new file mode 100644
index 0000000..29c0de1
--- /dev/null
+++ b/src/com/android/launcher3/icons/LauncherIcons.kt
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2016 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.icons
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Path
+import android.graphics.Rect
+import android.graphics.drawable.AdaptiveIconDrawable
+import android.os.UserHandle
+import com.android.launcher3.Flags
+import com.android.launcher3.InvariantDeviceProfile
+import com.android.launcher3.dagger.ApplicationContext
+import com.android.launcher3.dagger.LauncherAppSingleton
+import com.android.launcher3.dagger.LauncherComponentProvider.appComponent
+import com.android.launcher3.graphics.ThemeManager
+import com.android.launcher3.pm.UserCache
+import com.android.launcher3.util.UserIconInfo
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import java.util.concurrent.ConcurrentLinkedQueue
+import javax.inject.Inject
+
+/**
+ * Wrapper class to provide access to [BaseIconFactory] and also to provide pool of this class that
+ * are threadsafe.
+ */
+class LauncherIcons
+@AssistedInject
+internal constructor(
+    @ApplicationContext context: Context,
+    idp: InvariantDeviceProfile,
+    private var themeManager: ThemeManager,
+    private var userCache: UserCache,
+    @Assisted private val pool: ConcurrentLinkedQueue<LauncherIcons>,
+) : BaseIconFactory(context, idp.fillResIconDpi, idp.iconBitmapSize), AutoCloseable {
+
+    private val iconScale = themeManager.iconState.iconScale
+
+    init {
+        mThemeController = themeManager.themeController
+    }
+
+    /** Recycles a LauncherIcons that may be in-use. */
+    fun recycle() {
+        clear()
+        pool.add(this)
+    }
+
+    override fun getUserInfo(user: UserHandle): UserIconInfo {
+        return userCache.getUserInfo(user)
+    }
+
+    override fun getShapePath(drawable: AdaptiveIconDrawable, iconBounds: Rect): Path {
+        if (!Flags.enableLauncherIconShapes()) return super.getShapePath(drawable, iconBounds)
+        return themeManager.iconShape.getPath(iconBounds)
+    }
+
+    override fun getIconScale(): Float {
+        if (!Flags.enableLauncherIconShapes()) return super.getIconScale()
+        return themeManager.iconState.iconScale
+    }
+
+    override fun drawAdaptiveIcon(
+        canvas: Canvas,
+        drawable: AdaptiveIconDrawable,
+        overridePath: Path,
+    ) {
+        if (!Flags.enableLauncherIconShapes()) {
+            super.drawAdaptiveIcon(canvas, drawable, overridePath)
+            return
+        }
+        canvas.clipPath(overridePath)
+        canvas.drawColor(Color.BLACK)
+        canvas.save()
+        canvas.scale(iconScale, iconScale, canvas.width / 2f, canvas.height / 2f)
+        if (drawable.background != null) {
+            drawable.background.draw(canvas)
+        }
+        if (drawable.foreground != null) {
+            drawable.foreground.draw(canvas)
+        }
+        canvas.restore()
+    }
+
+    override fun close() {
+        recycle()
+    }
+
+    @AssistedFactory
+    internal interface LauncherIconsFactory {
+        fun create(pool: ConcurrentLinkedQueue<LauncherIcons>): LauncherIcons
+    }
+
+    @LauncherAppSingleton
+    class IconPool @Inject internal constructor(private val factory: LauncherIconsFactory) {
+        private var pool = ConcurrentLinkedQueue<LauncherIcons>()
+
+        fun obtain(): LauncherIcons = pool.let { it.poll() ?: factory.create(it) }
+
+        fun clear() {
+            pool = ConcurrentLinkedQueue()
+        }
+    }
+
+    companion object {
+
+        /**
+         * Return a new LauncherIcons instance from the global pool. Allows us to avoid allocating
+         * new objects in many cases.
+         */
+        @JvmStatic
+        fun obtain(context: Context): LauncherIcons = context.appComponent.iconPool.obtain()
+
+        @JvmStatic fun clearPool(context: Context) = context.appComponent.iconPool.clear()
+    }
+}
diff --git a/src/com/android/launcher3/logging/FileLog.java b/src/com/android/launcher3/logging/FileLog.java
index 924a440..5b25418 100644
--- a/src/com/android/launcher3/logging/FileLog.java
+++ b/src/com/android/launcher3/logging/FileLog.java
@@ -1,6 +1,6 @@
 package com.android.launcher3.logging;
 
-import static com.android.launcher3.util.Executors.createAndStartNewLooper;
+import static com.android.launcher3.util.LooperExecutor.createAndStartNewLooper;
 
 import android.os.Handler;
 import android.os.HandlerThread;
diff --git a/src/com/android/launcher3/logging/StatsLogManager.java b/src/com/android/launcher3/logging/StatsLogManager.java
index 9a1c874..2f1af68 100644
--- a/src/com/android/launcher3/logging/StatsLogManager.java
+++ b/src/com/android/launcher3/logging/StatsLogManager.java
@@ -905,6 +905,10 @@
         @UiEvent(doc = "Time passed between Contextual Search runnable creation and execution. This"
                 + " ensures that Recent animations have finished before Contextual Search starts.")
         LAUNCHER_LATENCY_OMNI_RUNNABLE(1546),
+
+        @UiEvent(doc = "Time passed between nav handle touch down and cancellation without "
+                + "triggering Contextual Search")
+        LAUNCHER_LATENCY_CONTEXTUAL_SEARCH_LPNH_ABANDON(2171),
         ;
 
         private final int mId;
diff --git a/src/com/android/launcher3/model/BaseLauncherBinder.java b/src/com/android/launcher3/model/BaseLauncherBinder.java
index 003bef3..a2ca6b6 100644
--- a/src/com/android/launcher3/model/BaseLauncherBinder.java
+++ b/src/com/android/launcher3/model/BaseLauncherBinder.java
@@ -28,7 +28,6 @@
 
 import static java.util.Collections.emptyList;
 
-import android.os.Process;
 import android.os.Trace;
 import android.util.Log;
 import android.util.Pair;
@@ -167,7 +166,7 @@
             return;
         }
         Map<PackageItemInfo, List<WidgetItem>>
-                widgetsByPackageItem = mBgDataModel.widgetsModel.getWidgetsByPackageItem();
+                widgetsByPackageItem = mBgDataModel.widgetsModel.getWidgetsByPackageItemForPicker();
         List<WidgetsListBaseEntry> widgets = new WidgetsListBaseEntriesBuilder(mApp.getContext())
                 .build(widgetsByPackageItem);
         Predicate<WidgetItem> filter = mBgDataModel.widgetsModel.getDefaultWidgetsFilter();
@@ -352,14 +351,8 @@
                 onCompleteSignal.executeAllAndDestroy();
             }
 
-            executeCallbacksTask(
-                    c -> {
-                        if (!enableWorkspaceInflation()) {
-                            MODEL_EXECUTOR.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
-                        }
-                        c.onInitialBindComplete(currentScreenIds, pendingTasks, onCompleteSignal,
-                                workspaceItemCount, isBindSync);
-                    }, mUiExecutor);
+            executeCallbacksTask(c -> c.onInitialBindComplete(currentScreenIds, pendingTasks,
+                    onCompleteSignal, workspaceItemCount, isBindSync), mUiExecutor);
         }
 
         private void setupPendingBind(
@@ -369,12 +362,8 @@
             executeCallbacksTask(c -> c.bindStringCache(cacheClone), pendingExecutor);
 
             executeCallbacksTask(c -> c.finishBindingItems(currentScreenIds), pendingExecutor);
-            pendingExecutor.execute(
-                    () -> {
-                        MODEL_EXECUTOR.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT);
-                        ItemInstallQueue.INSTANCE.get(mApp.getContext())
-                                .resumeModelPush(FLAG_LOADER_RUNNING);
-                    });
+            pendingExecutor.execute(() -> ItemInstallQueue.INSTANCE.get(mApp.getContext())
+                    .resumeModelPush(FLAG_LOADER_RUNNING));
         }
 
         /**
diff --git a/src/com/android/launcher3/model/DeviceGridState.java b/src/com/android/launcher3/model/DeviceGridState.java
index 90af215..d06f541 100644
--- a/src/com/android/launcher3/model/DeviceGridState.java
+++ b/src/com/android/launcher3/model/DeviceGridState.java
@@ -33,7 +33,6 @@
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherPrefs;
 import com.android.launcher3.logging.StatsLogManager.LauncherEvent;
-import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext;
 
 import java.util.Locale;
 import java.util.Objects;
@@ -68,7 +67,10 @@
     }
 
     public DeviceGridState(Context context) {
-        LauncherPrefs lp = LauncherPrefs.get(context);
+        this(LauncherPrefs.get(context));
+    }
+
+    public DeviceGridState(LauncherPrefs lp) {
         mGridSizeString = lp.get(WORKSPACE_SIZE);
         mNumHotseat = lp.get(HOTSEAT_COUNT);
         mDeviceType = lp.get(DEVICE_TYPE);
@@ -100,9 +102,6 @@
      * Stores the device state to shared preferences
      */
     public void writeToPrefs(Context context) {
-        if (context instanceof SandboxContext) {
-            return;
-        }
         LauncherPrefs.get(context).put(
                 WORKSPACE_SIZE.to(mGridSizeString),
                 HOTSEAT_COUNT.to(mNumHotseat),
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/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java
index c1ee69b..d44b289 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -36,6 +36,7 @@
 import static com.android.launcher3.model.ModelUtils.currentScreenContentFilter;
 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+import static com.android.launcher3.util.LooperExecutor.CALLER_LOADER_TASK;
 import static com.android.launcher3.util.PackageManagerHelper.hasShortcutsPermission;
 
 import android.appwidget.AppWidgetProviderInfo;
@@ -251,6 +252,7 @@
         }
 
         TraceHelper.INSTANCE.beginSection(TAG);
+        MODEL_EXECUTOR.elevatePriority(CALLER_LOADER_TASK);
         LoaderMemoryLogger memoryLogger = new LoaderMemoryLogger();
         mIsRestoreFromBackup =
                 LauncherPrefs.get(mApp.getContext()).get(IS_FIRST_LOAD_AFTER_RESTORE);
@@ -403,6 +405,7 @@
             memoryLogger.printLogs();
             throw e;
         }
+        MODEL_EXECUTOR.restorePriority(CALLER_LOADER_TASK);
         TraceHelper.INSTANCE.endSection();
     }
 
diff --git a/src/com/android/launcher3/model/ModelDbController.java b/src/com/android/launcher3/model/ModelDbController.java
index 3a55aa7..feae632 100644
--- a/src/com/android/launcher3/model/ModelDbController.java
+++ b/src/com/android/launcher3/model/ModelDbController.java
@@ -80,7 +80,6 @@
 import com.android.launcher3.provider.RestoreDbTask;
 import com.android.launcher3.util.IOUtils;
 import com.android.launcher3.util.IntArray;
-import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext;
 import com.android.launcher3.util.Partner;
 import com.android.launcher3.widget.LauncherWidgetHolder;
 
@@ -143,14 +142,11 @@
     }
 
     protected DatabaseHelper createDatabaseHelper(boolean forMigration, String dbFile) {
-        boolean isSandbox = mContext instanceof SandboxContext;
-        String dbName = isSandbox ? null : dbFile;
-
         // Set the flag for empty DB
         Runnable onEmptyDbCreateCallback = forMigration ? () -> { }
-                : () -> LauncherPrefs.get(mContext).putSync(getEmptyDbCreatedKey(dbName).to(true));
+                : () -> LauncherPrefs.get(mContext).putSync(getEmptyDbCreatedKey(dbFile).to(true));
 
-        DatabaseHelper databaseHelper = new DatabaseHelper(mContext, dbName,
+        DatabaseHelper databaseHelper = new DatabaseHelper(mContext, dbFile,
                 this::getSerialNumberForUser, onEmptyDbCreateCallback);
         // Table creation sometimes fails silently, which leads to a crash loop.
         // This way, we will try to create a table every time after crash, so the device
@@ -380,8 +376,7 @@
                 .filter(dbName -> mContext.getDatabasePath(dbName).exists())
                 .collect(Collectors.toList());
 
-        mOpenHelper = (mContext instanceof SandboxContext) ? oldHelper
-                : createDatabaseHelper(true, new DeviceGridState(idp).getDbFile());
+        mOpenHelper = createDatabaseHelper(true, new DeviceGridState(idp).getDbFile());
         try {
             // This is the current grid we have, given by the mContext
             DeviceGridState srcDeviceState = new DeviceGridState(mContext);
@@ -462,8 +457,7 @@
         List<String> existingDBs = LauncherFiles.GRID_DB_FILES.stream()
                 .filter(dbName -> mContext.getDatabasePath(dbName).exists())
                 .collect(Collectors.toList());
-        mOpenHelper = (mContext instanceof SandboxContext) ? oldHelper
-                : createDatabaseHelper(true /* forMigration */, targetDbName);
+        mOpenHelper = createDatabaseHelper(true /* forMigration */, targetDbName);
         try {
             // This is the current grid we have, given by the mContext
             DeviceGridState srcDeviceState = new DeviceGridState(mContext);
@@ -763,10 +757,6 @@
      * string will be "EMPTY_DATABASE_CREATED@minimal.db".
      */
     private ConstantItem<Boolean> getEmptyDbCreatedKey(String dbName) {
-        if (mContext instanceof SandboxContext) {
-            return LauncherPrefs.nonRestorableItem(EMPTY_DATABASE_CREATED,
-                    false /* default value */, EncryptionType.ENCRYPTED);
-        }
         String key = TextUtils.equals(dbName, LauncherFiles.LAUNCHER_DB)
                 ? EMPTY_DATABASE_CREATED : EMPTY_DATABASE_CREATED + "@" + dbName;
         return LauncherPrefs.backedUpItem(key, false /* default value */, EncryptionType.ENCRYPTED);
diff --git a/src/com/android/launcher3/model/ModelDelegate.java b/src/com/android/launcher3/model/ModelDelegate.java
index 5a2aef0..52a2188 100644
--- a/src/com/android/launcher3/model/ModelDelegate.java
+++ b/src/com/android/launcher3/model/ModelDelegate.java
@@ -23,62 +23,45 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.WorkerThread;
 
-import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.R;
+import com.android.launcher3.LauncherModel;
+import com.android.launcher3.dagger.ApplicationContext;
 import com.android.launcher3.shortcuts.ShortcutKey;
-import com.android.launcher3.util.PackageManagerHelper;
-import com.android.launcher3.util.ResourceBasedOverride;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.util.Map;
 
+import javax.inject.Inject;
+
 /**
  * Class to extend LauncherModel functionality to provide extra data
  */
-public class ModelDelegate implements ResourceBasedOverride {
-
-    /**
-     * Creates and initializes a new instance of the delegate
-     */
-    public static ModelDelegate newInstance(
-            Context context, LauncherAppState app, PackageManagerHelper pmHelper,
-            AllAppsList appsList, BgDataModel dataModel, boolean isPrimaryInstance) {
-        ModelDelegate delegate = Overrides.getObject(
-                ModelDelegate.class, context, R.string.model_delegate_class);
-        delegate.init(app, pmHelper, appsList, dataModel, isPrimaryInstance);
-        return delegate;
-    }
+public class ModelDelegate {
 
     protected final Context mContext;
-    protected PackageManagerHelper mPmHelper;
-    protected LauncherAppState mApp;
+    protected LauncherModel mModel;
     protected AllAppsList mAppsList;
     protected BgDataModel mDataModel;
-    protected boolean mIsPrimaryInstance;
 
-    public ModelDelegate(Context context) {
+    @Inject
+    public ModelDelegate(@ApplicationContext Context context) {
         mContext = context;
     }
 
     /**
      * Initializes the object with the given params.
      */
-    private void init(LauncherAppState app, PackageManagerHelper pmHelper, AllAppsList appsList,
-            BgDataModel dataModel, boolean isPrimaryInstance) {
-        this.mApp = app;
-        this.mPmHelper = pmHelper;
+    public void init(LauncherModel model, AllAppsList appsList, BgDataModel dataModel) {
+        this.mModel = model;
         this.mAppsList = appsList;
         this.mDataModel = dataModel;
-        this.mIsPrimaryInstance = isPrimaryInstance;
     }
 
     /** Called periodically to validate and update any data */
     @WorkerThread
     public void validateData() {
-        if (hasShortcutsPermission(mApp.getContext())
-                != mAppsList.hasShortcutHostPermission()) {
-            mApp.getModel().forceReload();
+        if (hasShortcutsPermission(mContext) != mAppsList.hasShortcutHostPermission()) {
+            mModel.forceReload();
         }
     }
 
diff --git a/src/com/android/launcher3/model/ModelInitializer.kt b/src/com/android/launcher3/model/ModelInitializer.kt
new file mode 100644
index 0000000..735a52a
--- /dev/null
+++ b/src/com/android/launcher3/model/ModelInitializer.kt
@@ -0,0 +1,167 @@
+/*
+ * 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.DaggerSingletonTracker
+import com.android.launcher3.util.Executors.MODEL_EXECUTOR
+import com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR
+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 javax.inject.Inject
+
+/** Utility class for initializing all model callbacks */
+class ModelInitializer
+@Inject
+constructor(
+    @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 lifeCycle: DaggerSingletonTracker,
+) {
+
+    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)
+        lifeCycle.addCloseable { idp.removeOnChangeListener(idpChangeListener) }
+
+        // Theme changes
+        val themeChangeListener = ThemeChangeListener { refreshAndReloadLauncher() }
+        themeManager.addChangeListener(themeChangeListener)
+        lifeCycle.addCloseable { themeManager.removeChangeListener(themeChangeListener) }
+
+        // System changes
+        val modelCallbacks = model.newModelCallbacks()
+        val launcherApps = context.getSystemService(LauncherApps::class.java)!!
+        launcherApps.registerCallback(modelCallbacks, MODEL_EXECUTOR.handler)
+        lifeCycle.addCloseable { 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)
+        lifeCycle.addCloseable { 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)
+            lifeCycle.addCloseable { reloadReceiver.unregisterReceiverSafely() }
+        }
+
+        // User changes
+        lifeCycle.addCloseable(userCache.addUserEventListener(model::onUserEvent))
+
+        // Private space settings changes
+        val psSettingsListener = SettingsCache.OnChangeListener { model.forceReload() }
+        settingsCache.register(PRIVATE_SPACE_HIDE_WHEN_LOCKED_URI, psSettingsListener)
+        lifeCycle.addCloseable {
+            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))
+        lifeCycle.addCloseable {
+            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)
+            lifeCycle.addCloseable {
+                getPrefs(context).unregisterOnSharedPreferenceChangeListener(smartSpacePrefChanges)
+            }
+        }
+
+        // Custom widgets
+        lifeCycle.addCloseable(customWidgetManager.addWidgetRefreshCallback(model::rebindCallbacks))
+
+        // Icon changes
+        lifeCycle.addCloseable(
+            iconProvider.registerIconChangeListener(model::onAppIconChanged, MODEL_EXECUTOR.handler)
+        )
+
+        // Install session changes
+        lifeCycle.addCloseable(installSessionHelper.registerInstallTracker(modelCallbacks))
+    }
+
+    companion object {
+        private const val ACTION_FORCE_RELOAD = "force-reload-launcher"
+    }
+}
diff --git a/src/com/android/launcher3/model/ModelTaskController.kt b/src/com/android/launcher3/model/ModelTaskController.kt
index 40ea17d..6e3e35e 100644
--- a/src/com/android/launcher3/model/ModelTaskController.kt
+++ b/src/com/android/launcher3/model/ModelTaskController.kt
@@ -77,7 +77,7 @@
     }
 
     fun bindUpdatedWidgets(dataModel: BgDataModel) {
-        val widgetsByPackageItem = dataModel.widgetsModel.widgetsByPackageItem
+        val widgetsByPackageItem = dataModel.widgetsModel.widgetsByPackageItemForPicker
         val allWidgets = WidgetsListBaseEntriesBuilder(app.context).build(widgetsByPackageItem)
 
         val defaultWidgetsFilter = dataModel.widgetsModel.defaultWidgetsFilter
diff --git a/src/com/android/launcher3/model/PackageUpdatedTask.java b/src/com/android/launcher3/model/PackageUpdatedTask.java
index d1eceb9..3cdb250 100644
--- a/src/com/android/launcher3/model/PackageUpdatedTask.java
+++ b/src/com/android/launcher3/model/PackageUpdatedTask.java
@@ -239,25 +239,31 @@
                             boolean isTargetValid = !cn.getClassName().equals(
                                     IconCache.EMPTY_CLASS_NAME);
                             if (itemInfo.itemType == ITEM_TYPE_DEEP_SHORTCUT) {
+                                int requestQuery = ShortcutRequest.PINNED;
+                                if (Flags.restoreArchivedShortcuts()) {
+                                    // Avoid race condition where shortcut service has no record of
+                                    // unarchived shortcut being pinned after restore.
+                                    // Launcher should be source-of-truth for if shortcut is pinned.
+                                    requestQuery = ShortcutRequest.ALL;
+                                }
                                 List<ShortcutInfo> shortcut =
                                         new ShortcutRequest(context, mUser)
                                                 .forPackage(cn.getPackageName(),
                                                         itemInfo.getDeepShortcutId())
-                                                .query(ShortcutRequest.PINNED);
-                                if (shortcut.isEmpty()
-                                        && !(Flags.restoreArchivedShortcuts()
-                                            && !itemInfo.isArchived())
-                                ) {
+                                                .query(requestQuery);
+                                if (shortcut.isEmpty()) {
                                     isTargetValid = false;
                                     if (DEBUG) {
-                                        Log.d(TAG, "Pinned Shortcut not found for updated"
-                                                + " package=" + itemInfo.getTargetPackage());
-                                    }
-                                } else if (!shortcut.isEmpty()) {
-                                    if (DEBUG) {
-                                        Log.d(TAG, "Found pinned shortcut for updated"
+                                        Log.d(TAG, "Shortcut not found for updated"
                                                 + " package=" + itemInfo.getTargetPackage()
-                                                + ", isTargetValid=" + isTargetValid);
+                                                + ", isArchived=" + itemInfo.isArchived());
+                                    }
+                                } else {
+                                    if (DEBUG) {
+                                        Log.d(TAG, "Found shortcut for updated"
+                                                + " package=" + itemInfo.getTargetPackage()
+                                                + ", isTargetValid=" + isTargetValid
+                                                + ", isArchived=" + itemInfo.isArchived());
                                     }
                                     itemInfo.updateFromDeepShortcutInfo(shortcut.get(0), context);
                                     infoUpdated = true;
diff --git a/src/com/android/launcher3/model/WidgetsModel.java b/src/com/android/launcher3/model/WidgetsModel.java
index a176465..ab960d8 100644
--- a/src/com/android/launcher3/model/WidgetsModel.java
+++ b/src/com/android/launcher3/model/WidgetsModel.java
@@ -70,6 +70,7 @@
     private final Map<PackageItemInfo, List<WidgetItem>> mWidgetsByPackageItem = new HashMap<>();
     @Nullable private Predicate<WidgetItem> mDefaultWidgetsFilter = null;
     @Nullable private Predicate<WidgetItem> mPredictedWidgetsFilter = null;
+    @Nullable private WidgetValidityCheckForPicker mWidgetValidityCheckForPicker = null;
 
     /**
      * Returns all widgets keyed by their component key.
@@ -87,13 +88,44 @@
     }
 
     /**
-     * Returns widgets grouped by the package item that they should belong to.
+     * Returns widgets (eligible for display in picker) keyed by their component key.
      */
-    public synchronized Map<PackageItemInfo, List<WidgetItem>> getWidgetsByPackageItem() {
-        if (!WIDGETS_ENABLED) {
+    public synchronized Map<ComponentKey, WidgetItem> getWidgetsByComponentKeyForPicker() {
+        if (!WIDGETS_ENABLED || mWidgetValidityCheckForPicker == null) {
             return Collections.emptyMap();
         }
-        return new HashMap<>(mWidgetsByPackageItem);
+
+        return mWidgetsByPackageItem.values().stream()
+                .flatMap(Collection::stream).distinct()
+                .filter(widgetItem -> mWidgetValidityCheckForPicker.test(widgetItem))
+                .collect(Collectors.toMap(
+                        widget -> new ComponentKey(widget.componentName, widget.user),
+                        Function.identity()
+                ));
+    }
+
+    /**
+     * Returns widgets (displayable in the widget picker) grouped by the package item that
+     * they should belong to.
+     */
+    public synchronized Map<PackageItemInfo, List<WidgetItem>> getWidgetsByPackageItemForPicker() {
+        if (!WIDGETS_ENABLED || mWidgetValidityCheckForPicker == null) {
+            return Collections.emptyMap();
+        }
+
+        return mWidgetsByPackageItem.entrySet().stream()
+                .collect(
+                        Collectors.toMap(
+                                Map.Entry::getKey,
+                                entry -> entry.getValue().stream()
+                                        .filter(widgetItem ->
+                                                mWidgetValidityCheckForPicker.test(widgetItem))
+                                        .collect(Collectors.toList())
+                        )
+                )
+                .entrySet().stream()
+                .filter(entry -> !entry.getValue().isEmpty())
+                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
     }
 
     /**
@@ -181,6 +213,9 @@
             Log.d(TAG, "addWidgetsAndShortcuts, widgetsShortcuts#=" + rawWidgetsShortcuts.size());
         }
 
+        // Refresh the validity checker with latest app state.
+        mWidgetValidityCheckForPicker = new WidgetValidityCheckForPicker(app);
+
         // Temporary cache for {@link PackageItemInfos} to avoid having to go through
         // {@link mPackageItemInfos} to locate the key to be used for {@link #mWidgetsList}
         PackageItemInfoCache packageItemInfoCache = new PackageItemInfoCache();
@@ -195,7 +230,6 @@
 
         // add and update.
         mWidgetsByPackageItem.putAll(rawWidgetsShortcuts.stream()
-                .filter(new WidgetValidityCheck(app))
                 .filter(new WidgetFlagCheck())
                 .flatMap(widgetItem -> getPackageUserKeys(app.getContext(), widgetItem).stream()
                         .map(key -> new Pair<>(packageItemInfoCache.getOrCreate(key), widgetItem)))
@@ -270,12 +304,15 @@
         return packageUserKeys;
     }
 
-    private static class WidgetValidityCheck implements Predicate<WidgetItem> {
+    /**
+     * Checks if widgets are eligible for displaying in widget picker / tray.
+     */
+    private static class WidgetValidityCheckForPicker implements Predicate<WidgetItem> {
 
         private final InvariantDeviceProfile mIdp;
         private final AppFilter mAppFilter;
 
-        WidgetValidityCheck(LauncherAppState app) {
+        WidgetValidityCheckForPicker(LauncherAppState app) {
             mIdp = app.getInvariantDeviceProfile();
             mAppFilter = new AppFilter(app.getContext());
         }
@@ -310,6 +347,10 @@
         }
     }
 
+    /**
+     * Checks if certain widgets that are available behind flag can be used across all surfaces in
+     * launcher.
+     */
     private static class WidgetFlagCheck implements Predicate<WidgetItem> {
 
         private static final String BUBBLES_SHORTCUT_WIDGET =
diff --git a/src/com/android/launcher3/model/WorkspaceItemProcessor.kt b/src/com/android/launcher3/model/WorkspaceItemProcessor.kt
index 3919eb7..99f2837 100644
--- a/src/com/android/launcher3/model/WorkspaceItemProcessor.kt
+++ b/src/com/android/launcher3/model/WorkspaceItemProcessor.kt
@@ -194,29 +194,35 @@
         if (intent.`package` == null) {
             intent.`package` = targetPkg
         }
-        val isPreArchived = appInfoWrapper.isArchived() && c.restoreFlag != 0
+
+        val isPreArchivedShortcut =
+            Flags.restoreArchivedShortcuts() &&
+                appInfoWrapper.isArchived() &&
+                c.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT &&
+                c.restoreFlag != 0
 
         // else if cn == null => can't infer much, leave it
         // else if !validPkg => could be restored icon or missing sd-card
         when {
-            !TextUtils.isEmpty(targetPkg) && (!validTarget || isPreArchived) -> {
+            !TextUtils.isEmpty(targetPkg) && (!validTarget || isPreArchivedShortcut) -> {
                 // Points to a valid app (superset of cn != null) but the apk
                 // is not available.
                 when {
-                    c.restoreFlag != 0 || isPreArchived -> {
+                    c.restoreFlag != 0 || isPreArchivedShortcut -> {
                         // Package is not yet available but might be
                         // installed later.
                         FileLog.d(
                             TAG,
                             "package not yet restored: $targetPkg, itemType=${c.itemType}" +
-                                "isPreArchived=$isPreArchived, restoreFlag=${c.restoreFlag}",
+                                ", isPreArchivedShortcut=$isPreArchivedShortcut" +
+                                ", restoreFlag=${c.restoreFlag}",
                         )
                         tempPackageKey.update(targetPkg, c.user)
                         when {
                             c.hasRestoreFlag(WorkspaceItemInfo.FLAG_RESTORE_STARTED) -> {
                                 // Restore has started once.
                             }
-                            installingPkgs.containsKey(tempPackageKey) || isPreArchived -> {
+                            installingPkgs.containsKey(tempPackageKey) || isPreArchivedShortcut -> {
                                 // App restore has started. Update the flag
                                 c.restoreFlag =
                                     c.restoreFlag or WorkspaceItemInfo.FLAG_RESTORE_STARTED
@@ -268,7 +274,7 @@
             )
             validTarget = false
         }
-        if (validTarget && !isPreArchived) {
+        if (validTarget && !isPreArchivedShortcut) {
             FileLog.d(
                 TAG,
                 "valid target true, marking restored: $targetPkg," +
@@ -283,7 +289,7 @@
         when {
             c.restoreFlag != 0 -> {
                 // Already verified above that user is same as default user
-                info = c.getRestoredItemInfo(intent, isPreArchived)
+                info = c.getRestoredItemInfo(intent, isPreArchivedShortcut)
             }
             c.itemType == Favorites.ITEM_TYPE_APPLICATION ->
                 info = c.getAppShortcutInfo(intent, allowMissingTarget, useLowResIcon, false)
diff --git a/src/com/android/launcher3/model/data/ItemInfoWithIcon.java b/src/com/android/launcher3/model/data/ItemInfoWithIcon.java
index ff40f30..b60b8cc 100644
--- a/src/com/android/launcher3/model/data/ItemInfoWithIcon.java
+++ b/src/com/android/launcher3/model/data/ItemInfoWithIcon.java
@@ -22,6 +22,7 @@
 import android.content.Intent;
 import android.os.Process;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.Flags;
@@ -44,6 +45,7 @@
     /**
      * The bitmap for the application icon
      */
+    @NonNull
     public BitmapInfo bitmap = BitmapInfo.LOW_RES_INFO;
 
     /**
diff --git a/src/com/android/launcher3/pageindicators/PageIndicatorDots.java b/src/com/android/launcher3/pageindicators/PageIndicatorDots.java
index a691e45..37f5189 100644
--- a/src/com/android/launcher3/pageindicators/PageIndicatorDots.java
+++ b/src/com/android/launcher3/pageindicators/PageIndicatorDots.java
@@ -16,6 +16,7 @@
 
 package com.android.launcher3.pageindicators;
 
+import static com.android.launcher3.Flags.enableLauncherVisualRefresh;
 import static com.android.launcher3.config.FeatureFlags.FOLDABLE_SINGLE_PAGE;
 
 import android.animation.Animator;
@@ -57,8 +58,8 @@
 public class PageIndicatorDots extends View implements Insettable, PageIndicator {
 
     private static final float SHIFT_PER_ANIMATION = 0.5f;
-    private static final float SHIFT_THRESHOLD = 0.1f;
-    private static final long ANIMATION_DURATION = 150;
+    private static final float SHIFT_THRESHOLD = (enableLauncherVisualRefresh() ? 0.5f : 0.2f);
+    private static final long ANIMATION_DURATION = (enableLauncherVisualRefresh() ? 200 : 150);
     private static final int PAGINATION_FADE_DELAY = ViewConfiguration.getScrollDefaultDelay();
     private static final int PAGINATION_FADE_IN_DURATION = 83;
     private static final int PAGINATION_FADE_OUT_DURATION = 167;
@@ -78,6 +79,7 @@
     // This value approximately overshoots to 1.5 times the original size.
     private static final float ENTER_ANIMATION_OVERSHOOT_TENSION = 4.9f;
 
+    // This is used to optimize the onDraw method by not constructing a new RectF each draw.
     private static final RectF sTempRect = new RectF();
 
     private static final FloatProperty<PageIndicatorDots> CURRENT_POSITION =
@@ -93,7 +95,7 @@
                     obj.invalidate();
                     obj.invalidateOutline();
                 }
-    };
+            };
 
     private static final IntProperty<PageIndicatorDots> PAGINATION_ALPHA =
             new IntProperty<PageIndicatorDots>("pagination_alpha") {
@@ -111,6 +113,7 @@
 
     private final Handler mDelayedPaginationFadeHandler = new Handler(Looper.getMainLooper());
     private final float mDotRadius;
+    private final float mGapWidth;
     private final float mCircleGap;
     private final boolean mIsRtl;
 
@@ -130,6 +133,7 @@
      * 1.0  => Active dot is at position 1
      */
     private float mCurrentPosition;
+    private int mLastPosition;
     private float mFinalPosition;
     private boolean mIsScrollPaused;
     @VisibleForTesting
@@ -157,7 +161,10 @@
         mPaginationPaint.setStyle(Style.FILL);
         mPaginationPaint.setColor(Themes.getAttrColor(context, R.attr.pageIndicatorDotColor));
         mDotRadius = getResources().getDimension(R.dimen.page_indicator_dot_size) / 2;
-        mCircleGap = DOT_GAP_FACTOR * mDotRadius;
+        mGapWidth = getResources().getDimension(R.dimen.page_indicator_gap_width);
+        mCircleGap = (enableLauncherVisualRefresh())
+                ? mDotRadius * 2 + mGapWidth
+                : DOT_GAP_FACTOR * mDotRadius;
         setOutlineProvider(new MyOutlineProver());
         mIsRtl = Utilities.isRtl(getResources());
     }
@@ -188,29 +195,40 @@
 
         mTotalScroll = totalScroll;
 
-        int scrollPerPage = totalScroll / (mNumPages - 1);
-        int pageToLeft = scrollPerPage == 0 ? 0 : currentScroll / scrollPerPage;
-        int pageToLeftScroll = pageToLeft * scrollPerPage;
-        int pageToRightScroll = pageToLeftScroll + scrollPerPage;
+        if (enableLauncherVisualRefresh()) {
+            float scrollPerPage = (float) totalScroll / (mNumPages - 1);
+            float position = currentScroll / scrollPerPage;
+            animateToPosition(Math.round(position));
 
-        float scrollThreshold = SHIFT_THRESHOLD * scrollPerPage;
-        if (currentScroll < pageToLeftScroll + scrollThreshold) {
-            // scroll is within the left page's threshold
-            animateToPosition(pageToLeft);
-            if (mShouldAutoHide) {
-                hideAfterDelay();
-            }
-        } else if (currentScroll > pageToRightScroll - scrollThreshold) {
-            // scroll is far enough from left page to go to the right page
-            animateToPosition(pageToLeft + 1);
-            if (mShouldAutoHide) {
+            float delta = Math.abs((int) position - position);
+            if (mShouldAutoHide && (delta < 0.1 || delta > 0.9)) {
                 hideAfterDelay();
             }
         } else {
-            // scroll is between left and right page
-            animateToPosition(pageToLeft + SHIFT_PER_ANIMATION);
-            if (mShouldAutoHide) {
-                mDelayedPaginationFadeHandler.removeCallbacksAndMessages(null);
+            int scrollPerPage = totalScroll / (mNumPages - 1);
+            int pageToLeft = scrollPerPage == 0 ? 0 : currentScroll / scrollPerPage;
+            int pageToLeftScroll = pageToLeft * scrollPerPage;
+            int pageToRightScroll = pageToLeftScroll + scrollPerPage;
+
+            float scrollThreshold = SHIFT_THRESHOLD * scrollPerPage;
+            if (currentScroll < pageToLeftScroll + scrollThreshold) {
+                // scroll is within the left page's threshold
+                animateToPosition(pageToLeft);
+                if (mShouldAutoHide) {
+                    hideAfterDelay();
+                }
+            } else if (currentScroll > pageToRightScroll - scrollThreshold) {
+                // scroll is far enough from left page to go to the right page
+                animateToPosition(pageToLeft + 1);
+                if (mShouldAutoHide) {
+                    hideAfterDelay();
+                }
+            } else {
+                // scroll is between left and right page
+                animateToPosition(pageToLeft + SHIFT_PER_ANIMATION);
+                if (mShouldAutoHide) {
+                    mDelayedPaginationFadeHandler.removeCallbacksAndMessages(null);
+                }
             }
         }
     }
@@ -283,15 +301,23 @@
 
     private void animateToPosition(float position) {
         mFinalPosition = position;
-        if (Math.abs(mCurrentPosition - mFinalPosition) < SHIFT_THRESHOLD) {
+        if (!enableLauncherVisualRefresh()
+                && Math.abs(mCurrentPosition - mFinalPosition) < SHIFT_THRESHOLD) {
             mCurrentPosition = mFinalPosition;
         }
-        if (mAnimator == null && Float.compare(mCurrentPosition, mFinalPosition) != 0) {
-            float positionForThisAnim = mCurrentPosition > mFinalPosition ?
-                    mCurrentPosition - SHIFT_PER_ANIMATION : mCurrentPosition + SHIFT_PER_ANIMATION;
+        if (mAnimator == null && Float.compare(mCurrentPosition, position) != 0) {
+            float positionForThisAnim = enableLauncherVisualRefresh()
+                    ? position
+                    : (mCurrentPosition > mFinalPosition
+                            ? mCurrentPosition - SHIFT_PER_ANIMATION
+                            : mCurrentPosition + SHIFT_PER_ANIMATION);
             mAnimator = ObjectAnimator.ofFloat(this, CURRENT_POSITION, positionForThisAnim);
             mAnimator.addListener(new AnimationCycleListener());
             mAnimator.setDuration(ANIMATION_DURATION);
+            if (enableLauncherVisualRefresh()) {
+                mLastPosition = (int) mCurrentPosition;
+                mAnimator.setInterpolator(new OvershootInterpolator());
+            }
             mAnimator.start();
         }
     }
@@ -314,6 +340,7 @@
         invalidate();
     }
 
+    // TODO(b/394355070): Verify Folder Entry Animation works correctly with visual updates
     public void playEntryAnimation() {
         int count = mEntryAnimationRadiusFactors.length;
         if (count == 0) {
@@ -391,6 +418,7 @@
 
     @Override
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        // TODO(b/394355070): Verify Folder Entry Animation works correctly with visual updates
         // Add extra spacing of mDotRadius on all sides so than entry animation could be run.
         int width = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY ?
                 MeasureSpec.getSize(widthMeasureSpec) : (int) ((mNumPages * 3 + 2) * mDotRadius);
@@ -410,17 +438,14 @@
             return;
         }
 
-        // Draw all page indicators;
         float circleGap = mCircleGap;
-        float startX = ((float) getWidth() / 2)
-                - (mCircleGap * (((float) mNumPages - 1) / 2))
-                - mDotRadius;
-
-        float x = startX + mDotRadius;
+        float x = ((float) getWidth() / 2) - (mCircleGap * ((float) mNumPages - 1) / 2);
         float y = getHeight() / 2;
 
         if (mEntryAnimationRadiusFactors != null) {
             // During entry animation, only draw the circles
+            // TODO(b/394355070): Verify Folder Entry Animation works correctly - visual updates
+
             if (mIsRtl) {
                 x = getWidth() - x;
                 circleGap = -circleGap;
@@ -432,18 +457,84 @@
                 x += circleGap;
             }
         } else {
+            // Save the current alpha value, so we can reset to it again after drawing the dots
             int alpha = mPaginationPaint.getAlpha();
 
-            // Here we draw the dots
-            mPaginationPaint.setAlpha((int) (alpha * DOT_ALPHA_FRACTION));
-            for (int i = 0; i < mNumPages; i++) {
-                canvas.drawCircle(x, y, mDotRadius, mPaginationPaint);
-                x += circleGap;
+            if (enableLauncherVisualRefresh()) {
+                int nonActiveAlpha = (int) (alpha * DOT_ALPHA_FRACTION);
+
+                float diameter = 2 * mDotRadius;
+                sTempRect.top = y - mDotRadius;
+                sTempRect.bottom = y + mDotRadius;
+                sTempRect.left = x - diameter;
+
+                float posDif = Math.abs(mLastPosition - mCurrentPosition);
+                float boundedPosition = (posDif > 1)
+                        ? Math.round(mCurrentPosition)
+                        : mCurrentPosition;
+                float bounceProgress = (posDif > 1) ? posDif - 1 : 0;
+                float bounceAdjustment = Math.abs(mCurrentPosition - boundedPosition) * diameter;
+
+                // Here we draw the dots, one at a time from the left-most dot to the right-most dot
+                // 1.0 => 000000 000000111111 000000
+                // 1.3 => 000000 0000001111 11000000
+                // 1.6 => 000000 00000011 1111000000
+                // 2.0 => 000000 000000 111111000000
+                for (int i = 0; i < mNumPages; i++) {
+                    mPaginationPaint.setAlpha(nonActiveAlpha);
+                    float delta = Math.abs(boundedPosition - i);
+                    if (delta <= SHIFT_THRESHOLD) {
+                        mPaginationPaint.setAlpha(alpha);
+                    }
+
+                    // If boundedPosition is 3.3, both 3 and 4 should enter this condition.
+                    // If boundedPosition is 3, only 3 should enter this condition.
+                    if (delta < 1) {
+                        sTempRect.right = sTempRect.left + diameter + ((1 - delta) * diameter);
+
+                        // While the animation is shifting the active pagination dots size from
+                        // the previously active one, to the newly active dot, there is no bounce
+                        // adjustment. The bounce happens in the "Overshoot" phase of the animation.
+                        // mLastPosition is used to determine when the currentPosition is just
+                        // leaving the page, or if it is in the overshoot phase.
+                        if (boundedPosition == i && bounceProgress != 0) {
+                            if (mLastPosition < mCurrentPosition) {
+                                sTempRect.left -= bounceAdjustment;
+                            } else {
+                                sTempRect.right += bounceAdjustment;
+                            }
+                        }
+                    } else {
+                        sTempRect.right = sTempRect.left + diameter;
+
+                        if (mLastPosition == i && bounceProgress != 0) {
+                            if (mLastPosition > mCurrentPosition) {
+                                sTempRect.left += bounceAdjustment;
+                            } else {
+                                sTempRect.right -= bounceAdjustment;
+                            }
+                        }
+                    }
+                    canvas.drawRoundRect(sTempRect, mDotRadius, mDotRadius, mPaginationPaint);
+
+                    // TODO(b/394355070) Verify RTL experience works correctly with visual updates
+                    sTempRect.left = sTempRect.right + mGapWidth;
+                }
+            } else {
+                // Here we draw the dots
+                mPaginationPaint.setAlpha((int) (alpha * DOT_ALPHA_FRACTION));
+                for (int i = 0; i < mNumPages; i++) {
+                    canvas.drawCircle(x, y, mDotRadius, mPaginationPaint);
+                    x += circleGap;
+                }
+
+                // Here we draw the current page indicator
+                mPaginationPaint.setAlpha(alpha);
+                canvas.drawRoundRect(getActiveRect(), mDotRadius, mDotRadius, mPaginationPaint);
             }
 
-            // Here we draw the current page indicator
+            // Reset the alpha so it doesn't become progressively more transparent each onDraw call
             mPaginationPaint.setAlpha(alpha);
-            canvas.drawRoundRect(getActiveRect(), mDotRadius, mDotRadius, mPaginationPaint);
         }
     }
 
@@ -499,6 +590,7 @@
         @Override
         public void getOutline(View view, Outline outline) {
             if (mEntryAnimationRadiusFactors == null) {
+                // TODO(b/394355070): Verify Outline works correctly with visual updates
                 RectF activeRect = getActiveRect();
                 outline.setRoundRect(
                         (int) activeRect.left,
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/pm/UserCache.java b/src/com/android/launcher3/pm/UserCache.java
index 0b18a87..20c0ecc 100644
--- a/src/com/android/launcher3/pm/UserCache.java
+++ b/src/com/android/launcher3/pm/UserCache.java
@@ -81,10 +81,7 @@
     }
 
     private final List<BiConsumer<UserHandle, String>> mUserEventListeners = new ArrayList<>();
-    private final SimpleBroadcastReceiver mUserChangeReceiver =
-            new SimpleBroadcastReceiver(MODEL_EXECUTOR, this::onUsersChanged);
-
-    private final Context mContext;
+    private final SimpleBroadcastReceiver mUserChangeReceiver;
     private final ApiWrapper mApiWrapper;
 
     @NonNull
@@ -99,16 +96,17 @@
             DaggerSingletonTracker tracker,
             ApiWrapper apiWrapper
     ) {
-        mContext = context;
         mApiWrapper = apiWrapper;
+        mUserChangeReceiver = new SimpleBroadcastReceiver(context,
+                MODEL_EXECUTOR, this::onUsersChanged);
         mUserToSerialMap = Collections.emptyMap();
         MODEL_EXECUTOR.execute(this::initAsync);
-        tracker.addCloseable(() -> mUserChangeReceiver.unregisterReceiverSafely(mContext));
+        tracker.addCloseable(() -> mUserChangeReceiver.unregisterReceiverSafely());
     }
 
     @WorkerThread
     private void initAsync() {
-        mUserChangeReceiver.register(mContext,
+        mUserChangeReceiver.register(
                 Intent.ACTION_MANAGED_PROFILE_AVAILABLE,
                 Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE,
                 Intent.ACTION_MANAGED_PROFILE_REMOVED,
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/provider/RestoreDbTask.java b/src/com/android/launcher3/provider/RestoreDbTask.java
index dc42920..23941bb 100644
--- a/src/com/android/launcher3/provider/RestoreDbTask.java
+++ b/src/com/android/launcher3/provider/RestoreDbTask.java
@@ -51,6 +51,7 @@
 import androidx.annotation.VisibleForTesting;
 import androidx.annotation.WorkerThread;
 
+import com.android.launcher3.Flags;
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherFiles;
@@ -130,9 +131,11 @@
         removeOldDBs(context, oldPhoneFileName);
         // The idp before this contains data about the old phone, after this it becomes the idp
         // of the current phone.
-        FileLog.d(TAG, "Resetting IDP to default for restore dest device");
-        idp.reset(context);
-        trySettingPreviousGridAsCurrent(context, idp, oldPhoneFileName, previousDbs);
+        if (!Flags.oneGridSpecs()) {
+            FileLog.d(TAG, "Resetting IDP to default for restore dest device");
+            idp.reset(context);
+            trySettingPreviousGridAsCurrent(context, idp, oldPhoneFileName, previousDbs);
+        }
     }
 
 
@@ -415,7 +418,11 @@
     }
 
     public static boolean isPending(Context context) {
-        return LauncherPrefs.get(context).has(RESTORE_DEVICE);
+        return isPending(LauncherPrefs.get(context));
+    }
+
+    public static boolean isPending(LauncherPrefs prefs) {
+        return prefs.has(RESTORE_DEVICE);
     }
 
     /**
@@ -531,7 +538,7 @@
         }
 
         logFavoritesTable(controller.getDb(), "launcher db after remap widget ids", null, null);
-        LauncherAppState.INSTANCE.executeIfCreated(app -> app.getModel().forceReload());
+        LauncherAppState.INSTANCE.get(context).getModel().reloadIfActive();
     }
 
     private static void logDatabaseWidgetInfo(ModelDbController controller) {
diff --git a/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java b/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java
index df27b54..b1653d0 100644
--- a/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java
+++ b/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java
@@ -16,6 +16,7 @@
 package com.android.launcher3.secondarydisplay;
 
 import static com.android.launcher3.util.WallpaperThemeManager.setWallpaperDependentTheme;
+import static com.android.window.flags.Flags.enableTaskbarConnectedDisplays;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -115,21 +116,19 @@
         if (mDragLayer != null) {
             return;
         }
-        InvariantDeviceProfile currentDisplayIdp = new InvariantDeviceProfile(
-                this, getWindow().getDecorView().getDisplay());
 
-        // Disable transpose layout and use multi-window mode so that the icons are scaled properly
-        mDeviceProfile = currentDisplayIdp.getDeviceProfile(this)
-                .toBuilder(this)
-                .setMultiWindowMode(true)
-                .setTransposeLayoutWithOrientation(false)
-                .build();
+        mDeviceProfile = InvariantDeviceProfile.INSTANCE.get(this)
+                .createDeviceProfileForSecondaryDisplay(this);
         mDeviceProfile.autoResizeAllAppsCells();
 
         setContentView(R.layout.secondary_launcher);
         mDragLayer = findViewById(R.id.drag_layer);
         mAppsView = findViewById(R.id.apps_view);
         mAppsButton = findViewById(R.id.all_apps_button);
+        // TODO (b/391965805): Replace this flag with DesktopExperiences flag.
+        if (enableTaskbarConnectedDisplays()) {
+            mAppsButton.setVisibility(View.INVISIBLE);
+        }
 
         mDragController.addDragListener(this);
         mPopupDataProvider = new PopupDataProvider(
@@ -249,7 +248,9 @@
                 @Override
                 public void onAnimationEnd(Animator animation) {
                     mAppsView.setVisibility(View.INVISIBLE);
-                    mAppsButton.setVisibility(View.VISIBLE);
+                    // TODO (b/391965805): Replace this flag with DesktopExperiences flag.
+                    mAppsButton.setVisibility(
+                            enableTaskbarConnectedDisplays() ? View.INVISIBLE : View.VISIBLE);
                     mAppsView.getSearchUiManager().resetSearch();
                 }
             });
diff --git a/src/com/android/launcher3/shapes/IconShapeModel.kt b/src/com/android/launcher3/shapes/IconShapeModel.kt
index dd6c432..fc49adc 100644
--- a/src/com/android/launcher3/shapes/IconShapeModel.kt
+++ b/src/com/android/launcher3/shapes/IconShapeModel.kt
@@ -21,4 +21,5 @@
     val title: String,
     val pathString: String,
     val folderPathString: String = pathString,
+    val iconScale: Float = 1f,
 )
diff --git a/src/com/android/launcher3/shapes/ShapesProvider.kt b/src/com/android/launcher3/shapes/ShapesProvider.kt
index 7e1f640..03e30d8 100644
--- a/src/com/android/launcher3/shapes/ShapesProvider.kt
+++ b/src/com/android/launcher3/shapes/ShapesProvider.kt
@@ -16,198 +16,71 @@
 
 package com.android.launcher3.shapes
 
+import androidx.annotation.VisibleForTesting
 import com.android.launcher3.Flags as LauncherFlags
 import com.android.systemui.shared.Flags
 
 object ShapesProvider {
-    val folderShapes =
-        if (LauncherFlags.enableLauncherIconShapes()) {
-            mapOf(
-                "clover" to
-                    "M 39.616 4" +
-                        "C 46.224 6.87 53.727 6.87 60.335 4" +
-                        "L 63.884 2.459" +
-                        "C 85.178 -6.789 106.789 14.822 97.541 36.116" +
-                        "L 96 39.665" +
-                        "C 93.13 46.273 93.13 53.776 96 60.384" +
-                        "L 97.541 63.934" +
-                        "C 106.789 85.227 85.178 106.839 63.884 97.591" +
-                        "L 60.335 96.049" +
-                        "C 53.727 93.179 46.224 93.179 39.616 96.049" +
-                        "L 36.066 97.591" +
-                        "C 14.773 106.839 -6.839 85.227 2.409 63.934" +
-                        "L 3.951 60.384" +
-                        "C 6.821 53.776 6.821 46.273 3.951 39.665" +
-                        "L 2.409 36.116" +
-                        "C -6.839 14.822 14.773 -6.789 36.066 2.459" +
-                        "Z",
-                "complexClover" to
-                    "M 49.85 6.764" +
-                        "L 50.013 6.971" +
-                        "L 50.175 6.764" +
-                        "C 53.422 2.635 58.309 0.207 63.538 0.207" +
-                        "C 65.872 0.207 68.175 0.692 70.381 1.648" +
-                        "L 71.79 2.264" +
-                        "L 71.792 2.265" +
-                        "A 3.46 3.46 0 0 0 74.515 2.265" +
-                        "L 74.517 2.264" +
-                        "L 75.926 1.652" +
-                        "A 17.1 17.1 0 0 1 82.769 0.207" +
-                        "C 88.495 0.207 93.824 3.117 97.022 7.989" +
-                        "C 100.21 12.848 100.697 18.712 98.36 24.087" +
-                        "L 97.749 25.496" +
-                        "V 25.497" +
-                        "A 3.45 3.45 0 0 0 97.749 28.222" +
-                        "V 28.223" +
-                        "L 98.36 29.632" +
-                        "C 100.697 35.007 100.207 40.871 97.022 45.73" +
-                        "A 17.5 17.5 0 0 1 93.264 49.838" +
-                        "L 93.06 50" +
-                        "L 93.264 50.162" +
-                        "A 17.5 17.5 0 0 1 97.022 54.27" +
-                        "C 100.21 59.129 100.697 64.993 98.36 70.368" +
-                        "V 71.778" +
-                        "A 3.45 3.45 0 0 0 97.749 74.503" +
-                        "V 74.504" +
-                        "L 98.36 75.913" +
-                        "C 100.697 81.288 100.207 87.152 97.022 92.011" +
-                        "C 93.824 96.883 88.495 99.793 82.769 99.793" +
-                        "C 80.435 99.793 78.132 99.308 75.926 98.348" +
-                        "L 74.517 97.736" +
-                        "H 74.515" +
-                        "A 3.5 3.5 0 0 0 73.153 97.455" +
-                        "C 72.682 97.455 72.225 97.552 71.792 97.736" +
-                        "H 71.79" +
-                        "L 70.381 98.348" +
-                        "A 17.1 17.1 0 0 1 63.538 99.793" +
-                        "C 58.309 99.793 53.422 97.365 50.175 93.236" +
-                        "L 50.013 93.029" +
-                        "L 49.85 93.236" +
-                        "C 46.603 97.365 41.717 99.793 36.488 99.793" +
-                        "C 34.154 99.793 31.851 99.308 29.645 98.348" +
-                        "L 28.236 97.736" +
-                        "H 28.234" +
-                        "A 3.5 3.5 0 0 0 26.872 97.455" +
-                        "C 26.401 97.455 25.944 97.552 25.511 97.736" +
-                        "H 25.509" +
-                        "L 24.1 98.348" +
-                        "A 17.1 17.1 0 0 1 17.257 99.793" +
-                        "C 11.53 99.793 6.202 96.883 3.004 92.011" +
-                        "C -0.181 87.152 -0.671 81.288 1.661 75.913" +
-                        "L 2.277 74.504" +
-                        "V 74.503" +
-                        "A 3.45 3.45 0 0 0 2.277 71.778" +
-                        "V 71.777" +
-                        "L 1.665 70.368" +
-                        "C -0.671 64.993 -0.181 59.129 3.004 54.274" +
-                        "A 17.5 17.5 0 0 1 6.761 50.162" +
-                        "L 6.965 50" +
-                        "L 6.761 49.838" +
-                        "A 17.5 17.5 0 0 1 3.004 45.73" +
-                        "C -0.181 40.871 -0.671 35.007 1.665 29.632" +
-                        "L 2.277 28.223" +
-                        "V 28.222" +
-                        "A 3.45 3.45 0 0 0 2.277 25.497" +
-                        "V 25.496" +
-                        "L 1.665 24.087" +
-                        "C -0.671 18.712 -0.181 12.848 3.004 7.994" +
-                        "V 7.993" +
-                        "C 6.202 3.117 11.53 0.207 17.257 0.207" +
-                        "C 19.591 0.207 21.894 0.692 24.1 1.652" +
-                        "L 25.509 2.264" +
-                        "L 25.511 2.265" +
-                        "A 3.46 3.46 0 0 0 28.234 2.265" +
-                        "L 28.236 2.264" +
-                        "L 29.645 1.652" +
-                        "A 17.1 17.1 0 0 1 36.488 0.207" +
-                        "C 41.717 0.207 46.603 2.635 49.85 6.764" +
-                        "Z",
-                "arch" to
-                    "M 50 0" +
-                        "L 72.5 0" +
-                        "A 27.5 27.5 0 0 1 100 27.5" +
-                        "L 100 86.67" +
-                        "A 13.33 13.33 0 0 1 86.67 100" +
-                        "L 13.33 100" +
-                        "A 13.33 13.33 0 0 1 0 86.67" +
-                        "L 0 27.5" +
-                        "A 27.5 27.5 0 0 1 27.5 0" +
-                        "Z",
-                "square" to
-                    "M 50 0" +
-                        "L 83.4 0" +
-                        "A 16.6 16.6 0 0 1 100 16.6" +
-                        "L 100 83.4" +
-                        "A 16.6 16.6 0 0 1 83.4 100" +
-                        "L 16.6 100" +
-                        "A 16.6 16.6 0 0 1 0 83.4" +
-                        "L 0 16.6" +
-                        "A 16.6 16.6 0 0 1 16.6 0" +
-                        "Z",
-            )
-        } else {
-            mapOf("circle" to "M50 0A50 50,0,1,1,50 100A50 50,0,1,1,50 0")
-        }
+    private const val FOLDER_CLOVER_PATH =
+        "M 39.616 4 C 46.224 6.87 53.727 6.87 60.335 4 L 63.884 2.459 C 85.178 -6.789 106.789 14.822 97.541 36.116 L 96 39.665 C 93.13 46.273 93.13 53.776 96 60.384 L 97.541 63.934 C 106.789 85.227 85.178 106.839 63.884 97.591 L 60.335 96.049 C 53.727 93.179 46.224 93.179 39.616 96.049 L 36.066 97.591 C 14.773 106.839 -6.839 85.227 2.409 63.934 L 3.951 60.384 C 6.821 53.776 6.821 46.273 3.951 39.665 L 2.409 36.116 C -6.839 14.822 14.773 -6.789 36.066 2.459 Z"
+    private const val FOLDER_COMPLEX_CLOVER_PATH =
+        "M 49.85 6.764 L 50.013 6.971 L 50.175 6.764 C 53.422 2.635 58.309 0.207 63.538 0.207 C 65.872 0.207 68.175 0.692 70.381 1.648 L 71.79 2.264 L 71.792 2.265 A 3.46 3.46 0 0 0 74.515 2.265 L 74.517 2.264 L 75.926 1.652 A 17.1 17.1 0 0 1 82.769 0.207 C 88.495 0.207 93.824 3.117 97.022 7.989 C 100.21 12.848 100.697 18.712 98.36 24.087 L 97.749 25.496 V 25.497 A 3.45 3.45 0 0 0 97.749 28.222 V 28.223 L 98.36 29.632 C 100.697 35.007 100.207 40.871 97.022 45.73 A 17.5 17.5 0 0 1 93.264 49.838 L 93.06 50 L 93.264 50.162 A 17.5 17.5 0 0 1 97.022 54.27 C 100.21 59.129 100.697 64.993 98.36 70.368 V 71.778 A 3.45 3.45 0 0 0 97.749 74.503 V 74.504 L 98.36 75.913 C 100.697 81.288 100.207 87.152 97.022 92.011 C 93.824 96.883 88.495 99.793 82.769 99.793 C 80.435 99.793 78.132 99.308 75.926 98.348 L 74.517 97.736 H 74.515 A 3.5 3.5 0 0 0 73.153 97.455 C 72.682 97.455 72.225 97.552 71.792 97.736 H 71.79 L 70.381 98.348 A 17.1 17.1 0 0 1 63.538 99.793 C 58.309 99.793 53.422 97.365 50.175 93.236 L 50.013 93.029 L 49.85 93.236 C 46.603 97.365 41.717 99.793 36.488 99.793 C 34.154 99.793 31.851 99.308 29.645 98.348 L 28.236 97.736 H 28.234 A 3.5 3.5 0 0 0 26.872 97.455 C 26.401 97.455 25.944 97.552 25.511 97.736 H 25.509 L 24.1 98.348 A 17.1 17.1 0 0 1 17.257 99.793 C 11.53 99.793 6.202 96.883 3.004 92.011 C -0.181 87.152 -0.671 81.288 1.661 75.913 L 2.277 74.504 V 74.503 A 3.45 3.45 0 0 0 2.277 71.778 V 71.777 L 1.665 70.368 C -0.671 64.993 -0.181 59.129 3.004 54.274 A 17.5 17.5 0 0 1 6.761 50.162 L 6.965 50 L 6.761 49.838 A 17.5 17.5 0 0 1 3.004 45.73 C -0.181 40.871 -0.671 35.007 1.665 29.632 L 2.277 28.223 V 28.222 A 3.45 3.45 0 0 0 2.277 25.497 V 25.496 L 1.665 24.087 C -0.671 18.712 -0.181 12.848 3.004 7.994 V 7.993 C 6.202 3.117 11.53 0.207 17.257 0.207 C 19.591 0.207 21.894 0.692 24.1 1.652 L 25.509 2.264 L 25.511 2.265 A 3.46 3.46 0 0 0 28.234 2.265 L 28.236 2.264 L 29.645 1.652 A 17.1 17.1 0 0 1 36.488 0.207 C 41.717 0.207 46.603 2.635 49.85 6.764 Z"
+    private const val FOLDER_ARCH_PATH =
+        "M 50 0 L 72.5 0 A 27.5 27.5 0 0 1 100 27.5 L 100 86.67 A 13.33 13.33 0 0 1 86.67 100 L 13.33 100 A 13.33 13.33 0 0 1 0 86.67 L 0 27.5 A 27.5 27.5 0 0 1 27.5 0 Z"
+    private const val FOLDER_SQUARE_PATH =
+        "M 50 0 L 83.4 0 A 16.6 16.6 0 0 1 100 16.6 L 100 83.4 A 16.6 16.6 0 0 1 83.4 100 L 16.6 100 A 16.6 16.6 0 0 1 0 83.4 L 0 16.6 A 16.6 16.6 0 0 1 16.6 0 Z"
+    private const val CIRCLE_PATH = "M50 0A50 50,0,1,1,50 100A50 50,0,1,1,50 0"
+    private const val SQUARE_PATH =
+        "M53.689 0.82 L53.689 .82 C67.434 .82 74.306 .82 79.758 2.978 87.649 6.103 93.897 12.351 97.022 20.242 99.18 25.694 99.18 32.566 99.18 46.311 V53.689 C99.18 67.434 99.18 74.306 97.022 79.758 93.897 87.649 87.649 93.897 79.758 97.022 74.306 99.18 67.434 99.18 53.689 99.18 H46.311 C32.566 99.18 25.694 99.18 20.242 97.022 12.351 93.897 6.103 87.649 2.978 79.758 .82 74.306 .82 67.434 .82 53.689 L.82 46.311 C.82 32.566 .82 25.694 2.978 20.242 6.103 12.351 12.351 6.103 20.242 2.978 25.694 .82 32.566 .82 46.311 .82Z"
+    private const val FOUR_SIDED_COOKIE_PATH =
+        "M39.888,4.517C46.338 7.319 53.662 7.319 60.112 4.517L63.605 3C84.733 -6.176 106.176 15.268 97 36.395L95.483 39.888C92.681 46.338 92.681 53.662 95.483 60.112L97 63.605C106.176 84.732 84.733 106.176 63.605 97L60.112 95.483C53.662 92.681 46.338 92.681 39.888 95.483L36.395 97C15.267 106.176 -6.176 84.732 3 63.605L4.517 60.112C7.319 53.662 7.319 46.338 4.517 39.888L3 36.395C -6.176 15.268 15.267 -6.176 36.395 3Z"
+    private const val SEVEN_SIDED_COOKIE_PATH =
+        "M35.209 4.878C36.326 3.895 36.884 3.404 37.397 3.006 44.82 -2.742 55.18 -2.742 62.603 3.006 63.116 3.404 63.674 3.895 64.791 4.878 65.164 5.207 65.351 5.371 65.539 5.529 68.167 7.734 71.303 9.248 74.663 9.932 74.902 9.981 75.147 10.025 75.637 10.113 77.1 10.375 77.831 10.506 78.461 10.66 87.573 12.893 94.032 21.011 94.176 30.412 94.186 31.062 94.151 31.805 94.08 33.293 94.057 33.791 94.045 34.04 94.039 34.285 93.958 37.72 94.732 41.121 96.293 44.18 96.404 44.399 96.522 44.618 96.759 45.056 97.467 46.366 97.821 47.021 98.093 47.611 102.032 56.143 99.727 66.266 92.484 72.24 91.983 72.653 91.381 73.089 90.177 73.961 89.774 74.254 89.572 74.4 89.377 74.548 86.647 76.626 84.477 79.353 83.063 82.483 82.962 82.707 82.865 82.936 82.671 83.395 82.091 84.766 81.8 85.451 81.51 86.033 77.31 94.44 67.977 98.945 58.801 96.994 58.166 96.859 57.451 96.659 56.019 96.259 55.54 96.125 55.3 96.058 55.063 95.998 51.74 95.154 48.26 95.154 44.937 95.998 44.699 96.058 44.46 96.125 43.981 96.259 42.549 96.659 41.834 96.859 41.199 96.994 32.023 98.945 22.69 94.44 18.49 86.033 18.2 85.451 17.909 84.766 17.329 83.395 17.135 82.936 17.038 82.707 16.937 82.483 15.523 79.353 13.353 76.626 10.623 74.548 10.428 74.4 10.226 74.254 9.823 73.961 8.619 73.089 8.017 72.653 7.516 72.24 .273 66.266 -2.032 56.143 1.907 47.611 2.179 47.021 2.533 46.366 3.241 45.056 3.478 44.618 3.596 44.399 3.707 44.18 5.268 41.121 6.042 37.72 5.961 34.285 5.955 34.04 5.943 33.791 5.92 33.293 5.849 31.805 5.814 31.062 5.824 30.412 5.968 21.011 12.427 12.893 21.539 10.66 22.169 10.506 22.9 10.375 24.363 10.113 24.853 10.025 25.098 9.981 25.337 9.932 28.697 9.248 31.833 7.734 34.461 5.529 34.649 5.371 34.836 5.207 35.209 4.878Z"
+    private const val ARCH_PATH =
+        "M50 0C77.614 0 100 22.386 100 50C100 85.471 100 86.476 99.9 87.321 99.116 93.916 93.916 99.116 87.321 99.9 86.476 100 85.471 100 83.46 100H16.54C14.529 100 13.524 100 12.679 99.9 6.084 99.116 .884 93.916 .1 87.321 0 86.476 0 85.471 0 83.46L0 50C0 22.386 22.386 0 50 0Z"
+    @VisibleForTesting const val CIRCLE_KEY = "circle"
+    @VisibleForTesting const val SQUARE_KEY = "square"
+    @VisibleForTesting const val FOUR_SIDED_COOKIE_KEY = "four_sided_cookie"
+    @VisibleForTesting const val SEVEN_SIDED_COOKIE_KEY = "seven_sided_cookie"
+    @VisibleForTesting const val ARCH_KEY = "arch"
 
     val iconShapes =
         if (Flags.newCustomizationPickerUi() && LauncherFlags.enableLauncherIconShapes()) {
-            mapOf(
-                "circle" to
-                    IconShapeModel(
-                        key = "circle",
-                        title = "circle",
-                        pathString = "M50 0A50 50,0,1,1,50 100A50 50,0,1,1,50 0",
-                        folderPathString = folderShapes["clover"]!!,
-                    ),
-                "square" to
-                    IconShapeModel(
-                        key = "square",
-                        title = "square",
-                        pathString =
-                            "M53.689 0.82 L53.689 .82 C67.434 .82 74.306 .82 79.758 2.978 87.649 6.103 93.897 12.351 97.022 20.242 99.18 25.694 99.18 32.566 99.18 46.311 V53.689 C99.18 67.434 99.18 74.306 97.022 79.758 93.897 87.649 87.649 93.897 79.758 97.022 74.306 99.18 67.434 99.18 53.689 99.18 H46.311 C32.566 99.18 25.694 99.18 20.242 97.022 12.351 93.897 6.103 87.649 2.978 79.758 .82 74.306 .82 67.434 .82 53.689 L.82 46.311 C.82 32.566 .82 25.694 2.978 20.242 6.103 12.351 12.351 6.103 20.242 2.978 25.694 .82 32.566 .82 46.311 .82Z",
-                        folderShapes["square"]!!,
-                    ),
-                "four_sided_cookie" to
-                    IconShapeModel(
-                        key = "four_sided_cookie",
-                        title = "4 sided cookie",
-                        pathString =
-                            "M39.888,4.517C46.338 7.319 53.662 7.319 60.112 4.517L63.605 3C84.733 -6.176 106.176 15.268 97 36.395L95.483 39.888C92.681 46.338 92.681 53.662 95.483 60.112L97 63.605C106.176 84.732 84.733 106.176 63.605 97L60.112 95.483C53.662 92.681 46.338 92.681 39.888 95.483L36.395 97C15.267 106.176 -6.176 84.732 3 63.605L4.517 60.112C7.319 53.662 7.319 46.338 4.517 39.888L3 36.395C -6.176 15.268 15.267 -6.176 36.395 3Z",
-                        folderPathString = folderShapes["complexClover"]!!,
-                    ),
-                "seven_sided_cookie" to
-                    IconShapeModel(
-                        key = "seven_sided_cookie",
-                        title = "7 sided cookie",
-                        pathString =
-                            "M35.209 4.878C36.326 3.895 36.884 3.404 37.397 3.006 44.82 -2.742 55.18 -2.742 62.603 3.006 63.116 3.404 63.674 3.895 64.791 4.878 65.164 5.207 65.351 5.371 65.539 5.529 68.167 7.734 71.303 9.248 74.663 9.932 74.902 9.981 75.147 10.025 75.637 10.113 77.1 10.375 77.831 10.506 78.461 10.66 87.573 12.893 94.032 21.011 94.176 30.412 94.186 31.062 94.151 31.805 94.08 33.293 94.057 33.791 94.045 34.04 94.039 34.285 93.958 37.72 94.732 41.121 96.293 44.18 96.404 44.399 96.522 44.618 96.759 45.056 97.467 46.366 97.821 47.021 98.093 47.611 102.032 56.143 99.727 66.266 92.484 72.24 91.983 72.653 91.381 73.089 90.177 73.961 89.774 74.254 89.572 74.4 89.377 74.548 86.647 76.626 84.477 79.353 83.063 82.483 82.962 82.707 82.865 82.936 82.671 83.395 82.091 84.766 81.8 85.451 81.51 86.033 77.31 94.44 67.977 98.945 58.801 96.994 58.166 96.859 57.451 96.659 56.019 96.259 55.54 96.125 55.3 96.058 55.063 95.998 51.74 95.154 48.26 95.154 44.937 95.998 44.699 96.058 44.46 96.125 43.981 96.259 42.549 96.659 41.834 96.859 41.199 96.994 32.023 98.945 22.69 94.44 18.49 86.033 18.2 85.451 17.909 84.766 17.329 83.395 17.135 82.936 17.038 82.707 16.937 82.483 15.523 79.353 13.353 76.626 10.623 74.548 10.428 74.4 10.226 74.254 9.823 73.961 8.619 73.089 8.017 72.653 7.516 72.24 .273 66.266 -2.032 56.143 1.907 47.611 2.179 47.021 2.533 46.366 3.241 45.056 3.478 44.618 3.596 44.399 3.707 44.18 5.268 41.121 6.042 37.72 5.961 34.285 5.955 34.04 5.943 33.791 5.92 33.293 5.849 31.805 5.814 31.062 5.824 30.412 5.968 21.011 12.427 12.893 21.539 10.66 22.169 10.506 22.9 10.375 24.363 10.113 24.853 10.025 25.098 9.981 25.337 9.932 28.697 9.248 31.833 7.734 34.461 5.529 34.649 5.371 34.836 5.207 35.209 4.878Z",
-                        folderPathString = folderShapes["clover"]!!,
-                    ),
-                "arch" to
-                    IconShapeModel(
-                        key = "arch",
-                        title = "arch",
-                        pathString =
-                            "M50 0C77.614 0 100 22.386 100 50C100 85.471 100 86.476 99.9 87.321 99.116 93.916 93.916 99.116 87.321 99.9 86.476 100 85.471 100 83.46 100H16.54C14.529 100 13.524 100 12.679 99.9 6.084 99.116 .884 93.916 .1 87.321 0 86.476 0 85.471 0 83.46L0 50C0 22.386 22.386 0 50 0Z",
-                        folderPathString = folderShapes["arch"]!!,
-                    ),
-                "sunny" to
-                    IconShapeModel(
-                        key = "sunny",
-                        title = "sunny",
-                        pathString =
-                            "M42.846 4.873C46.084 -.531 53.916 -.531 57.154 4.873L60.796 10.951C62.685 14.103 66.414 15.647 69.978 14.754L76.851 13.032C82.962 11.5 88.5 17.038 86.968 23.149L85.246 30.022C84.353 33.586 85.897 37.315 89.049 39.204L95.127 42.846C100.531 46.084 100.531 53.916 95.127 57.154L89.049 60.796C85.897 62.685 84.353 66.414 85.246 69.978L86.968 76.851C88.5 82.962 82.962 88.5 76.851 86.968L69.978 85.246C66.414 84.353 62.685 85.898 60.796 89.049L57.154 95.127C53.916 100.531 46.084 100.531 42.846 95.127L39.204 89.049C37.315 85.898 33.586 84.353 30.022 85.246L23.149 86.968C17.038 88.5 11.5 82.962 13.032 76.851L14.754 69.978C15.647 66.414 14.103 62.685 10.951 60.796L4.873 57.154C -.531 53.916 -.531 46.084 4.873 42.846L10.951 39.204C14.103 37.315 15.647 33.586 14.754 30.022L13.032 23.149C11.5 17.038 17.038 11.5 23.149 13.032L30.022 14.754C33.586 15.647 37.315 14.103 39.204 10.951L42.846 4.873Z",
-                        folderPathString = folderShapes["clover"]!!,
-                    ),
+            arrayOf(
+                IconShapeModel(
+                    key = CIRCLE_KEY,
+                    title = "circle",
+                    pathString = CIRCLE_PATH,
+                    folderPathString = FOLDER_CLOVER_PATH,
+                ),
+                IconShapeModel(
+                    key = SQUARE_KEY,
+                    title = "square",
+                    pathString = SQUARE_PATH,
+                    folderPathString = FOLDER_SQUARE_PATH,
+                ),
+                IconShapeModel(
+                    key = FOUR_SIDED_COOKIE_KEY,
+                    title = "4 sided cookie",
+                    pathString = FOUR_SIDED_COOKIE_PATH,
+                    folderPathString = FOLDER_COMPLEX_CLOVER_PATH,
+                    iconScale = 72f / 83.4f,
+                ),
+                IconShapeModel(
+                    key = SEVEN_SIDED_COOKIE_KEY,
+                    title = "7 sided cookie",
+                    pathString = SEVEN_SIDED_COOKIE_PATH,
+                    folderPathString = FOLDER_CLOVER_PATH,
+                    iconScale = 72f / 80f,
+                ),
+                IconShapeModel(
+                    key = ARCH_KEY,
+                    title = "arch",
+                    pathString = ARCH_PATH,
+                    folderPathString = FOLDER_ARCH_PATH,
+                ),
             )
         } else {
-            mapOf(
-                "circle" to
-                    IconShapeModel(
-                        key = "circle",
-                        title = "circle",
-                        pathString = "M50 0A50 50,0,1,1,50 100A50 50,0,1,1,50 0",
-                    )
-            )
+            arrayOf(IconShapeModel(key = CIRCLE_KEY, title = "circle", pathString = CIRCLE_PATH))
         }
 }
diff --git a/src/com/android/launcher3/testing/TestInformationProvider.java b/src/com/android/launcher3/testing/TestInformationProvider.java
index 17b472a..4b592e7 100644
--- a/src/com/android/launcher3/testing/TestInformationProvider.java
+++ b/src/com/android/launcher3/testing/TestInformationProvider.java
@@ -16,61 +16,40 @@
 
 package com.android.launcher3.testing;
 
-import android.content.ContentProvider;
-import android.content.ContentValues;
-import android.database.Cursor;
-import android.net.Uri;
+import android.content.Context;
 import android.os.Bundle;
 import android.util.Log;
 
-import com.android.launcher3.Utilities;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 
-public class TestInformationProvider extends ContentProvider {
+import com.android.launcher3.Utilities;
+import com.android.launcher3.util.ContentProviderProxy;
+
+public class TestInformationProvider extends ContentProviderProxy {
 
     private static final String TAG = "TestInformationProvider";
 
+    @Nullable
     @Override
-    public boolean onCreate() {
-        return true;
-    }
-
-    @Override
-    public int update(Uri uri, ContentValues contentValues, String s, String[] strings) {
-        return 0;
-    }
-
-    @Override
-    public int delete(Uri uri, String s, String[] strings) {
-        return 0;
-    }
-
-    @Override
-    public Uri insert(Uri uri, ContentValues contentValues) {
-        return null;
-    }
-
-    @Override
-    public String getType(Uri uri) {
-        return null;
-    }
-
-    @Override
-    public Cursor query(Uri uri, String[] strings, String s, String[] strings1, String s1) {
-        return null;
-    }
-
-    @Override
-    public Bundle call(String method, String arg, Bundle extras) {
+    public ProxyProvider getProxy(@NonNull Context context) {
         if (Utilities.isRunningInTestHarness()) {
-            TestInformationHandler handler = TestInformationHandler.newInstance(getContext());
-            handler.init(getContext());
+            return new ProxyProvider() {
+                @Nullable
+                @Override
+                public Bundle call(@NonNull String method, @Nullable String arg,
+                        @Nullable Bundle extras) {
+                    TestInformationHandler handler = TestInformationHandler.newInstance(context);
+                    handler.init(context);
 
-            Bundle response =  handler.call(method, arg, extras);
-            if (response == null) {
-                Log.e(TAG, "Couldn't handle method: " + method + "; current handler="
-                        + handler.getClass().getSimpleName());
-            }
-            return response;
+                    Bundle response = handler.call(method, arg, extras);
+                    if (response == null) {
+                        Log.e(TAG, "Couldn't handle method: " + method + "; current handler="
+                                + handler.getClass().getSimpleName());
+                    }
+                    return response;
+                }
+            };
         }
         return null;
     }
diff --git a/src/com/android/launcher3/touch/AllAppsSwipeController.java b/src/com/android/launcher3/touch/AllAppsSwipeController.java
index 107bcc1..2cc4909 100644
--- a/src/com/android/launcher3/touch/AllAppsSwipeController.java
+++ b/src/com/android/launcher3/touch/AllAppsSwipeController.java
@@ -207,6 +207,11 @@
             }
             config.setInterpolator(ANIM_WORKSPACE_SCALE, DECELERATED_EASE);
             config.setInterpolator(ANIM_DEPTH, DECELERATED_EASE);
+            if (launcher.getDeviceProfile().isPhone) {
+                config.setInterpolator(ANIM_WORKSPACE_FADE, INSTANT);
+                config.setInterpolator(ANIM_HOTSEAT_FADE, INSTANT);
+                config.animFlags |= StateAnimationConfig.SKIP_DEPTH_CONTROLLER;
+            }
         } else {
             if (config.isUserControlled()) {
                 config.setInterpolator(ANIM_DEPTH, Interpolators.reverse(BLUR_MANUAL));
@@ -248,6 +253,11 @@
             }
             config.setInterpolator(ANIM_WORKSPACE_SCALE, DECELERATED_EASE);
             config.setInterpolator(ANIM_DEPTH, DECELERATED_EASE);
+            if (launcher.getDeviceProfile().isPhone) {
+                config.setInterpolator(ANIM_WORKSPACE_FADE, FINAL_FRAME);
+                config.setInterpolator(ANIM_HOTSEAT_FADE, FINAL_FRAME);
+                config.animFlags |= StateAnimationConfig.SKIP_DEPTH_CONTROLLER;
+            }
         } else {
             config.setInterpolator(ANIM_DEPTH,
                     config.isUserControlled() ? BLUR_MANUAL : BLUR_ATOMIC);
diff --git a/src/com/android/launcher3/util/ApiWrapper.java b/src/com/android/launcher3/util/ApiWrapper.java
index 56337b0..0510d59 100644
--- a/src/com/android/launcher3/util/ApiWrapper.java
+++ b/src/com/android/launcher3/util/ApiWrapper.java
@@ -216,6 +216,13 @@
         return 0;
     }
 
+    /**
+     * Checks if the shortcut is using an icon with file or URI source
+     */
+    public boolean isFileDrawable(@NonNull ShortcutInfo shortcutInfo) {
+        return false;
+    }
+
     private static class NoopDrawable extends ColorDrawable {
         @Override
         public int getIntrinsicHeight() {
diff --git a/src/com/android/launcher3/util/ContentProviderProxy.kt b/src/com/android/launcher3/util/ContentProviderProxy.kt
new file mode 100644
index 0000000..db693db
--- /dev/null
+++ b/src/com/android/launcher3/util/ContentProviderProxy.kt
@@ -0,0 +1,86 @@
+/*
+ * 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.util
+
+import android.content.ContentProvider
+import android.content.ContentValues
+import android.content.Context
+import android.database.Cursor
+import android.net.Uri
+import android.os.Bundle
+
+/** Wrapper around [ContentProvider] which allows delegating all calls to an interface */
+abstract class ContentProviderProxy : ContentProvider() {
+
+    override fun onCreate() = true
+
+    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int =
+        checkGetProxy()?.delete(uri, selection, selectionArgs) ?: 0
+
+    /** Do not route this call through proxy as it doesn't generally require initializing objects */
+    override fun getType(uri: Uri): String? = null
+
+    override fun insert(uri: Uri, values: ContentValues?): Uri? =
+        checkGetProxy()?.insert(uri, values)
+
+    override fun query(
+        uri: Uri,
+        projection: Array<out String>?,
+        selection: String?,
+        selectionArgs: Array<out String>?,
+        sortOrder: String?,
+    ): Cursor? = checkGetProxy()?.query(uri, projection, selection, selectionArgs, sortOrder)
+
+    override fun update(
+        uri: Uri,
+        values: ContentValues?,
+        selection: String?,
+        selectionArgs: Array<out String>?,
+    ): Int = checkGetProxy()?.update(uri, values, selection, selectionArgs) ?: 0
+
+    override fun call(method: String, arg: String?, extras: Bundle?): Bundle? =
+        checkGetProxy()?.call(method, arg, extras)
+
+    private fun checkGetProxy(): ProxyProvider? = context?.let { getProxy(it) }
+
+    abstract fun getProxy(ctx: Context): ProxyProvider?
+
+    /** Interface for handling the actual content provider calls */
+    interface ProxyProvider {
+
+        fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int = 0
+
+        fun insert(uri: Uri, values: ContentValues?): Uri? = null
+
+        fun query(
+            uri: Uri,
+            projection: Array<out String>?,
+            selection: String?,
+            selectionArgs: Array<out String>?,
+            sortOrder: String?,
+        ): Cursor? = null
+
+        fun update(
+            uri: Uri,
+            values: ContentValues?,
+            selection: String?,
+            selectionArgs: Array<out String>?,
+        ): Int = 0
+
+        fun call(method: String, arg: String?, extras: Bundle?): Bundle? = null
+    }
+}
diff --git a/src/com/android/launcher3/util/DisplayController.java b/src/com/android/launcher3/util/DisplayController.java
index ee1af81..52f8887 100644
--- a/src/com/android/launcher3/util/DisplayController.java
+++ b/src/com/android/launcher3/util/DisplayController.java
@@ -18,6 +18,7 @@
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
 
+import static com.android.launcher3.Flags.enableOverviewOnConnectedDisplays;
 import static com.android.launcher3.InvariantDeviceProfile.TYPE_MULTI_DISPLAY;
 import static com.android.launcher3.InvariantDeviceProfile.TYPE_PHONE;
 import static com.android.launcher3.InvariantDeviceProfile.TYPE_TABLET;
@@ -42,9 +43,11 @@
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.Log;
+import android.util.SparseArray;
 import android.view.Display;
 
 import androidx.annotation.AnyThread;
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
 import androidx.annotation.VisibleForTesting;
@@ -63,8 +66,10 @@
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
@@ -78,8 +83,7 @@
  */
 @SuppressLint("NewApi")
 @LauncherAppSingleton
-public class DisplayController implements ComponentCallbacks,
-        DesktopVisibilityListener {
+public class DisplayController implements DesktopVisibilityListener {
 
     private static final String TAG = "DisplayController";
     private static final boolean DEBUG = false;
@@ -99,31 +103,29 @@
     public static final int CHANGE_NAVIGATION_MODE = 1 << 4;
     public static final int CHANGE_TASKBAR_PINNING = 1 << 5;
     public static final int CHANGE_DESKTOP_MODE = 1 << 6;
+    public static final int CHANGE_SHOW_LOCKED_TASKBAR = 1 << 7;
 
     public static final int CHANGE_ALL = CHANGE_ACTIVE_SCREEN | CHANGE_ROTATION
             | CHANGE_DENSITY | CHANGE_SUPPORTED_BOUNDS | CHANGE_NAVIGATION_MODE
-            | CHANGE_TASKBAR_PINNING | CHANGE_DESKTOP_MODE;
+            | CHANGE_TASKBAR_PINNING | CHANGE_DESKTOP_MODE | CHANGE_SHOW_LOCKED_TASKBAR;
 
     private static final String ACTION_OVERLAY_CHANGED = "android.intent.action.OVERLAY_CHANGED";
     private static final String TARGET_OVERLAY_PACKAGE = "android";
 
-    private final Context mContext;
     private final WindowManagerProxy mWMProxy;
 
-    // Null for SDK < S
-    private final Context mWindowContext;
+    private final @ApplicationContext Context mAppContext;
 
     // The callback in this listener updates DeviceProfile, which other listeners might depend on
     private DisplayInfoChangeListener mPriorityListener;
-    private final CopyOnWriteArrayList<DisplayInfoChangeListener> mListeners =
-            new CopyOnWriteArrayList<>();
+
+    private final SparseArray<PerDisplayInfo> mPerDisplayInfo =
+            new SparseArray<>();
 
     // We will register broadcast receiver on main thread to ensure not missing changes on
     // TARGET_OVERLAY_PACKAGE and ACTION_OVERLAY_CHANGED.
-    private final SimpleBroadcastReceiver mReceiver =
-            new SimpleBroadcastReceiver(MAIN_EXECUTOR, this::onIntent);
+    private final SimpleBroadcastReceiver mReceiver;
 
-    private Info mInfo;
     private boolean mDestroyed = false;
 
     @Inject
@@ -131,19 +133,20 @@
             WindowManagerProxy wmProxy,
             LauncherPrefs prefs,
             DaggerSingletonTracker lifecycle) {
-        mContext = context;
+        mAppContext = context;
         mWMProxy = wmProxy;
 
         if (enableTaskbarPinning()) {
             LauncherPrefChangeListener prefListener = key -> {
+                Info info = getInfo();
                 boolean isTaskbarPinningChanged = TASKBAR_PINNING_KEY.equals(key)
-                        && mInfo.mIsTaskbarPinned != prefs.get(TASKBAR_PINNING);
+                        && info.mIsTaskbarPinned != prefs.get(TASKBAR_PINNING);
                 boolean isTaskbarPinningDesktopModeChanged =
                         TASKBAR_PINNING_DESKTOP_MODE_KEY.equals(key)
-                                && mInfo.mIsTaskbarPinnedInDesktopMode != prefs.get(
+                                && info.mIsTaskbarPinnedInDesktopMode != prefs.get(
                                 TASKBAR_PINNING_IN_DESKTOP_MODE);
                 if (isTaskbarPinningChanged || isTaskbarPinningDesktopModeChanged) {
-                    notifyConfigChange();
+                    notifyConfigChange(DEFAULT_DISPLAY);
                 }
             };
 
@@ -153,23 +156,50 @@
                         prefListener, TASKBAR_PINNING, TASKBAR_PINNING_IN_DESKTOP_MODE));
         }
 
-        Display display = context.getSystemService(DisplayManager.class)
-                .getDisplay(DEFAULT_DISPLAY);
-        mWindowContext = mContext.createWindowContext(display, TYPE_APPLICATION, null);
-        mWindowContext.registerComponentCallbacks(this);
+        DisplayManager displayManager = context.getSystemService(DisplayManager.class);
+        Display defaultDisplay = displayManager.getDisplay(DEFAULT_DISPLAY);
+        PerDisplayInfo defaultPerDisplayInfo = getOrCreatePerDisplayInfo(defaultDisplay);
 
         // Initialize navigation mode change listener
-        mReceiver.registerPkgActions(mContext, TARGET_OVERLAY_PACKAGE, ACTION_OVERLAY_CHANGED);
+        mReceiver = new SimpleBroadcastReceiver(context, MAIN_EXECUTOR, this::onIntent);
+        mReceiver.registerPkgActions(TARGET_OVERLAY_PACKAGE, ACTION_OVERLAY_CHANGED);
 
-        mInfo = new Info(mWindowContext, wmProxy,
-                wmProxy.estimateInternalDisplayBounds(mWindowContext));
         wmProxy.registerDesktopVisibilityListener(this);
-        FileLog.i(TAG, "(CTOR) perDisplayBounds: " + mInfo.mPerDisplayBounds);
+        FileLog.i(TAG, "(CTOR) perDisplayBounds: "
+                + defaultPerDisplayInfo.mInfo.mPerDisplayBounds);
+
+        if (enableOverviewOnConnectedDisplays()) {
+            final DisplayManager.DisplayListener displayListener =
+                    new DisplayManager.DisplayListener() {
+                        @Override
+                        public void onDisplayAdded(int displayId) {
+                            getOrCreatePerDisplayInfo(displayManager.getDisplay(displayId));
+                        }
+
+                        @Override
+                        public void onDisplayChanged(int displayId) {
+                        }
+
+                        @Override
+                        public void onDisplayRemoved(int displayId) {
+                            removePerDisplayInfo(displayId);
+                        }
+                    };
+            displayManager.registerDisplayListener(displayListener, MAIN_EXECUTOR.getHandler());
+            lifecycle.addCloseable(() -> {
+                displayManager.unregisterDisplayListener(displayListener);
+            });
+            // Add any PerDisplayInfos for already-connected displays.
+            Arrays.stream(displayManager.getDisplays())
+                    .forEach((it) ->
+                            getOrCreatePerDisplayInfo(
+                                    displayManager.getDisplay(it.getDisplayId())));
+        }
 
         lifecycle.addCloseable(() -> {
             mDestroyed = true;
-            mWindowContext.unregisterComponentCallbacks(this);
-            mReceiver.unregisterReceiverSafely(mContext);
+            defaultPerDisplayInfo.cleanup();
+            mReceiver.unregisterReceiverSafely();
             wmProxy.unregisterDesktopVisibilityListener(this);
         });
     }
@@ -214,6 +244,13 @@
     }
 
     /**
+     * Returns whether the taskbar is pinned in gesture navigation mode.
+     */
+    public static boolean isInDesktopMode(Context context) {
+        return INSTANCE.get(context).getInfo().isInDesktopMode();
+    }
+
+    /**
      * Returns whether the taskbar is forced to be pinned when home is visible.
      */
     public static boolean showLockedTaskbarOnHome(Context context) {
@@ -229,8 +266,8 @@
     }
 
     @Override
-    public void onDesktopVisibilityChanged(boolean visible) {
-        notifyConfigChange();
+    public void onIsInDesktopModeChanged(int displayId, boolean isInDesktopModeAndNotInOverview) {
+        notifyConfigChange(displayId);
     }
 
     /**
@@ -253,60 +290,88 @@
         }
         if (ACTION_OVERLAY_CHANGED.equals(intent.getAction())) {
             Log.d(TAG, "Overlay changed, notifying listeners");
-            notifyConfigChange();
+            notifyConfigChange(DEFAULT_DISPLAY);
         }
     }
 
+    @VisibleForTesting
+    public void onConfigurationChanged(Configuration config) {
+        onConfigurationChanged(config, DEFAULT_DISPLAY);
+    }
+
     @UiThread
-    @Override
-    public final void onConfigurationChanged(Configuration config) {
+    private void onConfigurationChanged(Configuration config, int displayId) {
         Log.d(TASKBAR_NOT_DESTROYED_TAG, "DisplayController#onConfigurationChanged: " + config);
-        if (config.densityDpi != mInfo.densityDpi
-                || config.fontScale != mInfo.fontScale
-                || !mInfo.mScreenSizeDp.equals(
-                        new PortraitSize(config.screenHeightDp, config.screenWidthDp))
-                || mWindowContext.getDisplay().getRotation() != mInfo.rotation
-                || mWMProxy.showLockedTaskbarOnHome(mWindowContext)
-                        != mInfo.showLockedTaskbarOnHome()
-                || mWMProxy.showDesktopTaskbarForFreeformDisplay(mWindowContext)
-                        != mInfo.showDesktopTaskbarForFreeformDisplay()) {
-            notifyConfigChange();
+        PerDisplayInfo perDisplayInfo = mPerDisplayInfo.get(displayId);
+        Context windowContext = perDisplayInfo.mWindowContext;
+        Info info = perDisplayInfo.mInfo;
+        if (config.densityDpi != info.densityDpi
+                || config.fontScale != info.fontScale
+                || !info.mScreenSizeDp.equals(
+                    new PortraitSize(config.screenHeightDp, config.screenWidthDp))
+                || windowContext.getDisplay().getRotation() != info.rotation
+                || mWMProxy.showLockedTaskbarOnHome(windowContext)
+                != info.showLockedTaskbarOnHome()
+                || mWMProxy.showDesktopTaskbarForFreeformDisplay(windowContext)
+                != info.showDesktopTaskbarForFreeformDisplay()) {
+            notifyConfigChange(displayId);
         }
     }
 
-    @Override
-    public final void onLowMemory() { }
-
     public void setPriorityListener(DisplayInfoChangeListener listener) {
         mPriorityListener = listener;
     }
 
     public void addChangeListener(DisplayInfoChangeListener listener) {
-        mListeners.add(listener);
+        addChangeListenerForDisplay(listener, DEFAULT_DISPLAY);
     }
 
     public void removeChangeListener(DisplayInfoChangeListener listener) {
-        mListeners.remove(listener);
+        removeChangeListenerForDisplay(listener, DEFAULT_DISPLAY);
+    }
+
+    public void addChangeListenerForDisplay(DisplayInfoChangeListener listener, int displayId) {
+        PerDisplayInfo perDisplayInfo = mPerDisplayInfo.get(displayId);
+        if (perDisplayInfo != null) {
+            perDisplayInfo.addListener(listener);
+        }
+    }
+
+    public void removeChangeListenerForDisplay(DisplayInfoChangeListener listener, int displayId) {
+        PerDisplayInfo perDisplayInfo = mPerDisplayInfo.get(displayId);
+        if (perDisplayInfo != null) {
+            perDisplayInfo.removeListener(listener);
+        }
     }
 
     public Info getInfo() {
-        return mInfo;
+        return mPerDisplayInfo.get(DEFAULT_DISPLAY).mInfo;
+    }
+
+    public @Nullable Info getInfoForDisplay(int displayId) {
+        if (enableOverviewOnConnectedDisplays()) {
+            PerDisplayInfo perDisplayInfo = mPerDisplayInfo.get(displayId);
+            if (perDisplayInfo != null) {
+                return perDisplayInfo.mInfo;
+            } else {
+                return null;
+            }
+        } else {
+            return getInfo();
+        }
     }
 
     @AnyThread
     public void notifyConfigChange() {
-        Info oldInfo = mInfo;
+        notifyConfigChange(DEFAULT_DISPLAY);
+    }
 
-        Context displayInfoContext = mWindowContext;
-        Info newInfo = new Info(displayInfoContext, mWMProxy, oldInfo.mPerDisplayBounds);
+    @AnyThread
+    public void notifyConfigChange(int displayId) {
+        notifyConfigChangeForDisplay(displayId);
+    }
 
-        if (newInfo.densityDpi != oldInfo.densityDpi || newInfo.fontScale != oldInfo.fontScale
-                || newInfo.getNavigationMode() != oldInfo.getNavigationMode()) {
-            // Cache may not be valid anymore, recreate without cache
-            newInfo = new Info(displayInfoContext, mWMProxy,
-                    mWMProxy.estimateInternalDisplayBounds(displayInfoContext));
-        }
-
+    private int calculateChange(Info oldInfo, Info newInfo) {
         int change = 0;
         if (!newInfo.normalizedDisplayInfo.equals(oldInfo.normalizedDisplayInfo)) {
             change |= CHANGE_ACTIVE_SCREEN;
@@ -328,34 +393,82 @@
         }
         if ((newInfo.mIsTaskbarPinned != oldInfo.mIsTaskbarPinned)
                 || (newInfo.mIsTaskbarPinnedInDesktopMode
-                    != oldInfo.mIsTaskbarPinnedInDesktopMode)
+                != oldInfo.mIsTaskbarPinnedInDesktopMode)
                 || newInfo.isPinnedTaskbar() != oldInfo.isPinnedTaskbar()) {
             change |= CHANGE_TASKBAR_PINNING;
         }
         if (newInfo.mIsInDesktopMode != oldInfo.mIsInDesktopMode) {
             change |= CHANGE_DESKTOP_MODE;
         }
+        if (newInfo.mShowLockedTaskbarOnHome != oldInfo.mShowLockedTaskbarOnHome) {
+            change |= CHANGE_SHOW_LOCKED_TASKBAR;
+        }
 
         if (DEBUG) {
             Log.d(TAG, "handleInfoChange - change: " + getChangeFlagsString(change));
         }
+        return change;
+    }
 
-        if (change != 0) {
-            mInfo = newInfo;
-            final int flags = change;
-            MAIN_EXECUTOR.execute(() -> notifyChange(displayInfoContext, flags));
+    private Info getNewInfo(Info oldInfo, Context displayInfoContext) {
+        Info newInfo = new Info(displayInfoContext, mWMProxy, oldInfo.mPerDisplayBounds);
+
+        if (newInfo.densityDpi != oldInfo.densityDpi || newInfo.fontScale != oldInfo.fontScale
+                || newInfo.getNavigationMode() != oldInfo.getNavigationMode()) {
+            // Cache may not be valid anymore, recreate without cache
+            newInfo = new Info(displayInfoContext, mWMProxy,
+                    mWMProxy.estimateInternalDisplayBounds(displayInfoContext));
+        }
+        return newInfo;
+    }
+
+    @AnyThread
+    public void notifyConfigChangeForDisplay(int displayId) {
+        PerDisplayInfo perDisplayInfo = mPerDisplayInfo.get(displayId);
+        if (perDisplayInfo == null) return;
+        Info oldInfo = perDisplayInfo.mInfo;
+        final Info newInfo = getNewInfo(oldInfo, perDisplayInfo.mWindowContext);
+        final int flags = calculateChange(oldInfo, newInfo);
+        if (flags != 0) {
+            MAIN_EXECUTOR.execute(() -> {
+                perDisplayInfo.mInfo = newInfo;
+                if (displayId == DEFAULT_DISPLAY && mPriorityListener != null) {
+                    mPriorityListener.onDisplayInfoChanged(perDisplayInfo.mWindowContext, newInfo,
+                            flags);
+                }
+                perDisplayInfo.notifyListeners(newInfo, flags);
+            });
         }
     }
 
-    private void notifyChange(Context context, int flags) {
-        if (mPriorityListener != null) {
-            mPriorityListener.onDisplayInfoChanged(context, mInfo, flags);
+    private PerDisplayInfo getOrCreatePerDisplayInfo(Display display) {
+        int displayId = display.getDisplayId();
+        PerDisplayInfo perDisplayInfo = mPerDisplayInfo.get(displayId);
+        if (perDisplayInfo != null) {
+            return perDisplayInfo;
         }
+        if (DEBUG) {
+            Log.d(TAG,
+                    String.format("getOrCreatePerDisplayInfo - no cached value found for %d",
+                            displayId));
+        }
+        Context windowContext = mAppContext.createWindowContext(display, TYPE_APPLICATION, null);
+        Info info = new Info(windowContext, mWMProxy,
+                mWMProxy.estimateInternalDisplayBounds(windowContext));
+        perDisplayInfo = new PerDisplayInfo(displayId, windowContext, info);
+        mPerDisplayInfo.put(displayId, perDisplayInfo);
+        return perDisplayInfo;
+    }
 
-        int count = mListeners.size();
-        for (int i = 0; i < count; i++) {
-            mListeners.get(i).onDisplayInfoChanged(context, mInfo, flags);
-        }
+    /**
+     * Clean up resources for the given display id.
+     * @param displayId The display id
+     */
+    void removePerDisplayInfo(int displayId) {
+        PerDisplayInfo info = mPerDisplayInfo.get(displayId);
+        if (info == null) return;
+        info.cleanup();
+        mPerDisplayInfo.remove(displayId);
     }
 
     public static class Info {
@@ -448,7 +561,7 @@
             mIsTaskbarPinned = LauncherPrefs.get(displayInfoContext).get(TASKBAR_PINNING);
             mIsTaskbarPinnedInDesktopMode = LauncherPrefs.get(displayInfoContext).get(
                     TASKBAR_PINNING_IN_DESKTOP_MODE);
-            mIsInDesktopMode = wmProxy.isInDesktopMode();
+            mIsInDesktopMode = wmProxy.isInDesktopMode(DEFAULT_DISPLAY);
             mShowLockedTaskbarOnHome = wmProxy.showLockedTaskbarOnHome(displayInfoContext);
             mShowDesktopTaskbarForFreeformDisplay = wmProxy.showDesktopTaskbarForFreeformDisplay(
                     displayInfoContext);
@@ -494,6 +607,13 @@
         }
 
         /**
+         * Returns whether the taskbar is in desktop mode.
+         */
+        public boolean isInDesktopMode() {
+            return mIsInDesktopMode;
+        }
+
+        /**
          * Returns {@code true} if the bounds represent a tablet.
          */
         public boolean isTablet(WindowBounds bounds) {
@@ -575,6 +695,7 @@
         appendFlag(result, change, CHANGE_NAVIGATION_MODE, "CHANGE_NAVIGATION_MODE");
         appendFlag(result, change, CHANGE_TASKBAR_PINNING, "CHANGE_TASKBAR_VARIANT");
         appendFlag(result, change, CHANGE_DESKTOP_MODE, "CHANGE_DESKTOP_MODE");
+        appendFlag(result, change, CHANGE_SHOW_LOCKED_TASKBAR, "CHANGE_SHOW_LOCKED_TASKBAR");
         return result.toString();
     }
 
@@ -582,20 +703,29 @@
      * Dumps the current state information
      */
     public void dump(PrintWriter pw) {
-        Info info = mInfo;
-        pw.println("DisplayController.Info:");
-        pw.println("  normalizedDisplayInfo=" + info.normalizedDisplayInfo);
-        pw.println("  rotation=" + info.rotation);
-        pw.println("  fontScale=" + info.fontScale);
-        pw.println("  densityDpi=" + info.densityDpi);
-        pw.println("  navigationMode=" + info.getNavigationMode().name());
-        pw.println("  isTaskbarPinned=" + info.mIsTaskbarPinned);
-        pw.println("  isTaskbarPinnedInDesktopMode=" + info.mIsTaskbarPinnedInDesktopMode);
-        pw.println("  isInDesktopMode=" + info.mIsInDesktopMode);
-        pw.println("  currentSize=" + info.currentSize);
-        info.mPerDisplayBounds.forEach((key, value) -> pw.println(
-                "  perDisplayBounds - " + key + ": " + value));
-        pw.println("  isTransientTaskbar=" + info.isTransientTaskbar());
+        int count = mPerDisplayInfo.size();
+        for (int i = 0; i < count; ++i) {
+            int displayId = mPerDisplayInfo.keyAt(i);
+            Info info = getInfoForDisplay(displayId);
+            if (info == null) {
+                continue;
+            }
+            pw.println(String.format(Locale.ENGLISH, "DisplayController.Info (displayId=%d):",
+                    displayId));
+            pw.println("  normalizedDisplayInfo=" + info.normalizedDisplayInfo);
+            pw.println("  rotation=" + info.rotation);
+            pw.println("  fontScale=" + info.fontScale);
+            pw.println("  densityDpi=" + info.densityDpi);
+            pw.println("  navigationMode=" + info.getNavigationMode().name());
+            pw.println("  isTaskbarPinned=" + info.mIsTaskbarPinned);
+            pw.println("  isTaskbarPinnedInDesktopMode=" + info.mIsTaskbarPinnedInDesktopMode);
+            pw.println("  isInDesktopMode=" + info.mIsInDesktopMode);
+            pw.println("  showLockedTaskbarOnHome=" + info.showLockedTaskbarOnHome());
+            pw.println("  currentSize=" + info.currentSize);
+            info.mPerDisplayBounds.forEach((key, value) -> pw.println(
+                    "  perDisplayBounds - " + key + ": " + value));
+            pw.println("  isTransientTaskbar=" + info.isTransientTaskbar());
+        }
     }
 
     /**
@@ -623,4 +753,47 @@
         }
     }
 
+    private class PerDisplayInfo implements ComponentCallbacks {
+        final int mDisplayId;
+        final CopyOnWriteArrayList<DisplayInfoChangeListener> mListeners =
+                new CopyOnWriteArrayList<>();
+        final Context mWindowContext;
+        Info mInfo;
+
+        PerDisplayInfo(int displayId, Context windowContext, Info info) {
+            this.mDisplayId = displayId;
+            this.mWindowContext = windowContext;
+            this.mInfo = info;
+            windowContext.registerComponentCallbacks(this);
+        }
+
+        void addListener(DisplayInfoChangeListener listener) {
+            mListeners.add(listener);
+        }
+
+        void removeListener(DisplayInfoChangeListener listener) {
+            mListeners.remove(listener);
+        }
+
+        void notifyListeners(Info info, int flags) {
+            int count = mListeners.size();
+            for (int i = 0; i < count; ++i) {
+                mListeners.get(i).onDisplayInfoChanged(mWindowContext, info, flags);
+            }
+        }
+
+        @Override
+        public void onConfigurationChanged(@NonNull Configuration newConfig) {
+            DisplayController.this.onConfigurationChanged(newConfig, mDisplayId);
+        }
+
+        @Override
+        public void onLowMemory() {}
+
+        void cleanup() {
+            mWindowContext.unregisterComponentCallbacks(this);
+            mListeners.clear();
+        }
+    }
+
 }
diff --git a/src/com/android/launcher3/util/Executors.java b/src/com/android/launcher3/util/Executors.java
index c622b71..296fc8a 100644
--- a/src/com/android/launcher3/util/Executors.java
+++ b/src/com/android/launcher3/util/Executors.java
@@ -16,8 +16,8 @@
 package com.android.launcher3.util;
 
 import static android.os.Process.THREAD_PRIORITY_BACKGROUND;
+import static android.os.Process.THREAD_PRIORITY_FOREGROUND;
 
-import android.os.HandlerThread;
 import android.os.Looper;
 import android.os.Process;
 
@@ -51,21 +51,20 @@
     /**
      * An {@link LooperExecutor} to be used with async task where order is important.
      */
-    public static final LooperExecutor ORDERED_BG_EXECUTOR = new LooperExecutor(
-            createAndStartNewLooper("BackgroundExecutor", THREAD_PRIORITY_BACKGROUND));
+    public static final LooperExecutor ORDERED_BG_EXECUTOR =
+            new LooperExecutor("BackgroundExecutor", THREAD_PRIORITY_BACKGROUND);
 
     /**
      * Returns the executor for running tasks on the main thread.
      */
     public static final LooperExecutor MAIN_EXECUTOR =
-            new LooperExecutor(Looper.getMainLooper());
+            new LooperExecutor(Looper.getMainLooper(), THREAD_PRIORITY_FOREGROUND);
 
     /**
      * A background executor for using time sensitive actions where user is waiting for response.
      */
     public static final LooperExecutor UI_HELPER_EXECUTOR =
-            new LooperExecutor(
-                    createAndStartNewLooper("UiThreadHelper", Process.THREAD_PRIORITY_FOREGROUND));
+            new LooperExecutor("UiThreadHelper", Process.THREAD_PRIORITY_FOREGROUND);
 
 
     /** A background executor to preinflate views. */
@@ -75,26 +74,9 @@
                             "preinflate-allapps-icons", THREAD_PRIORITY_BACKGROUND));
 
     /**
-     * Utility method to get a started handler thread statically
-     */
-    public static Looper createAndStartNewLooper(String name) {
-        return createAndStartNewLooper(name, Process.THREAD_PRIORITY_DEFAULT);
-    }
-
-    /**
-     * Utility method to get a started handler thread statically with the provided priority
-     */
-    public static Looper createAndStartNewLooper(String name, int priority) {
-        HandlerThread thread = new HandlerThread(name, priority);
-        thread.start();
-        return thread.getLooper();
-    }
-
-    /**
      * Executor used for running Launcher model related tasks (eg loading icons or updated db)
      */
-    public static final LooperExecutor MODEL_EXECUTOR =
-            new LooperExecutor(createAndStartNewLooper("launcher-loader"));
+    public static final LooperExecutor MODEL_EXECUTOR = new LooperExecutor("launcher-loader");
 
     /**
      * Returns and caches a single thread executor for a given package.
@@ -102,9 +84,7 @@
      * @param packageName Package associated with the executor.
      */
     public static LooperExecutor getPackageExecutor(String packageName) {
-        return PACKAGE_EXECUTORS.computeIfAbsent(
-                packageName, p -> new LooperExecutor(
-                        createAndStartNewLooper(p, Process.THREAD_PRIORITY_DEFAULT)));
+        return PACKAGE_EXECUTORS.computeIfAbsent(packageName, LooperExecutor::new);
     }
 
     /**
diff --git a/src/com/android/launcher3/util/LockedUserState.kt b/src/com/android/launcher3/util/LockedUserState.kt
index a6a6ceb..742a327 100644
--- a/src/com/android/launcher3/util/LockedUserState.kt
+++ b/src/com/android/launcher3/util/LockedUserState.kt
@@ -44,7 +44,7 @@
 
     @VisibleForTesting
     val userUnlockedReceiver =
-        SimpleBroadcastReceiver(UI_HELPER_EXECUTOR) {
+        SimpleBroadcastReceiver(context, UI_HELPER_EXECUTOR) {
             if (Intent.ACTION_USER_UNLOCKED == it.action) {
                 isUserUnlocked = true
             }
@@ -61,7 +61,6 @@
         isUserUnlockedAtLauncherStartup = isUserUnlocked
         if (!isUserUnlocked) {
             userUnlockedReceiver.register(
-                context,
                 {
                     // If user is unlocked while registering broadcast receiver, we should update
                     // [isUserUnlocked], which will call [notifyUserUnlocked] in setter
@@ -72,7 +71,7 @@
                 Intent.ACTION_USER_UNLOCKED,
             )
         }
-        lifeCycle.addCloseable { userUnlockedReceiver.unregisterReceiverSafely(context) }
+        lifeCycle.addCloseable { userUnlockedReceiver.unregisterReceiverSafely() }
     }
 
     private fun checkIsUserUnlocked() =
@@ -80,7 +79,7 @@
 
     private fun notifyUserUnlocked() {
         mUserUnlockedActions.executeAllAndDestroy()
-        userUnlockedReceiver.unregisterReceiverSafely(context)
+        userUnlockedReceiver.unregisterReceiverSafely()
     }
 
     /**
diff --git a/src/com/android/launcher3/util/LooperExecutor.java b/src/com/android/launcher3/util/LooperExecutor.java
deleted file mode 100644
index 3a8a13c..0000000
--- a/src/com/android/launcher3/util/LooperExecutor.java
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- * Copyright (C) 2017 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.util;
-
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.Looper;
-import android.os.Process;
-
-import java.util.List;
-import java.util.concurrent.AbstractExecutorService;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Extension of {@link AbstractExecutorService} which executed on a provided looper.
- */
-public class LooperExecutor extends AbstractExecutorService {
-
-    private final Handler mHandler;
-
-    public LooperExecutor(Looper looper) {
-        mHandler = new Handler(looper);
-    }
-
-    public Handler getHandler() {
-        return mHandler;
-    }
-
-    @Override
-    public void execute(Runnable runnable) {
-        if (getHandler().getLooper() == Looper.myLooper()) {
-            runnable.run();
-        } else {
-            getHandler().post(runnable);
-        }
-    }
-
-    /**
-     * Same as execute, but never runs the action inline.
-     */
-    public void post(Runnable runnable) {
-        getHandler().post(runnable);
-    }
-
-    /**
-     * Not supported and throws an exception when used.
-     */
-    @Override
-    @Deprecated
-    public void shutdown() {
-        throw new UnsupportedOperationException();
-    }
-
-    /**
-     * Not supported and throws an exception when used.
-     */
-    @Override
-    @Deprecated
-    public List<Runnable> shutdownNow() {
-        throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public boolean isShutdown() {
-        return false;
-    }
-
-    @Override
-    public boolean isTerminated() {
-        return false;
-    }
-
-    /**
-     * Not supported and throws an exception when used.
-     */
-    @Override
-    @Deprecated
-    public boolean awaitTermination(long l, TimeUnit timeUnit) {
-        throw new UnsupportedOperationException();
-    }
-
-    /**
-     * Returns the thread for this executor
-     */
-    public Thread getThread() {
-        return getHandler().getLooper().getThread();
-    }
-
-    /**
-     * Returns the looper for this executor
-     */
-    public Looper getLooper() {
-        return getHandler().getLooper();
-    }
-
-    /**
-     * Set the priority of a thread, based on Linux priorities.
-     * @param priority Linux priority level, from -20 for highest scheduling priority
-     *                to 19 for lowest scheduling priority.
-     * @see Process#setThreadPriority(int, int)
-     */
-    public void setThreadPriority(int priority) {
-        Process.setThreadPriority(((HandlerThread) getThread()).getThreadId(), priority);
-    }
-}
diff --git a/src/com/android/launcher3/util/LooperExecutor.kt b/src/com/android/launcher3/util/LooperExecutor.kt
new file mode 100644
index 0000000..6ff528d
--- /dev/null
+++ b/src/com/android/launcher3/util/LooperExecutor.kt
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2017 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.util
+
+import android.os.Handler
+import android.os.HandlerThread
+import android.os.Looper
+import android.os.Process
+import android.os.Process.THREAD_PRIORITY_FOREGROUND
+import androidx.annotation.IntDef
+import java.util.concurrent.AbstractExecutorService
+import java.util.concurrent.TimeUnit
+import kotlin.annotation.AnnotationRetention.SOURCE
+
+/** Extension of [AbstractExecutorService] which executed on a provided looper. */
+class LooperExecutor(looper: Looper, private val defaultPriority: Int) : AbstractExecutorService() {
+    val handler: Handler = Handler(looper)
+
+    @JvmOverloads
+    constructor(
+        name: String,
+        defaultPriority: Int = Process.THREAD_PRIORITY_DEFAULT,
+    ) : this(createAndStartNewLooper(name, defaultPriority), defaultPriority)
+
+    /** Returns the thread for this executor */
+    val thread: Thread
+        get() = handler.looper.thread
+
+    /** Returns the looper for this executor */
+    val looper: Looper
+        get() = handler.looper
+
+    @ElevationCaller private var elevationFlags: Int = 0
+
+    override fun execute(runnable: Runnable) {
+        if (handler.looper == Looper.myLooper()) {
+            runnable.run()
+        } else {
+            handler.post(runnable)
+        }
+    }
+
+    /** Same as execute, but never runs the action inline. */
+    fun post(runnable: Runnable) {
+        handler.post(runnable)
+    }
+
+    @Deprecated("Not supported and throws an exception when used")
+    override fun shutdown() {
+        throw UnsupportedOperationException()
+    }
+
+    @Deprecated("Not supported and throws an exception when used.")
+    override fun shutdownNow(): List<Runnable> {
+        throw UnsupportedOperationException()
+    }
+
+    override fun isShutdown() = false
+
+    override fun isTerminated() = false
+
+    @Deprecated("Not supported and throws an exception when used.")
+    override fun awaitTermination(l: Long, timeUnit: TimeUnit): Boolean {
+        throw UnsupportedOperationException()
+    }
+
+    /**
+     * Increases the priority of the thread for the [caller]. Multiple calls with same caller are
+     * ignored. The priority is reset once wall callers have restored priority
+     */
+    fun elevatePriority(@ElevationCaller caller: Int) {
+        val wasElevated = elevationFlags != 0
+        elevationFlags = elevationFlags.or(caller)
+        if (elevationFlags != 0 && !wasElevated)
+            Process.setThreadPriority(
+                (thread as HandlerThread).threadId,
+                THREAD_PRIORITY_FOREGROUND,
+            )
+    }
+
+    /** Restores to default priority if it was previously elevated */
+    fun restorePriority(@ElevationCaller caller: Int) {
+        val wasElevated = elevationFlags != 0
+        elevationFlags = elevationFlags.and(caller.inv())
+        if (elevationFlags == 0 && wasElevated)
+            Process.setThreadPriority((thread as HandlerThread).threadId, defaultPriority)
+    }
+
+    @Retention(SOURCE)
+    @IntDef(value = [CALLER_LOADER_TASK, CALLER_ICON_CACHE], flag = true)
+    annotation class ElevationCaller
+
+    companion object {
+        /** Utility method to get a started handler thread statically with the provided priority */
+        @JvmOverloads
+        @JvmStatic
+        fun createAndStartNewLooper(
+            name: String,
+            priority: Int = Process.THREAD_PRIORITY_DEFAULT,
+        ): Looper = HandlerThread(name, priority).apply { start() }.looper
+
+        const val CALLER_LOADER_TASK = 1 shl 0
+        const val CALLER_ICON_CACHE = 1 shl 1
+    }
+}
diff --git a/src/com/android/launcher3/util/ScreenOnTracker.java b/src/com/android/launcher3/util/ScreenOnTracker.java
index 50be98b..8ffe9ea 100644
--- a/src/com/android/launcher3/util/ScreenOnTracker.java
+++ b/src/com/android/launcher3/util/ScreenOnTracker.java
@@ -46,34 +46,31 @@
     private final SimpleBroadcastReceiver mReceiver;
     private final CopyOnWriteArrayList<ScreenOnListener> mListeners = new CopyOnWriteArrayList<>();
 
-    private final Context mContext;
     private boolean mIsScreenOn;
 
     @Inject
     ScreenOnTracker(@ApplicationContext Context context, DaggerSingletonTracker tracker) {
         // Assume that the screen is on to begin with
-        mContext = context;
-        mReceiver = new SimpleBroadcastReceiver(UI_HELPER_EXECUTOR, this::onReceive);
+        mReceiver = new SimpleBroadcastReceiver(context, UI_HELPER_EXECUTOR, this::onReceive);
         init(tracker);
     }
 
     @VisibleForTesting
     ScreenOnTracker(@ApplicationContext Context context, SimpleBroadcastReceiver receiver,
             DaggerSingletonTracker tracker) {
-        mContext = context;
         mReceiver = receiver;
         init(tracker);
     }
 
     private void init(DaggerSingletonTracker tracker) {
         mIsScreenOn = true;
-        mReceiver.register(mContext, ACTION_SCREEN_ON, ACTION_SCREEN_OFF, ACTION_USER_PRESENT);
+        mReceiver.register(ACTION_SCREEN_ON, ACTION_SCREEN_OFF, ACTION_USER_PRESENT);
         tracker.addCloseable(this);
     }
 
     @Override
     public void close() {
-        mReceiver.unregisterReceiverSafely(mContext);
+        mReceiver.unregisterReceiverSafely();
     }
 
     @VisibleForTesting
diff --git a/src/com/android/launcher3/util/SimpleBroadcastReceiver.java b/src/com/android/launcher3/util/SimpleBroadcastReceiver.java
index 539a7cb..7a40abe 100644
--- a/src/com/android/launcher3/util/SimpleBroadcastReceiver.java
+++ b/src/com/android/launcher3/util/SimpleBroadcastReceiver.java
@@ -25,22 +25,29 @@
 import android.text.TextUtils;
 
 import androidx.annotation.AnyThread;
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import java.util.function.Consumer;
 
 public class SimpleBroadcastReceiver extends BroadcastReceiver {
+    public static final String TAG = "SimpleBroadcastReceiver";
+    // Keeps a strong reference to the context.
+    private final Context mContext;
 
     private final Consumer<Intent> mIntentConsumer;
 
     // Handler to register/unregister broadcast receiver
     private final Handler mHandler;
 
-    public SimpleBroadcastReceiver(LooperExecutor looperExecutor, Consumer<Intent> intentConsumer) {
-        this(looperExecutor.getHandler(), intentConsumer);
+    public SimpleBroadcastReceiver(@NonNull Context context, LooperExecutor looperExecutor,
+            Consumer<Intent> intentConsumer) {
+        this(context, looperExecutor.getHandler(), intentConsumer);
     }
 
-    public SimpleBroadcastReceiver(Handler handler, Consumer<Intent> intentConsumer) {
+    public SimpleBroadcastReceiver(@NonNull Context context, Handler handler,
+            Consumer<Intent> intentConsumer) {
+        mContext = context;
         mIntentConsumer = intentConsumer;
         mHandler = handler;
     }
@@ -50,18 +57,18 @@
         mIntentConsumer.accept(intent);
     }
 
-    /** Calls {@link #register(Context, Runnable, String...)} with null completionCallback. */
+    /** Calls {@link #register(Runnable, String...)} with null completionCallback. */
     @AnyThread
-    public void register(Context context, String... actions) {
-        register(context, null, actions);
+    public void register(String... actions) {
+        register(null, actions);
     }
 
     /**
-     * Calls {@link #register(Context, Runnable, int, String...)} with null completionCallback.
+     * Calls {@link #register(Runnable, int, String...)} with null completionCallback.
      */
     @AnyThread
-    public void register(Context context, int flags, String... actions) {
-        register(context, null, flags, actions);
+    public void register(int flags, String... actions) {
+        register(null, flags, actions);
     }
 
     /**
@@ -74,19 +81,18 @@
      *                           while registerReceiver() is executed on a binder call.
      */
     @AnyThread
-    public void register(
-            Context context, @Nullable Runnable completionCallback, String... actions) {
+    public void register(@Nullable Runnable completionCallback, String... actions) {
         if (Looper.myLooper() == mHandler.getLooper()) {
-            registerInternal(context, completionCallback, actions);
+            registerInternal(mContext, completionCallback, actions);
         } else {
-            mHandler.post(() -> registerInternal(context, completionCallback, actions));
+            mHandler.post(() -> registerInternal(mContext, completionCallback, actions));
         }
     }
 
     /** Register broadcast receiver and run completion callback if passed. */
     @AnyThread
     private void registerInternal(
-            Context context, @Nullable Runnable completionCallback, String... actions) {
+            @NonNull Context context, @Nullable Runnable completionCallback, String... actions) {
         context.registerReceiver(this, getFilter(actions));
         if (completionCallback != null) {
             completionCallback.run();
@@ -94,37 +100,37 @@
     }
 
     /**
-     * Same as {@link #register(Context, Runnable, String...)} above but with additional flags
-     * params.
+     * Same as {@link #register(Runnable, String...)} above but with additional flags
+     * params utilizine the original {@link Context}.
      */
     @AnyThread
-    public void register(
-            Context context, @Nullable Runnable completionCallback, int flags, String... actions) {
+    public void register(@Nullable Runnable completionCallback, int flags, String... actions) {
         if (Looper.myLooper() == mHandler.getLooper()) {
-            registerInternal(context, completionCallback, flags, actions);
+            registerInternal(mContext, completionCallback, flags, actions);
         } else {
-            mHandler.post(() -> registerInternal(context, completionCallback, flags, actions));
+            mHandler.post(() -> registerInternal(mContext, completionCallback, flags, actions));
         }
     }
 
     /** Register broadcast receiver and run completion callback if passed. */
     @AnyThread
     private void registerInternal(
-            Context context, @Nullable Runnable completionCallback, int flags, String... actions) {
+            @NonNull Context context, @Nullable Runnable completionCallback, int flags,
+            String... actions) {
         context.registerReceiver(this, getFilter(actions), flags);
         if (completionCallback != null) {
             completionCallback.run();
         }
     }
 
-    /** Same as {@link #register(Context, Runnable, String...)} above but with pkg name. */
+    /** Same as {@link #register(Runnable, String...)} above but with pkg name. */
     @AnyThread
-    public void registerPkgActions(Context context, @Nullable String pkg, String... actions) {
+    public void registerPkgActions(@Nullable String pkg, String... actions) {
         if (Looper.myLooper() == mHandler.getLooper()) {
-            context.registerReceiver(this, getPackageFilter(pkg, actions));
+            mContext.registerReceiver(this, getPackageFilter(pkg, actions));
         } else {
             mHandler.post(() -> {
-                context.registerReceiver(this, getPackageFilter(pkg, actions));
+                mContext.registerReceiver(this, getPackageFilter(pkg, actions));
             });
         }
     }
@@ -135,19 +141,19 @@
      * unregister happens on {@link #mHandler}'s looper.
      */
     @AnyThread
-    public void unregisterReceiverSafely(Context context) {
+    public void unregisterReceiverSafely() {
         if (Looper.myLooper() == mHandler.getLooper()) {
-            unregisterReceiverSafelyInternal(context);
+            unregisterReceiverSafelyInternal(mContext);
         } else {
             mHandler.post(() -> {
-                unregisterReceiverSafelyInternal(context);
+                unregisterReceiverSafelyInternal(mContext);
             });
         }
     }
 
     /** Unregister broadcast receiver ignoring any errors. */
     @AnyThread
-    private void unregisterReceiverSafelyInternal(Context context) {
+    private void unregisterReceiverSafelyInternal(@NonNull Context context) {
         try {
             context.unregisterReceiver(this);
         } catch (IllegalArgumentException e) {
diff --git a/src/com/android/launcher3/util/SplitConfigurationOptions.java b/src/com/android/launcher3/util/SplitConfigurationOptions.java
index 44a7c6f..e1ef77a 100644
--- a/src/com/android/launcher3/util/SplitConfigurationOptions.java
+++ b/src/com/android/launcher3/util/SplitConfigurationOptions.java
@@ -127,10 +127,10 @@
         /** This rect represents the actual gap between the two apps */
         public final Rect visualDividerBounds;
         // This class is orientation-agnostic, so we compute both for later use
-        public final float topTaskPercent;
-        public final float leftTaskPercent;
-        public final float dividerWidthPercent;
-        public final float dividerHeightPercent;
+        private final float topTaskPercent;
+        private final float leftTaskPercent;
+        private final float dividerWidthPercent;
+        private final float dividerHeightPercent;
         public final int snapPosition;
 
         /**
@@ -190,6 +190,39 @@
             dividerHeightPercent = visualDividerBounds.height() / totalHeight;
         }
 
+        /**
+         * Returns the percentage size of the left/top task (compared to the full width/height of
+         * the split pair). E.g. if the left task is 4 units wide, the divider is 2 units, and the
+         * right task is 4 units, this method will return 0.4f.
+         */
+        public float getLeftTopTaskPercent() {
+            // topTaskPercent and leftTaskPercent are defined at creation time, and are not updated
+            // on device rotate, so we have to check appsStackedVertically to return the right
+            // creation-time measurements.
+            return appsStackedVertically ? topTaskPercent : leftTaskPercent;
+        }
+
+        /**
+         * Returns the percentage size of the divider's thickness (compared to the full width/height
+         * of the split pair). E.g. if the left task is 4 units wide, the divider is 2 units, and
+         * the right task is 4 units, this method will return 0.2f.
+         */
+        public float getDividerPercent() {
+            // dividerHeightPercent and dividerWidthPercent are defined at creation time, and are
+            // not updated on device rotate, so we have to check appsStackedVertically to return
+            // the right creation-time measurements.
+            return appsStackedVertically ? dividerHeightPercent : dividerWidthPercent;
+        }
+
+        /**
+         * Returns the percentage size of the right/bottom task (compared to the full width/height
+         * of the split pair). E.g. if the left task is 4 units wide, the divider is 2 units, and
+         * the right task is 4 units, this method will return 0.4f.
+         */
+        public float getRightBottomTaskPercent() {
+            return 1 - (getLeftTopTaskPercent() + getDividerPercent());
+        }
+
         @Override
         public String toString() {
             return "LeftTop: " + leftTopBounds + ", taskId: " + leftTopTaskId + "\n"
diff --git a/src/com/android/launcher3/util/WallpaperOffsetInterpolator.java b/src/com/android/launcher3/util/WallpaperOffsetInterpolator.java
index f8cbe0d..26a04a5 100644
--- a/src/com/android/launcher3/util/WallpaperOffsetInterpolator.java
+++ b/src/com/android/launcher3/util/WallpaperOffsetInterpolator.java
@@ -31,8 +31,7 @@
     // Don't use all the wallpaper for parallax until you have at least this many pages
     private static final int MIN_PARALLAX_PAGE_SPAN = 4;
 
-    private final SimpleBroadcastReceiver mWallpaperChangeReceiver =
-            new SimpleBroadcastReceiver(UI_HELPER_EXECUTOR, i -> onWallpaperChanged());
+    private final SimpleBroadcastReceiver mWallpaperChangeReceiver;
     private final Workspace<?> mWorkspace;
     private final boolean mIsRtl;
     private final Handler mHandler;
@@ -46,6 +45,8 @@
 
     public WallpaperOffsetInterpolator(Workspace<?> workspace) {
         mWorkspace = workspace;
+        mWallpaperChangeReceiver = new SimpleBroadcastReceiver(
+                workspace.getContext(), UI_HELPER_EXECUTOR, i -> onWallpaperChanged());
         mIsRtl = Utilities.isRtl(workspace.getResources());
         mHandler = new OffsetHandler(workspace.getContext());
     }
@@ -198,11 +199,10 @@
     public void setWindowToken(IBinder token) {
         mWindowToken = token;
         if (mWindowToken == null && mRegistered) {
-            mWallpaperChangeReceiver.unregisterReceiverSafely(mWorkspace.getContext());
+            mWallpaperChangeReceiver.unregisterReceiverSafely();
             mRegistered = false;
         } else if (mWindowToken != null && !mRegistered) {
-            mWallpaperChangeReceiver.register(
-                    mWorkspace.getContext(), ACTION_WALLPAPER_CHANGED);
+            mWallpaperChangeReceiver.register(ACTION_WALLPAPER_CHANGED);
             onWallpaperChanged();
             mRegistered = true;
         }
diff --git a/src/com/android/launcher3/util/window/WindowManagerProxy.java b/src/com/android/launcher3/util/window/WindowManagerProxy.java
index f511ef2..11f0bc2 100644
--- a/src/com/android/launcher3/util/window/WindowManagerProxy.java
+++ b/src/com/android/launcher3/util/window/WindowManagerProxy.java
@@ -109,7 +109,7 @@
     /**
      * Returns if we are in desktop mode or not.
      */
-    public boolean isInDesktopMode() {
+    public boolean isInDesktopMode(int displayId) {
         return false;
     }
 
@@ -503,11 +503,23 @@
     /** A listener for when the user enters/exits Desktop Mode.  */
     public interface DesktopVisibilityListener {
         /**
-         * Callback for when the user enters or exits Desktop Mode
+         * Called when the desktop mode state on the display whose ID is `displayId` changes.
          *
-         * @param visible whether Desktop Mode is now visible
+         * @param displayId The ID of the display for which this notification is triggering.
+         * @param isInDesktopModeAndNotInOverview True if a desktop is currently active on the given
+         *                                        display, and Overview is currently inactive.
          */
-        void onDesktopVisibilityChanged(boolean visible);
+        default void onIsInDesktopModeChanged(int displayId,
+                boolean isInDesktopModeAndNotInOverview) {
+        }
+
+        /**
+         * Called whenever the conditions that allow the creation of desks change.
+         *
+         * @param canCreateDesks whether it is possible to create new desks.
+         */
+        default void onCanCreateDesksChanged(boolean canCreateDesks) {
+        }
     }
 
 }
diff --git a/src/com/android/launcher3/views/ClipIconView.java b/src/com/android/launcher3/views/ClipIconView.java
index f90a3e4..a295d6b 100644
--- a/src/com/android/launcher3/views/ClipIconView.java
+++ b/src/com/android/launcher3/views/ClipIconView.java
@@ -20,6 +20,7 @@
 import static com.android.launcher3.Utilities.boundToRange;
 import static com.android.launcher3.Utilities.mapToRange;
 import static com.android.launcher3.anim.AnimatorListeners.forEndCallback;
+import static com.android.launcher3.icons.IconNormalizer.ICON_VISIBLE_AREA_FACTOR;
 import static com.android.launcher3.views.FloatingIconView.SHAPE_PROGRESS_DURATION;
 
 import static java.lang.Math.max;
@@ -44,10 +45,11 @@
 import androidx.core.util.Consumer;
 
 import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Flags;
 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.ThemeManager;
 
 /**
  * A view used to draw both layers of an {@link AdaptiveIconDrawable}.
@@ -67,6 +69,7 @@
     private boolean mIsAdaptiveIcon = false;
 
     private ValueAnimator mRevealAnimator;
+    private float mIconScale;
 
     private final Rect mStartRevealRect = new Rect();
     private final Rect mEndRevealRect = new Rect();
@@ -172,9 +175,12 @@
 
         mTaskCornerRadius = cornerRadius / scale;
         if (mIsAdaptiveIcon) {
-            if (!isOpening && progress >= shapeProgressStart) {
+            final ThemeManager themeManager = ThemeManager.INSTANCE.get(getContext());
+            mIconScale = themeManager.getIconState().getIconScale();
+            if ((!isOpening || Flags.enableLauncherIconShapes())
+                    && progress >= shapeProgressStart) {
                 if (mRevealAnimator == null) {
-                    mRevealAnimator = IconShape.INSTANCE.get(getContext()).getShape()
+                    mRevealAnimator = themeManager.getIconShape()
                             .createRevealAnimator(this, mStartRevealRect,
                                     mOutline, mTaskCornerRadius, !isOpening);
                     mRevealAnimator.addListener(forEndCallback(() -> mRevealAnimator = null));
@@ -258,8 +264,7 @@
             mStartRevealRect.set(0, 0, originalWidth, originalHeight);
 
             if (!isFolderIcon) {
-                Utilities.scaleRectAboutCenter(mStartRevealRect,
-                        IconShape.INSTANCE.get(getContext()).getNormalizationScale());
+                Utilities.scaleRectAboutCenter(mStartRevealRect, ICON_VISIBLE_AREA_FACTOR);
             }
 
             if (dp.isLandscape) {
@@ -309,17 +314,24 @@
 
     @Override
     public void draw(Canvas canvas) {
-        int count = canvas.save();
+        int count1 = canvas.save();
         if (mClipPath != null) {
             canvas.clipPath(mClipPath);
         }
-        super.draw(canvas);
+        int count2 = canvas.save();
+        float iconCenterX =
+                (mFinalDrawableBounds.right - mFinalDrawableBounds.left) / 2f * mIconScale;
+        float iconCenterY =
+                (mFinalDrawableBounds.bottom - mFinalDrawableBounds.top) / 2f * mIconScale;
+        canvas.scale(mIconScale, mIconScale, iconCenterX, iconCenterY);
         if (mBackground != null) {
             mBackground.draw(canvas);
         }
         if (mForeground != null) {
             mForeground.draw(canvas);
         }
+        canvas.restoreToCount(count2);
+        super.draw(canvas);
         if (mTaskViewArtist != null) {
             canvas.saveLayerAlpha(
                     0,
@@ -333,7 +345,7 @@
             canvas.scale(drawScale, drawScale);
             mTaskViewArtist.taskViewDrawCallback.accept(canvas);
         }
-        canvas.restoreToCount(count);
+        canvas.restoreToCount(count1);
     }
 
     void recycle() {
diff --git a/src/com/android/launcher3/views/DoubleShadowBubbleTextView.java b/src/com/android/launcher3/views/DoubleShadowBubbleTextView.java
index 392d9a7..05bc4d8 100644
--- a/src/com/android/launcher3/views/DoubleShadowBubbleTextView.java
+++ b/src/com/android/launcher3/views/DoubleShadowBubbleTextView.java
@@ -25,6 +25,7 @@
 import android.os.Build;
 import android.text.Spannable;
 import android.text.SpannableString;
+import android.text.TextUtils;
 import android.text.style.ImageSpan;
 import android.util.AttributeSet;
 import android.util.Log;
@@ -102,7 +103,7 @@
 
     @Override
     public void onDraw(Canvas canvas) {
-        if (shouldDrawAppContrastTile()) {
+        if (shouldDrawAppContrastTile() && !TextUtils.isEmpty(getText())) {
             drawAppContrastTile(canvas);
         }
         // If text is transparent or shadow alpha is 0, don't draw any shadow
diff --git a/src/com/android/launcher3/views/FloatingIconView.java b/src/com/android/launcher3/views/FloatingIconView.java
index 22857b1..5b3abc3 100644
--- a/src/com/android/launcher3/views/FloatingIconView.java
+++ b/src/com/android/launcher3/views/FloatingIconView.java
@@ -55,7 +55,7 @@
 import com.android.launcher3.folder.FolderIcon;
 import com.android.launcher3.graphics.PreloadIconDrawable;
 import com.android.launcher3.icons.FastBitmapDrawable;
-import com.android.launcher3.icons.LauncherIcons;
+import com.android.launcher3.icons.IconNormalizer;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.ItemInfoWithIcon;
 import com.android.launcher3.popup.SystemShortcut;
@@ -463,10 +463,7 @@
         Rect bounds = new Rect(0, 0, (int) position.width() + blurSizeOutline,
                 (int) position.height() + blurSizeOutline);
         bounds.inset(blurSizeOutline / 2, blurSizeOutline / 2);
-
-        try (LauncherIcons li = LauncherIcons.obtain(l)) {
-            Utilities.scaleRectAboutCenter(bounds, li.getNormalizer().getScale(drawable));
-        }
+        Utilities.scaleRectAboutCenter(bounds, IconNormalizer.ICON_VISIBLE_AREA_FACTOR);
 
         bounds.inset(
                 (int) (-bounds.width() * AdaptiveIconDrawable.getExtraInsetFraction()),
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/src/com/android/launcher3/widget/picker/WidgetRecommendationCategoryProvider.java b/src/com/android/launcher3/widget/picker/WidgetRecommendationCategoryProvider.java
index f8dc6b0..8f34fe3 100644
--- a/src/com/android/launcher3/widget/picker/WidgetRecommendationCategoryProvider.java
+++ b/src/com/android/launcher3/widget/picker/WidgetRecommendationCategoryProvider.java
@@ -35,19 +35,7 @@
  * own implementation. Method {@code getWidgetRecommendationCategory} is called per widget to get
  * the category.</p>
  */
-public class WidgetRecommendationCategoryProvider implements ResourceBasedOverride {
-    private static final String TAG = "WidgetRecommendationCategoryProvider";
-
-    /**
-     * Retrieve instance of this object that can be overridden in runtime based on the build
-     * variant of the application.
-     */
-    public static WidgetRecommendationCategoryProvider newInstance(Context context) {
-        Preconditions.assertWorkerThread();
-        return Overrides.getObject(
-                WidgetRecommendationCategoryProvider.class, context.getApplicationContext(),
-                R.string.widget_recommendation_category_provider_class);
-    }
+public class WidgetRecommendationCategoryProvider {
 
     /**
      * Returns a {@link WidgetRecommendationCategory} for the provided widget item that can be used
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 9c64ec9..3658989 100644
--- a/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
@@ -28,11 +28,13 @@
 import android.view.Surface
 import androidx.test.core.app.ApplicationProvider.getApplicationContext
 import androidx.test.platform.app.InstrumentationRegistry
+import com.android.launcher3.LauncherPrefs.Companion.GRID_NAME
 import com.android.launcher3.dagger.LauncherAppComponent
 import com.android.launcher3.dagger.LauncherAppSingleton
 import com.android.launcher3.testing.shared.ResourceUtils
 import com.android.launcher3.util.AllModulesMinusWMProxy
 import com.android.launcher3.util.DisplayController
+import com.android.launcher3.util.FakePrefsModule
 import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext
 import com.android.launcher3.util.NavigationMode
 import com.android.launcher3.util.WindowBounds
@@ -70,7 +72,7 @@
     protected open val runningContext: Context = getApplicationContext()
     private val displayController: DisplayController = mock()
     private val windowManagerProxy: WindowManagerProxy = mock()
-    private val launcherPrefs: LauncherPrefs = mock()
+    private lateinit var launcherPrefs: LauncherPrefs
 
     @get:Rule val setFlagsRule = SetFlagsRule(SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT)
 
@@ -132,6 +134,7 @@
         isGestureMode: Boolean = true,
         isVerticalBar: Boolean = false,
         isFixedLandscape: Boolean = false,
+        gridName: String? = GRID_NAME.defaultValue,
     ) {
         val (naturalX, naturalY) = deviceSpec.naturalSize
         val windowsBounds = phoneWindowsBounds(deviceSpec, isGestureMode, naturalX, naturalY)
@@ -145,6 +148,7 @@
             isGestureMode,
             densityDpi = deviceSpec.densityDpi,
             isFixedLandscape = isFixedLandscape,
+            gridName = gridName,
         )
     }
 
@@ -152,6 +156,7 @@
         deviceSpec: DeviceSpec,
         isLandscape: Boolean = false,
         isGestureMode: Boolean = true,
+        gridName: String? = GRID_NAME.defaultValue,
     ) {
         val (naturalX, naturalY) = deviceSpec.naturalSize
         val windowsBounds = tabletWindowsBounds(deviceSpec, naturalX, naturalY)
@@ -164,6 +169,7 @@
             rotation = if (isLandscape) Surface.ROTATION_0 else Surface.ROTATION_90,
             isGestureMode,
             densityDpi = deviceSpec.densityDpi,
+            gridName = gridName,
         )
     }
 
@@ -173,6 +179,7 @@
         isLandscape: Boolean = false,
         isGestureMode: Boolean = true,
         isFolded: Boolean = false,
+        gridName: String? = GRID_NAME.defaultValue,
     ) {
         val (unfoldedNaturalX, unfoldedNaturalY) = deviceSpecUnfolded.naturalSize
         val unfoldedWindowsBounds =
@@ -199,6 +206,7 @@
                 rotation = if (isLandscape) Surface.ROTATION_90 else Surface.ROTATION_0,
                 isGestureMode = isGestureMode,
                 densityDpi = deviceSpecFolded.densityDpi,
+                gridName = gridName,
             )
         } else {
             initializeCommonVars(
@@ -207,6 +215,7 @@
                 rotation = if (isLandscape) Surface.ROTATION_0 else Surface.ROTATION_90,
                 isGestureMode = isGestureMode,
                 densityDpi = deviceSpecUnfolded.densityDpi,
+                gridName = gridName,
             )
         }
     }
@@ -282,8 +291,11 @@
         isGestureMode: Boolean = true,
         densityDpi: Int,
         isFixedLandscape: Boolean = false,
+        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)
@@ -311,18 +323,23 @@
         context.initDaggerComponent(
             DaggerAbsDPTestSandboxComponent.builder()
                 .bindWMProxy(windowManagerProxy)
-                .bindLauncherPrefs(launcherPrefs)
                 .bindDisplayController(displayController)
         )
+        launcherPrefs = context.appComponent.launcherPrefs
+        launcherPrefs.put(
+            LauncherPrefs.TASKBAR_PINNING.to(false),
+            LauncherPrefs.TASKBAR_PINNING_IN_DESKTOP_MODE.to(true),
+            LauncherPrefs.FIXED_LANDSCAPE_MODE.to(isFixedLandscape),
+            LauncherPrefs.HOTSEAT_COUNT.to(-1),
+            LauncherPrefs.DEVICE_TYPE.to(-1),
+            LauncherPrefs.WORKSPACE_SIZE.to(""),
+            LauncherPrefs.DB_FILE.to(""),
+            LauncherPrefs.ENABLE_TWOLINE_ALLAPPS_TOGGLE.to(true),
+        )
+        if (gridName != null) {
+            launcherPrefs.put(GRID_NAME, gridName)
+        }
 
-        whenever(launcherPrefs.get(LauncherPrefs.TASKBAR_PINNING)).thenReturn(false)
-        whenever(launcherPrefs.get(LauncherPrefs.TASKBAR_PINNING_IN_DESKTOP_MODE)).thenReturn(true)
-        whenever(launcherPrefs.get(LauncherPrefs.FIXED_LANDSCAPE_MODE)).thenReturn(isFixedLandscape)
-        whenever(launcherPrefs.get(LauncherPrefs.HOTSEAT_COUNT)).thenReturn(-1)
-        whenever(launcherPrefs.get(LauncherPrefs.DEVICE_TYPE)).thenReturn(-1)
-        whenever(launcherPrefs.get(LauncherPrefs.WORKSPACE_SIZE)).thenReturn("")
-        whenever(launcherPrefs.get(LauncherPrefs.DB_FILE)).thenReturn("")
-        whenever(launcherPrefs.get(LauncherPrefs.ENABLE_TWOLINE_ALLAPPS_TOGGLE)).thenReturn(true)
         val info = spy(DisplayController.Info(context, windowManagerProxy, perDisplayBoundsCache))
         whenever(displayController.info).thenReturn(info)
         whenever(info.isTransientTaskbar).thenReturn(isGestureMode)
@@ -347,7 +364,13 @@
         context.assets.open("dumpTests/$fileName").bufferedReader().use(BufferedReader::readText)
 
     private fun writeToDevice(context: Context, fileName: String, content: String) {
-        File(context.getDir("dumpTests", Context.MODE_PRIVATE), fileName).writeText(content)
+        val dir =
+            File(context.filesDir, "dumpTests").also {
+                if (!it.exists()) {
+                    it.mkdirs()
+                }
+            }
+        File(dir, fileName).writeText(content)
     }
 
     protected fun Float.dpToPx(): Float {
@@ -365,15 +388,13 @@
 }
 
 @LauncherAppSingleton
-@Component(modules = [AllModulesMinusWMProxy::class])
+@Component(modules = [AllModulesMinusWMProxy::class, FakePrefsModule::class])
 interface AbsDPTestSandboxComponent : LauncherAppComponent {
 
     @Component.Builder
     interface Builder : LauncherAppComponent.Builder {
         @BindsInstance fun bindWMProxy(proxy: WindowManagerProxy): Builder
 
-        @BindsInstance fun bindLauncherPrefs(prefs: LauncherPrefs): Builder
-
         @BindsInstance fun bindDisplayController(displayController: DisplayController): Builder
 
         override fun build(): AbsDPTestSandboxComponent
diff --git a/tests/multivalentTests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt b/tests/multivalentTests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt
index bfbdb18..f855c51 100644
--- a/tests/multivalentTests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt
@@ -15,7 +15,6 @@
  */
 package com.android.launcher3
 
-import android.content.Context
 import android.graphics.PointF
 import android.graphics.Rect
 import android.platform.test.rule.AllowedDevices
@@ -23,10 +22,11 @@
 import android.platform.test.rule.IgnoreLimit
 import android.platform.test.rule.LimitDevicesRule
 import android.util.SparseArray
-import androidx.test.core.app.ApplicationProvider
 import com.android.launcher3.DeviceProfile.DEFAULT_DIMENSION_PROVIDER
 import com.android.launcher3.DeviceProfile.DEFAULT_PROVIDER
+import com.android.launcher3.testing.shared.ResourceUtils.INVALID_RESOURCE_HANDLE
 import com.android.launcher3.util.DisplayController.Info
+import com.android.launcher3.util.SandboxApplication
 import com.android.launcher3.util.WindowBounds
 import java.io.PrintWriter
 import java.io.StringWriter
@@ -46,7 +46,8 @@
 @IgnoreLimit(ignoreLimit = BuildConfig.IS_STUDIO_BUILD)
 abstract class FakeInvariantDeviceProfileTest {
 
-    protected lateinit var context: Context
+    @get:Rule val context = SandboxApplication()
+
     protected lateinit var inv: InvariantDeviceProfile
     protected val info = mock<Info>()
     protected lateinit var windowBounds: WindowBounds
@@ -59,7 +60,6 @@
 
     @Before
     open fun setUp() {
-        context = ApplicationProvider.getApplicationContext()
         // make sure to reset values
         useTwoPanels = false
         isGestureMode = true
@@ -70,6 +70,8 @@
             context,
             inv,
             info,
+            context.appComponent.wmProxy,
+            context.appComponent.themeManager,
             windowBounds,
             SparseArray(),
             /*isMultiWindowMode=*/ false,
@@ -107,7 +109,7 @@
         transposeLayoutWithOrientation = true
 
         inv =
-            InvariantDeviceProfile().apply {
+            context.appComponent.idp.apply {
                 numRows = 5
                 numColumns = 4
                 numSearchContainerColumns = 4
@@ -169,6 +171,14 @@
                 inlineQsb = BooleanArray(4) { false }
 
                 devicePaddingId = R.xml.paddings_handhelds
+
+                isFixedLandscape = false
+                workspaceSpecsId = INVALID_RESOURCE_HANDLE
+                allAppsSpecsId = INVALID_RESOURCE_HANDLE
+                folderSpecsId = INVALID_RESOURCE_HANDLE
+                hotseatSpecsId = INVALID_RESOURCE_HANDLE
+                workspaceCellSpecsId = INVALID_RESOURCE_HANDLE
+                allAppsCellSpecsId = INVALID_RESOURCE_HANDLE
             }
     }
 
@@ -189,7 +199,7 @@
         useTwoPanels = false
 
         inv =
-            InvariantDeviceProfile().apply {
+            context.appComponent.idp.apply {
                 numRows = 5
                 numColumns = 6
                 numSearchContainerColumns = 3
@@ -252,6 +262,14 @@
                 inlineQsb = booleanArrayOf(false, true, false, false)
 
                 devicePaddingId = R.xml.paddings_handhelds
+
+                isFixedLandscape = false
+                workspaceSpecsId = INVALID_RESOURCE_HANDLE
+                allAppsSpecsId = INVALID_RESOURCE_HANDLE
+                folderSpecsId = INVALID_RESOURCE_HANDLE
+                hotseatSpecsId = INVALID_RESOURCE_HANDLE
+                workspaceCellSpecsId = INVALID_RESOURCE_HANDLE
+                allAppsCellSpecsId = INVALID_RESOURCE_HANDLE
             }
     }
 
@@ -274,7 +292,7 @@
         useTwoPanels = true
 
         inv =
-            InvariantDeviceProfile().apply {
+            context.appComponent.idp.apply {
                 numRows = rows
                 numColumns = cols
                 numSearchContainerColumns = cols
@@ -332,6 +350,14 @@
                 inlineQsb = booleanArrayOf(false, false, false, false)
 
                 devicePaddingId = R.xml.paddings_handhelds
+
+                isFixedLandscape = false
+                workspaceSpecsId = INVALID_RESOURCE_HANDLE
+                allAppsSpecsId = INVALID_RESOURCE_HANDLE
+                folderSpecsId = INVALID_RESOURCE_HANDLE
+                hotseatSpecsId = INVALID_RESOURCE_HANDLE
+                workspaceCellSpecsId = INVALID_RESOURCE_HANDLE
+                allAppsCellSpecsId = INVALID_RESOURCE_HANDLE
             }
     }
 
diff --git a/tests/multivalentTests/src/com/android/launcher3/FakeLauncherPrefs.kt b/tests/multivalentTests/src/com/android/launcher3/FakeLauncherPrefs.kt
index 5e1e548..4d01d4d 100644
--- a/tests/multivalentTests/src/com/android/launcher3/FakeLauncherPrefs.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/FakeLauncherPrefs.kt
@@ -21,19 +21,24 @@
 import android.content.SharedPreferences
 import com.android.launcher3.dagger.ApplicationContext
 import com.android.launcher3.dagger.LauncherAppSingleton
-import java.io.File
+import com.android.launcher3.util.DaggerSingletonTracker
+import java.util.UUID
 import javax.inject.Inject
 
 /** Emulates Launcher preferences for a test environment. */
 @LauncherAppSingleton
-class FakeLauncherPrefs @Inject constructor(@ApplicationContext context: Context) :
+class FakeLauncherPrefs
+@Inject
+constructor(@ApplicationContext context: Context, lifeCycle: DaggerSingletonTracker) :
     LauncherPrefs(context) {
 
-    private val backingPrefs =
-        context.getSharedPreferences(
-            File.createTempFile("fake-pref", ".xml", context.filesDir),
-            MODE_PRIVATE,
-        )
+    private val prefName = "fake-pref-" + UUID.randomUUID().toString()
+
+    private val backingPrefs = context.getSharedPreferences(prefName, MODE_PRIVATE)
+
+    init {
+        lifeCycle.addCloseable { context.deleteSharedPreferences(prefName) }
+    }
 
     override fun getSharedPrefs(item: Item): SharedPreferences = backingPrefs
 }
diff --git a/tests/multivalentTests/src/com/android/launcher3/FakeLauncherPrefsTest.kt b/tests/multivalentTests/src/com/android/launcher3/FakeLauncherPrefsTest.kt
index c57c86f..0941c79 100644
--- a/tests/multivalentTests/src/com/android/launcher3/FakeLauncherPrefsTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/FakeLauncherPrefsTest.kt
@@ -18,10 +18,14 @@
 
 import androidx.test.core.app.ApplicationProvider.getApplicationContext
 import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.android.launcher3.util.DaggerSingletonTracker
 import com.android.launcher3.util.LauncherMultivalentJUnit
 import com.google.common.truth.Truth.assertThat
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
 
 private val TEST_CONSTANT_ITEM = LauncherPrefs.nonRestorableItem("TEST_BOOLEAN_ITEM", false)
 
@@ -36,7 +40,15 @@
 
 @RunWith(LauncherMultivalentJUnit::class)
 class FakeLauncherPrefsTest {
-    private val launcherPrefs = FakeLauncherPrefs(getApplicationContext())
+
+    @Mock lateinit var lifeCycle: DaggerSingletonTracker
+    private lateinit var launcherPrefs: FakeLauncherPrefs
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        launcherPrefs = FakeLauncherPrefs(getApplicationContext(), lifeCycle)
+    }
 
     @Test
     fun testGet_constantItemNotInPrefs_returnsDefaultValue() {
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/icons/IconCacheTest.java b/tests/multivalentTests/src/com/android/launcher3/icons/IconCacheTest.java
index 9026748..0aaf4d7 100644
--- a/tests/multivalentTests/src/com/android/launcher3/icons/IconCacheTest.java
+++ b/tests/multivalentTests/src/com/android/launcher3/icons/IconCacheTest.java
@@ -56,7 +56,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
-import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.icons.cache.CachingLogic;
 import com.android.launcher3.icons.cache.IconCacheUpdateHandler;
 import com.android.launcher3.icons.cache.LauncherActivityCachingLogic;
@@ -69,11 +69,13 @@
 import com.android.launcher3.util.ApplicationInfoWrapper;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.PackageUserKey;
+import com.android.launcher3.util.SandboxApplication;
 
 import com.google.common.truth.Truth;
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -85,20 +87,18 @@
 @RunWith(AndroidJUnit4.class)
 public class IconCacheTest {
 
-    private Context mContext;
+    @Rule public SandboxApplication mContext = new SandboxApplication();
+
     private IconCache mIconCache;
 
     private ComponentName mMyComponent;
 
     @Before
     public void setup() {
-        mContext = getInstrumentation().getTargetContext();
         mMyComponent = new ComponentName(mContext, SettingsActivity.class);
 
         // In memory icon cache
-        mIconCache = new IconCache(mContext,
-                InvariantDeviceProfile.INSTANCE.get(mContext), null,
-                new LauncherIconProvider(mContext));
+        mIconCache = LauncherAppState.getInstance(mContext).getIconCache();
     }
 
     @After
diff --git a/tests/multivalentTests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.kt b/tests/multivalentTests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.kt
index ce04682..08b8f81 100644
--- a/tests/multivalentTests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.kt
@@ -155,7 +155,7 @@
     private fun verifyItemSpaceFinderCall(nonEmptyScreenIds: List<Int>, numberOfExpectedCall: Int) {
         verify(mWorkspaceItemSpaceFinder, times(numberOfExpectedCall))
             .findSpaceForItem(
-                same(mAppState),
+                eq(mAppState),
                 same(mModelHelper.bgDataModel),
                 eq(IntArray.wrap(*nonEmptyScreenIds.toIntArray())),
                 eq(IntArray()),
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/LoaderCursorTest.java b/tests/multivalentTests/src/com/android/launcher3/model/LoaderCursorTest.java
index 11047fb..ad40818 100644
--- a/tests/multivalentTests/src/com/android/launcher3/model/LoaderCursorTest.java
+++ b/tests/multivalentTests/src/com/android/launcher3/model/LoaderCursorTest.java
@@ -70,6 +70,7 @@
 import com.android.launcher3.Flags;
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.icons.BitmapInfo;
 import com.android.launcher3.icons.LauncherIcons;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
@@ -258,7 +259,6 @@
         initCursor(ITEM_TYPE_APPLICATION, "title");
         assertTrue(mLoaderCursor.moveToNext());
         WorkspaceItemInfo itemInfo = new WorkspaceItemInfo();
-        itemInfo.bitmap = null;
         itemInfo.runtimeStatusFlags |= FLAG_ARCHIVED;
         Bitmap expectedBitmap = LauncherIcons.obtain(mContext)
                 .createIconBitmap(decodeByteArray(sTestBlob, 0, sTestBlob.length))
@@ -289,7 +289,7 @@
         initCursor(ITEM_TYPE_APPLICATION, "title");
         assertTrue(mLoaderCursor.moveToNext());
         WorkspaceItemInfo itemInfo = new WorkspaceItemInfo();
-        itemInfo.bitmap = null;
+        BitmapInfo original = itemInfo.bitmap;
         itemInfo.runtimeStatusFlags |= FLAG_ARCHIVED;
         Intent intent = new Intent();
         intent.setComponent(new ComponentName("package", "class"));
@@ -297,7 +297,7 @@
         // When
         mLoaderCursor.loadWorkspaceTitleAndIcon(false, false, itemInfo);
         // Then
-        assertThat(itemInfo.bitmap).isNull();
+        assertThat(itemInfo.bitmap).isEqualTo(original);
     }
 
     @Test
diff --git a/tests/multivalentTests/src/com/android/launcher3/model/WidgetsModelTest.kt b/tests/multivalentTests/src/com/android/launcher3/model/WidgetsModelTest.kt
index ae4ff04..d704195 100644
--- a/tests/multivalentTests/src/com/android/launcher3/model/WidgetsModelTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/model/WidgetsModelTest.kt
@@ -119,6 +119,11 @@
                     // A widget in different package (none of that app's widgets are in widget
                     // sections xml)
                     createAppWidgetProviderInfo(AppBTestWidgetComponent),
+                    // A widget in different app that is meant to be hidden from picker
+                    createAppWidgetProviderInfo(
+                        AppCPinOnlyTestWidgetComponent,
+                        /*hideFromPicker=*/ true,
+                    ),
                 )
             )
 
@@ -129,12 +134,13 @@
     }
 
     @Test
-    fun widgetsByPackage_treatsWidgetSectionsAsSeparatePackageItems() {
+    fun widgetsByPackageForPicker_treatsWidgetSectionsAsSeparatePackageItems() {
         loadWidgets()
 
-        val packages: Map<PackageItemInfo, List<WidgetItem>> = underTest.widgetsByPackageItem
+        val packages: Map<PackageItemInfo, List<WidgetItem>> =
+            underTest.widgetsByPackageItemForPicker
 
-        // expect 3 package items
+        // expect 3 package items (no app C as its widget is hidden from picker)
         // one for the custom section with widget from appA
         // one for package section for second widget from appA (that wasn't listed in xml)
         // and one for package section for appB
@@ -167,6 +173,13 @@
         assertThat(appBPackageSection).hasSize(1)
         val widgetsInAppBSection = appBPackageSection.entries.first().value
         assertThat(widgetsInAppBSection).hasSize(1)
+
+        // No App C's package section - as the only widget hosted by it is hidden in picker
+        val appCPackageSection =
+            packageSections.filter {
+                it.key.packageName == AppCPinOnlyTestWidgetComponent.packageName
+            }
+        assertThat(appCPackageSection).isEmpty()
     }
 
     @Test
@@ -175,7 +188,29 @@
 
         val widgetsByComponentKey: Map<ComponentKey, WidgetItem> = underTest.widgetsByComponentKey
 
+        // Has all widgets including ones not visible in picker
+        assertThat(widgetsByComponentKey).hasSize(4)
+        widgetsByComponentKey.forEach { entry ->
+            assertThat(entry.key).isEqualTo(entry.value as ComponentKey)
+        }
+    }
+
+    @Test
+    fun widgetComponentMapForPicker_excludesWidgetsHiddenInPicker() {
+        loadWidgets()
+
+        val widgetsByComponentKey: Map<ComponentKey, WidgetItem> =
+            underTest.widgetsByComponentKeyForPicker
+
+        // Has all widgets excluding the appC's widget.
         assertThat(widgetsByComponentKey).hasSize(3)
+        assertThat(
+                widgetsByComponentKey.filter {
+                    it.key.componentName == AppCPinOnlyTestWidgetComponent
+                }
+            )
+            .isEmpty()
+        // widgets mapped correctly
         widgetsByComponentKey.forEach { entry ->
             assertThat(entry.key).isEqualTo(entry.value as ComponentKey)
         }
@@ -189,7 +224,7 @@
     }
 
     @Test
-    fun getWidgetsByPackageItem_returnsACopyOfMap() {
+    fun getWidgetsByPackageItemForPicker_returnsACopyOfMap() {
         loadWidgets()
 
         val latch = CountDownLatch(1)
@@ -198,8 +233,8 @@
 
             // each "widgetsByPackageItem" read returns a different copy of the map held internally.
             // Modifying one shouldn't impact another.
-            for ((_, _) in underTest.widgetsByPackageItem.entries) {
-                underTest.widgetsByPackageItem.clear()
+            for ((_, _) in underTest.widgetsByPackageItemForPicker.entries) {
+                underTest.widgetsByPackageItemForPicker.clear()
                 if (update) { // trigger update
                     update = false
                     // Similarly, model could update its code independently while a client is
@@ -256,6 +291,9 @@
         private val AppBTestWidgetComponent: ComponentName =
             ComponentName.createRelative("com.test.package", "TestProvider")
 
+        private val AppCPinOnlyTestWidgetComponent: ComponentName =
+            ComponentName.createRelative("com.testC.package", "PinOnlyTestProvider")
+
         private const val LOAD_WIDGETS_TIMEOUT_SECONDS = 2L
     }
 }
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..66b8be0 100644
--- a/tests/multivalentTests/src/com/android/launcher3/shapes/ShapesProviderTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/shapes/ShapesProviderTest.kt
@@ -22,7 +22,12 @@
 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.launcher3.shapes.ShapesProvider.ARCH_KEY
+import com.android.launcher3.shapes.ShapesProvider.CIRCLE_KEY
+import com.android.launcher3.shapes.ShapesProvider.FOUR_SIDED_COOKIE_KEY
+import com.android.launcher3.shapes.ShapesProvider.SEVEN_SIDED_COOKIE_KEY
+import com.android.launcher3.shapes.ShapesProvider.SQUARE_KEY
 import com.android.systemui.shared.Flags.FLAG_NEW_CUSTOMIZATION_PICKER_UI
 import org.junit.Rule
 import org.junit.Test
@@ -37,90 +42,99 @@
     @Test
     @EnableFlags(FLAG_ENABLE_LAUNCHER_ICON_SHAPES, FLAG_NEW_CUSTOMIZATION_PICKER_UI)
     fun `verify valid path arch`() {
-        ShapesProvider.iconShapes["arch"]?.apply {
-            GenericPathShape(pathString)
-            PathParser.createPathFromPathData(pathString)
-        }
+        ShapesProvider.iconShapes
+            .find { it.key == ARCH_KEY }!!
+            .run {
+                GenericPathShape(pathString)
+                PathParser.createPathFromPathData(pathString)
+            }
     }
 
     @Test
     @EnableFlags(FLAG_ENABLE_LAUNCHER_ICON_SHAPES, FLAG_NEW_CUSTOMIZATION_PICKER_UI)
     fun `verify valid path 4_sided_cookie`() {
-        ShapesProvider.iconShapes["4_sided_cookie"]?.apply {
-            GenericPathShape(pathString)
-            PathParser.createPathFromPathData(pathString)
-        }
+        ShapesProvider.iconShapes
+            .find { it.key == FOUR_SIDED_COOKIE_KEY }!!
+            .run {
+                GenericPathShape(pathString)
+                PathParser.createPathFromPathData(pathString)
+            }
     }
 
     @Test
     @EnableFlags(FLAG_ENABLE_LAUNCHER_ICON_SHAPES, FLAG_NEW_CUSTOMIZATION_PICKER_UI)
     fun `verify valid path seven_sided_cookie`() {
-        ShapesProvider.iconShapes["seven_sided_cookie"]?.apply {
-            GenericPathShape(pathString)
-            PathParser.createPathFromPathData(pathString)
-        }
-    }
-
-    @Test
-    @EnableFlags(FLAG_ENABLE_LAUNCHER_ICON_SHAPES, FLAG_NEW_CUSTOMIZATION_PICKER_UI)
-    fun `verify valid path sunny`() {
-        ShapesProvider.iconShapes["sunny"]?.apply {
-            GenericPathShape(pathString)
-            PathParser.createPathFromPathData(pathString)
-        }
+        ShapesProvider.iconShapes
+            .find { it.key == SEVEN_SIDED_COOKIE_KEY }!!
+            .run {
+                GenericPathShape(pathString)
+                PathParser.createPathFromPathData(pathString)
+            }
     }
 
     @Test
     @EnableFlags(FLAG_ENABLE_LAUNCHER_ICON_SHAPES, FLAG_NEW_CUSTOMIZATION_PICKER_UI)
     fun `verify valid path circle`() {
-        ShapesProvider.iconShapes["circle"]?.apply {
-            GenericPathShape(pathString)
-            PathParser.createPathFromPathData(pathString)
-        }
+        ShapesProvider.iconShapes
+            .find { it.key == CIRCLE_KEY }!!
+            .run {
+                GenericPathShape(pathString)
+                PathParser.createPathFromPathData(pathString)
+            }
     }
 
     @Test
     @EnableFlags(FLAG_ENABLE_LAUNCHER_ICON_SHAPES, FLAG_NEW_CUSTOMIZATION_PICKER_UI)
     fun `verify valid path square`() {
-        ShapesProvider.iconShapes["square"]?.apply {
-            GenericPathShape(pathString)
-            PathParser.createPathFromPathData(pathString)
-        }
+        ShapesProvider.iconShapes
+            .find { it.key == ARCH_KEY }!!
+            .run {
+                GenericPathShape(pathString)
+                PathParser.createPathFromPathData(pathString)
+            }
     }
 
     @Test
     @EnableFlags(FLAG_ENABLE_LAUNCHER_ICON_SHAPES, FLAG_NEW_CUSTOMIZATION_PICKER_UI)
     fun `verify valid folder path clover`() {
-        ShapesProvider.folderShapes["clover"]?.let { pathString ->
-            GenericPathShape(pathString)
-            PathParser.createPathFromPathData(pathString)
-        }
+        ShapesProvider.iconShapes
+            .find { it.key == CIRCLE_KEY }!!
+            .run {
+                GenericPathShape(folderPathString)
+                PathParser.createPathFromPathData(folderPathString)
+            }
     }
 
     @Test
     @EnableFlags(FLAG_ENABLE_LAUNCHER_ICON_SHAPES, FLAG_NEW_CUSTOMIZATION_PICKER_UI)
     fun `verify valid folder path complexClover`() {
-        ShapesProvider.folderShapes["complexClover"]?.let { pathString ->
-            GenericPathShape(pathString)
-            PathParser.createPathFromPathData(pathString)
-        }
+        ShapesProvider.iconShapes
+            .find { it.key == FOUR_SIDED_COOKIE_KEY }!!
+            .run {
+                GenericPathShape(folderPathString)
+                PathParser.createPathFromPathData(folderPathString)
+            }
     }
 
     @Test
     @EnableFlags(FLAG_ENABLE_LAUNCHER_ICON_SHAPES, FLAG_NEW_CUSTOMIZATION_PICKER_UI)
     fun `verify valid folder path arch`() {
-        ShapesProvider.folderShapes["arch"]?.let { pathString ->
-            GenericPathShape(pathString)
-            PathParser.createPathFromPathData(pathString)
-        }
+        ShapesProvider.iconShapes
+            .find { it.key == ARCH_KEY }!!
+            .run {
+                GenericPathShape(folderPathString)
+                PathParser.createPathFromPathData(folderPathString)
+            }
     }
 
     @Test
     @EnableFlags(FLAG_ENABLE_LAUNCHER_ICON_SHAPES, FLAG_NEW_CUSTOMIZATION_PICKER_UI)
     fun `verify valid folder path square`() {
-        ShapesProvider.folderShapes["square"]?.let { pathString ->
-            GenericPathShape(pathString)
-            PathParser.createPathFromPathData(pathString)
-        }
+        ShapesProvider.iconShapes
+            .find { it.key == SQUARE_KEY }!!
+            .run {
+                GenericPathShape(folderPathString)
+                PathParser.createPathFromPathData(folderPathString)
+            }
     }
 }
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/DisplayControllerTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/DisplayControllerTest.kt
index 588a668..0ecb38e 100644
--- a/tests/multivalentTests/src/com/android/launcher3/util/DisplayControllerTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/util/DisplayControllerTest.kt
@@ -34,6 +34,7 @@
 import com.android.launcher3.util.DisplayController.CHANGE_DENSITY
 import com.android.launcher3.util.DisplayController.CHANGE_DESKTOP_MODE
 import com.android.launcher3.util.DisplayController.CHANGE_ROTATION
+import com.android.launcher3.util.DisplayController.CHANGE_SHOW_LOCKED_TASKBAR
 import com.android.launcher3.util.DisplayController.CHANGE_TASKBAR_PINNING
 import com.android.launcher3.util.DisplayController.DisplayInfoChangeListener
 import com.android.launcher3.util.LauncherModelHelper.SandboxModelContext
@@ -203,14 +204,19 @@
     fun testTaskbarPinningChangeInLockedTaskbarChange() {
         whenever(windowManagerProxy.showLockedTaskbarOnHome(any())).thenReturn(true)
         whenever(windowManagerProxy.isHomeVisible(any())).thenReturn(true)
-        whenever(windowManagerProxy.isInDesktopMode()).thenReturn(false)
+        whenever(windowManagerProxy.isInDesktopMode(any())).thenReturn(false)
         whenever(launcherPrefs.get(TASKBAR_PINNING)).thenReturn(false)
         DisplayController.enableTaskbarModePreferenceForTests(true)
 
         assertTrue(displayController.getInfo().isTransientTaskbar())
         displayController.notifyConfigChange()
+
         verify(displayInfoChangeListener)
-            .onDisplayInfoChanged(any(), any(), eq(CHANGE_TASKBAR_PINNING))
+            .onDisplayInfoChanged(
+                any(),
+                any(),
+                eq(CHANGE_TASKBAR_PINNING or CHANGE_SHOW_LOCKED_TASKBAR),
+            )
         assertFalse(displayController.getInfo().isTransientTaskbar())
     }
 
@@ -219,7 +225,7 @@
     fun testLockedTaskbarChangeOnConfigurationChanged() {
         whenever(windowManagerProxy.showLockedTaskbarOnHome(any())).thenReturn(true)
         whenever(windowManagerProxy.isHomeVisible(any())).thenReturn(true)
-        whenever(windowManagerProxy.isInDesktopMode()).thenReturn(false)
+        whenever(windowManagerProxy.isInDesktopMode(any())).thenReturn(false)
         whenever(launcherPrefs.get(TASKBAR_PINNING)).thenReturn(false)
         DisplayController.enableTaskbarModePreferenceForTests(true)
         assertTrue(displayController.getInfo().isTransientTaskbar())
@@ -227,7 +233,11 @@
         displayController.onConfigurationChanged(configuration)
 
         verify(displayInfoChangeListener)
-            .onDisplayInfoChanged(any(), any(), eq(CHANGE_TASKBAR_PINNING))
+            .onDisplayInfoChanged(
+                any(),
+                any(),
+                eq(CHANGE_TASKBAR_PINNING or CHANGE_SHOW_LOCKED_TASKBAR),
+            )
         assertFalse(displayController.getInfo().isTransientTaskbar())
     }
 
@@ -237,7 +247,7 @@
         whenever(windowManagerProxy.showDesktopTaskbarForFreeformDisplay(any())).thenReturn(true)
         whenever(launcherPrefs.get(TASKBAR_PINNING)).thenReturn(false)
         whenever(launcherPrefs.get(TASKBAR_PINNING_IN_DESKTOP_MODE)).thenReturn(false)
-        whenever(windowManagerProxy.isInDesktopMode()).thenReturn(true)
+        whenever(windowManagerProxy.isInDesktopMode(any())).thenReturn(true)
         whenever(windowManagerProxy.isHomeVisible(any())).thenReturn(false)
         DisplayController.enableTaskbarModePreferenceForTests(true)
 
@@ -256,7 +266,7 @@
         whenever(windowManagerProxy.showDesktopTaskbarForFreeformDisplay(any())).thenReturn(true)
         whenever(launcherPrefs.get(TASKBAR_PINNING)).thenReturn(false)
         whenever(launcherPrefs.get(TASKBAR_PINNING_IN_DESKTOP_MODE)).thenReturn(false)
-        whenever(windowManagerProxy.isInDesktopMode()).thenReturn(false)
+        whenever(windowManagerProxy.isInDesktopMode(any())).thenReturn(false)
         whenever(windowManagerProxy.isHomeVisible(any())).thenReturn(false)
         DisplayController.enableTaskbarModePreferenceForTests(true)
 
@@ -275,7 +285,7 @@
         whenever(windowManagerProxy.showDesktopTaskbarForFreeformDisplay(any())).thenReturn(true)
         whenever(launcherPrefs.get(TASKBAR_PINNING)).thenReturn(false)
         whenever(launcherPrefs.get(TASKBAR_PINNING_IN_DESKTOP_MODE)).thenReturn(false)
-        whenever(windowManagerProxy.isInDesktopMode()).thenReturn(false)
+        whenever(windowManagerProxy.isInDesktopMode(any())).thenReturn(false)
         whenever(windowManagerProxy.isHomeVisible(any())).thenReturn(true)
         DisplayController.enableTaskbarModePreferenceForTests(true)
 
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/LauncherModelHelper.java b/tests/multivalentTests/src/com/android/launcher3/util/LauncherModelHelper.java
index 09b9a3b..cee5559 100644
--- a/tests/multivalentTests/src/com/android/launcher3/util/LauncherModelHelper.java
+++ b/tests/multivalentTests/src/com/android/launcher3/util/LauncherModelHelper.java
@@ -21,8 +21,8 @@
 
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
-import static com.android.launcher3.util.TestUtil.runOnExecutorSync;
 import static com.android.launcher3.util.TestUtil.grantWriteSecurePermission;
+import static com.android.launcher3.util.TestUtil.runOnExecutorSync;
 
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
@@ -51,6 +51,7 @@
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherModel;
+import com.android.launcher3.dagger.LauncherBaseAppComponent;
 import com.android.launcher3.model.BgDataModel;
 import com.android.launcher3.model.BgDataModel.Callbacks;
 import com.android.launcher3.model.ModelDbController;
@@ -267,14 +268,6 @@
         }
 
         @Override
-        public <T extends SafeCloseable> T createObject(MainThreadInitializedObject<T> object) {
-            if (object == LauncherAppState.INSTANCE) {
-                return (T) new LauncherAppState(this, null /* iconCacheFileName */);
-            }
-            return super.createObject(object);
-        }
-
-        @Override
         public File getDatabasePath(String name) {
             if (!mDbDir.exists()) {
                 mDbDir.mkdirs();
@@ -342,5 +335,10 @@
             }
             return success;
         }
+
+        @Override
+        public void initDaggerComponent(LauncherBaseAppComponent.Builder componentBuilder) {
+            super.initDaggerComponent(componentBuilder.iconsDbName(null));
+        }
     }
 }
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/ScreenOnTrackerTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/ScreenOnTrackerTest.kt
index 45cc19c..9c3f223 100644
--- a/tests/multivalentTests/src/com/android/launcher3/util/ScreenOnTrackerTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/util/ScreenOnTrackerTest.kt
@@ -51,7 +51,7 @@
 
     @Test
     fun test_default_state() {
-        verify(receiver).register(context, ACTION_SCREEN_ON, ACTION_SCREEN_OFF, ACTION_USER_PRESENT)
+        verify(receiver).register(ACTION_SCREEN_ON, ACTION_SCREEN_OFF, ACTION_USER_PRESENT)
         assertThat(underTest.isScreenOn).isTrue()
     }
 
@@ -59,7 +59,7 @@
     fun close_unregister_receiver() {
         underTest.close()
 
-        verify(receiver).unregisterReceiverSafely(context)
+        verify(receiver).unregisterReceiverSafely()
     }
 
     @Test
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/SimpleBroadcastReceiverTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/SimpleBroadcastReceiverTest.kt
index d3e27b6..17933f2 100644
--- a/tests/multivalentTests/src/com/android/launcher3/util/SimpleBroadcastReceiverTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/util/SimpleBroadcastReceiverTest.kt
@@ -52,7 +52,7 @@
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
-        underTest = SimpleBroadcastReceiver(UI_HELPER_EXECUTOR, intentConsumer)
+        underTest = SimpleBroadcastReceiver(context, UI_HELPER_EXECUTOR, intentConsumer)
         if (Looper.getMainLooper() == null) {
             Looper.prepareMainLooper()
         }
@@ -60,7 +60,7 @@
 
     @Test
     fun async_register() {
-        underTest.register(context, "test_action_1", "test_action_2")
+        underTest.register("test_action_1", "test_action_2")
         awaitTasksCompleted()
 
         verify(context).registerReceiver(same(underTest), intentFilterCaptor.capture())
@@ -72,7 +72,7 @@
 
     @Test
     fun async_register_withCompletionRunnable() {
-        underTest.register(context, completionRunnable, "test_action_1", "test_action_2")
+        underTest.register(completionRunnable, "test_action_1", "test_action_2")
         awaitTasksCompleted()
 
         verify(context).registerReceiver(same(underTest), intentFilterCaptor.capture())
@@ -85,7 +85,7 @@
 
     @Test
     fun async_register_withCompletionRunnable_and_flag() {
-        underTest.register(context, completionRunnable, 1, "test_action_1", "test_action_2")
+        underTest.register(completionRunnable, 1, "test_action_1", "test_action_2")
         awaitTasksCompleted()
 
         verify(context).registerReceiver(same(underTest), intentFilterCaptor.capture(), eq(1))
@@ -98,7 +98,7 @@
 
     @Test
     fun async_register_with_package() {
-        underTest.registerPkgActions(context, "pkg", "test_action_1", "test_action_2")
+        underTest.registerPkgActions("pkg", "test_action_1", "test_action_2")
 
         awaitTasksCompleted()
         verify(context).registerReceiver(same(underTest), intentFilterCaptor.capture())
@@ -112,9 +112,10 @@
 
     @Test
     fun sync_register_withCompletionRunnable_and_flag() {
-        underTest = SimpleBroadcastReceiver(Handler(Looper.getMainLooper()), intentConsumer)
+        underTest =
+            SimpleBroadcastReceiver(context, Handler(Looper.getMainLooper()), intentConsumer)
 
-        underTest.register(context, completionRunnable, 1, "test_action_1", "test_action_2")
+        underTest.register(completionRunnable, 1, "test_action_1", "test_action_2")
         getInstrumentation().waitForIdleSync()
 
         verify(context).registerReceiver(same(underTest), intentFilterCaptor.capture(), eq(1))
@@ -127,7 +128,7 @@
 
     @Test
     fun async_unregister() {
-        underTest.unregisterReceiverSafely(context)
+        underTest.unregisterReceiverSafely()
 
         awaitTasksCompleted()
         verify(context).unregisterReceiver(same(underTest))
@@ -135,9 +136,10 @@
 
     @Test
     fun sync_unregister() {
-        underTest = SimpleBroadcastReceiver(Handler(Looper.getMainLooper()), intentConsumer)
+        underTest =
+            SimpleBroadcastReceiver(context, Handler(Looper.getMainLooper()), intentConsumer)
 
-        underTest.unregisterReceiverSafely(context)
+        underTest.unregisterReceiverSafely()
         getInstrumentation().waitForIdleSync()
 
         verify(context).unregisterReceiver(same(underTest))
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/WidgetUtils.java b/tests/multivalentTests/src/com/android/launcher3/util/WidgetUtils.java
index a87a208..9fbd7ff 100644
--- a/tests/multivalentTests/src/com/android/launcher3/util/WidgetUtils.java
+++ b/tests/multivalentTests/src/com/android/launcher3/util/WidgetUtils.java
@@ -15,6 +15,8 @@
  */
 package com.android.launcher3.util;
 
+import static android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_HIDE_FROM_PICKER;
+
 import android.appwidget.AppWidgetProviderInfo;
 import android.content.ComponentName;
 import android.content.Context;
@@ -83,14 +85,30 @@
 
     /**
      * Creates a {@link AppWidgetProviderInfo} for the provided component name
+     *
+     * @param cn component name of the appwidget provider
+     * @param hideFromPicker indicates if the widget should appear in widget picker
      */
-    public static AppWidgetProviderInfo createAppWidgetProviderInfo(ComponentName cn) {
+    public static AppWidgetProviderInfo createAppWidgetProviderInfo(ComponentName cn,
+            boolean hideFromPicker) {
         ActivityInfo activityInfo = new ActivityInfo();
         activityInfo.applicationInfo = new ApplicationInfo();
         activityInfo.applicationInfo.uid = Process.myUid();
         AppWidgetProviderInfo info = new AppWidgetProviderInfo();
+        if (hideFromPicker) {
+            info.widgetFeatures = WIDGET_FEATURE_HIDE_FROM_PICKER;
+        }
         info.providerInfo = activityInfo;
         info.provider = cn;
         return info;
     }
+
+    /**
+     * Creates a {@link AppWidgetProviderInfo} for the provided component name
+     *
+     * @param cn component name of the appwidget provider
+     */
+    public static AppWidgetProviderInfo createAppWidgetProviderInfo(ComponentName cn) {
+        return createAppWidgetProviderInfo(cn, /*hideFromPicker=*/ false);
+    }
 }
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/GeneratedPreviewTest.kt b/tests/multivalentTests/src/com/android/launcher3/widget/GeneratedPreviewTest.kt
index b92582c..6af0950 100644
--- a/tests/multivalentTests/src/com/android/launcher3/widget/GeneratedPreviewTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/GeneratedPreviewTest.kt
@@ -17,7 +17,6 @@
 import androidx.test.filters.SmallTest
 import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
 import com.android.launcher3.Flags.FLAG_ENABLE_GENERATED_PREVIEWS
-import com.android.launcher3.InvariantDeviceProfile
 import com.android.launcher3.R
 import com.android.launcher3.icons.IconCache
 import com.android.launcher3.model.WidgetItem
@@ -100,7 +99,7 @@
 
     private fun createWidgetItem() {
         Executors.MODEL_EXECUTOR.submit {
-                val idp = InvariantDeviceProfile()
+                val idp = context.appComponent.idp
                 widgetItem = WidgetItem(appWidgetProviderInfo, idp, iconCache, context)
             }
             .get()
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfoTest.java b/tests/multivalentTests/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfoTest.java
index 48cf3df..b3fd0f7 100644
--- a/tests/multivalentTests/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfoTest.java
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfoTest.java
@@ -15,15 +15,12 @@
  */
 package com.android.launcher3.widget;
 
-import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
-
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.doAnswer;
 
 import android.appwidget.AppWidgetHostView;
-import android.content.Context;
 import android.graphics.Point;
 import android.graphics.Rect;
 
@@ -32,16 +29,14 @@
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.util.SandboxApplication;
 
-import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mockito;
 
-import java.util.ArrayList;
 import java.util.Collections;
-import java.util.List;
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
@@ -52,12 +47,7 @@
     private static final int NUM_OF_COLS = 4;
     private static final int NUM_OF_ROWS = 5;
 
-    private Context mContext;
-
-    @Before
-    public void setUp() {
-        mContext = getApplicationContext();
-    }
+    @Rule public SandboxApplication mContext = new SandboxApplication();
 
     @Test
     public void initSpans_minWidthSmallerThanCellWidth_shouldInitializeSpansToOne() {
@@ -256,8 +246,9 @@
     }
 
     private InvariantDeviceProfile createIDP() {
-        DeviceProfile dp = LauncherAppState.getIDP(mContext)
-                .getDeviceProfile(mContext).copy(mContext);
+        InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(mContext);
+
+        DeviceProfile dp = idp.getDeviceProfile(mContext).copy(mContext);
         DeviceProfile profile = Mockito.spy(dp);
         doAnswer(i -> {
             ((Point) i.getArgument(0)).set(CELL_SIZE, CELL_SIZE);
@@ -267,10 +258,7 @@
         profile.cellLayoutBorderSpacePx = new Point(SPACE_SIZE, SPACE_SIZE);
         profile.widgetPadding.setEmpty();
 
-        InvariantDeviceProfile idp = new InvariantDeviceProfile();
-        List<DeviceProfile> supportedProfiles = new ArrayList<>(idp.supportedProfiles);
-        supportedProfiles.add(profile);
-        idp.supportedProfiles = Collections.unmodifiableList(supportedProfiles);
+        idp.supportedProfiles = Collections.singletonList(profile);
         idp.numColumns = NUM_OF_COLS;
         idp.numRows = NUM_OF_ROWS;
         return idp;
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/picker/OWNERS b/tests/multivalentTests/src/com/android/launcher3/widget/picker/OWNERS
index 775b0c7..716ab90 100644
--- a/tests/multivalentTests/src/com/android/launcher3/widget/picker/OWNERS
+++ b/tests/multivalentTests/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/multivalentTests/src/com/android/launcher3/widget/picker/WidgetRecommendationCategoryProviderTest.java b/tests/multivalentTests/src/com/android/launcher3/widget/picker/WidgetRecommendationCategoryProviderTest.java
index ac67d2b..2fbeaf1 100644
--- a/tests/multivalentTests/src/com/android/launcher3/widget/picker/WidgetRecommendationCategoryProviderTest.java
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/picker/WidgetRecommendationCategoryProviderTest.java
@@ -25,8 +25,6 @@
 import static android.content.pm.ApplicationInfo.CATEGORY_VIDEO;
 import static android.content.pm.ApplicationInfo.FLAG_INSTALLED;
 
-import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
-
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.ArgumentMatchers.any;
@@ -35,11 +33,9 @@
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.when;
 
 import android.appwidget.AppWidgetProviderInfo;
 import android.content.ComponentName;
-import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.LauncherApps;
 import android.os.Process;
@@ -53,12 +49,14 @@
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.model.WidgetItem;
 import com.android.launcher3.util.Executors;
+import com.android.launcher3.util.SandboxApplication;
 import com.android.launcher3.util.WidgetUtils;
 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 
 import com.google.common.collect.ImmutableMap;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -92,22 +90,22 @@
 
     private final ApplicationInfo mTestAppInfo = ApplicationInfoBuilder.newBuilder().setPackageName(
             TEST_PACKAGE).setName(TEST_APP_NAME).build();
-    private Context mContext;
+
+    @Rule public SandboxApplication mContext = new SandboxApplication();
     @Mock
     private IconCache mIconCache;
 
     private WidgetItem mTestWidgetItem;
-    @Mock
+
     private LauncherApps mLauncherApps;
     private InvariantDeviceProfile mTestProfile;
 
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
-        mContext = spy(getInstrumentation().getTargetContext());
-        doReturn(mLauncherApps).when(mContext).getSystemService(LauncherApps.class);
+        mLauncherApps = mContext.spyService(LauncherApps.class);
         mTestAppInfo.flags = FLAG_INSTALLED;
-        mTestProfile = new InvariantDeviceProfile();
+        mTestProfile = InvariantDeviceProfile.INSTANCE.get(mContext);
         mTestProfile.numRows = 5;
         mTestProfile.numColumns = 5;
         createTestWidgetItem();
@@ -128,10 +126,10 @@
                 testCategories.entrySet()) {
 
             mTestAppInfo.category = testCategory.getKey();
-            when(mLauncherApps.getApplicationInfo(/*packageName=*/ eq(TEST_PACKAGE),
+            doReturn(mTestAppInfo).when(mLauncherApps).getApplicationInfo(
+                    /*packageName=*/ eq(TEST_PACKAGE),
                     /*flags=*/ anyInt(),
-                    /*user=*/ eq(Process.myUserHandle())))
-                    .thenReturn(mTestAppInfo);
+                    /*user=*/ eq(Process.myUserHandle()));
 
             WidgetRecommendationCategory category = Executors.MODEL_EXECUTOR.submit(() ->
                     new WidgetRecommendationCategoryProvider().getWidgetRecommendationCategory(
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinderTest.java b/tests/multivalentTests/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinderTest.java
index c9b6d4f..767ab63 100644
--- a/tests/multivalentTests/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinderTest.java
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinderTest.java
@@ -15,8 +15,6 @@
  */
 package com.android.launcher3.widget.picker;
 
-import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
-
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.ArgumentMatchers.any;
@@ -48,11 +46,13 @@
 import com.android.launcher3.model.data.PackageItemInfo;
 import com.android.launcher3.util.ActivityContextWrapper;
 import com.android.launcher3.util.PackageUserKey;
+import com.android.launcher3.util.SandboxApplication;
 import com.android.launcher3.util.WidgetUtils;
 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -67,6 +67,7 @@
     private static final String TEST_PACKAGE = "com.google.test";
     private static final String APP_NAME = "Test app";
 
+    @Rule public SandboxApplication app = new SandboxApplication();
     private Context mContext;
     private WidgetsListHeaderViewHolderBinder mViewHolderBinder;
     private InvariantDeviceProfile mTestProfile;
@@ -80,9 +81,9 @@
     public void setUp() {
         MockitoAnnotations.initMocks(this);
 
-        mContext = new ActivityContextWrapper(new ContextThemeWrapper(getApplicationContext(),
-                R.style.WidgetContainerTheme));
-        mTestProfile = new InvariantDeviceProfile();
+        mContext = new ActivityContextWrapper(new ContextThemeWrapper(
+                app, R.style.WidgetContainerTheme));
+        mTestProfile = InvariantDeviceProfile.INSTANCE.get(app);
         mTestProfile.numRows = 5;
         mTestProfile.numColumns = 5;
 
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinderTest.java b/tests/multivalentTests/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinderTest.java
index 86bbcc1..e6f13a6 100644
--- a/tests/multivalentTests/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinderTest.java
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinderTest.java
@@ -15,7 +15,6 @@
  */
 package com.android.launcher3.widget.picker;
 
-import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -50,6 +49,7 @@
 import com.android.launcher3.model.WidgetItem;
 import com.android.launcher3.model.data.PackageItemInfo;
 import com.android.launcher3.util.ActivityContextWrapper;
+import com.android.launcher3.util.SandboxApplication;
 import com.android.launcher3.util.WidgetUtils;
 import com.android.launcher3.widget.DatabaseWidgetPreviewLoader;
 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
@@ -57,6 +57,7 @@
 import com.android.launcher3.widget.model.WidgetsListContentEntry;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -71,6 +72,7 @@
     private static final String TEST_PACKAGE = "com.google.test";
     private static final String APP_NAME = "Test app";
 
+    @Rule public SandboxApplication app = new SandboxApplication();
     private Context mContext;
     private WidgetsListTableViewHolderBinder mViewHolderBinder;
     private InvariantDeviceProfile mTestProfile;
@@ -85,8 +87,8 @@
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
-        mContext = new ActivityContextWrapper(getApplicationContext());
-        mTestProfile = new InvariantDeviceProfile();
+        mContext = new ActivityContextWrapper(app);
+        mTestProfile = InvariantDeviceProfile.INSTANCE.get(app);
         mTestProfile.numRows = 5;
         mTestProfile.numColumns = 5;
 
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/picker/model/WidgetsListContentEntryTest.java b/tests/multivalentTests/src/com/android/launcher3/widget/picker/model/WidgetsListContentEntryTest.java
index 6088c8e..bd34de6 100644
--- a/tests/multivalentTests/src/com/android/launcher3/widget/picker/model/WidgetsListContentEntryTest.java
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/picker/model/WidgetsListContentEntryTest.java
@@ -37,10 +37,12 @@
 import com.android.launcher3.icons.cache.CachedObject;
 import com.android.launcher3.model.WidgetItem;
 import com.android.launcher3.model.data.PackageItemInfo;
+import com.android.launcher3.util.SandboxApplication;
 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.model.WidgetsListContentEntry;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -64,6 +66,7 @@
     private final ComponentName mWidget3 = ComponentName.createRelative(PACKAGE_NAME, ".mWidget3");
     private final Map<ComponentName, String> mWidgetsToLabels = new HashMap();
 
+    @Rule public SandboxApplication app = new SandboxApplication();
     @Mock private IconCache mIconCache;
 
     private InvariantDeviceProfile mTestProfile;
@@ -76,7 +79,7 @@
         mWidgetsToLabels.put(mWidget2, "Dog");
         mWidgetsToLabels.put(mWidget3, "Bird");
 
-        mTestProfile = new InvariantDeviceProfile();
+        mTestProfile = InvariantDeviceProfile.INSTANCE.get(app);
         mTestProfile.numRows = 5;
         mTestProfile.numColumns = 5;
 
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/picker/search/SimpleWidgetsSearchAlgorithmTest.java b/tests/multivalentTests/src/com/android/launcher3/widget/picker/search/SimpleWidgetsSearchAlgorithmTest.java
index 59f352b..0cdda3a 100644
--- a/tests/multivalentTests/src/com/android/launcher3/widget/picker/search/SimpleWidgetsSearchAlgorithmTest.java
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/picker/search/SimpleWidgetsSearchAlgorithmTest.java
@@ -46,6 +46,7 @@
 import com.android.launcher3.model.WidgetItem;
 import com.android.launcher3.model.data.PackageItemInfo;
 import com.android.launcher3.search.SearchCallback;
+import com.android.launcher3.util.SandboxApplication;
 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.model.WidgetsListBaseEntry;
 import com.android.launcher3.widget.model.WidgetsListContentEntry;
@@ -53,6 +54,7 @@
 import com.android.launcher3.widget.picker.search.WidgetsSearchBar.WidgetsSearchDataProvider;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -66,6 +68,7 @@
 @RunWith(AndroidJUnit4.class)
 public class SimpleWidgetsSearchAlgorithmTest {
 
+    @Rule public SandboxApplication app = new SandboxApplication();
     @Mock private IconCache mIconCache;
 
     private InvariantDeviceProfile mTestProfile;
@@ -90,7 +93,7 @@
             CachedObject componentWithLabel = invocation.getArgument(0);
             return componentWithLabel.getComponent().getShortClassName();
         }).when(mIconCache).getTitleNoCache(any());
-        mTestProfile = new InvariantDeviceProfile();
+        mTestProfile = InvariantDeviceProfile.INSTANCE.get(app);
         mTestProfile.numRows = 5;
         mTestProfile.numColumns = 5;
         mContext = getApplicationContext();
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/picker/util/WidgetPreviewContainerSizesTest.kt b/tests/multivalentTests/src/com/android/launcher3/widget/picker/util/WidgetPreviewContainerSizesTest.kt
index 7a858e4..2452a88 100644
--- a/tests/multivalentTests/src/com/android/launcher3/widget/picker/util/WidgetPreviewContainerSizesTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/picker/util/WidgetPreviewContainerSizesTest.kt
@@ -25,6 +25,7 @@
 import com.android.launcher3.DeviceProfile
 import com.android.launcher3.InvariantDeviceProfile
 import com.android.launcher3.LauncherAppState
+import com.android.launcher3.dagger.LauncherComponentProvider.appComponent
 import com.android.launcher3.icons.IconCache
 import com.android.launcher3.model.WidgetItem
 import com.android.launcher3.util.ActivityContextWrapper
@@ -53,7 +54,7 @@
         context = ActivityContextWrapper(ApplicationProvider.getApplicationContext())
         testInvariantProfile = LauncherAppState.getIDP(context)
         widgetItemInvariantProfile =
-            InvariantDeviceProfile().apply {
+            context.appComponent.idp.apply {
                 numRows = TEST_GRID_SIZE
                 numColumns = TEST_GRID_SIZE
             }
@@ -143,13 +144,13 @@
             widgetSize: Point,
             context: Context,
             invariantDeviceProfile: InvariantDeviceProfile,
-            iconCache: IconCache
+            iconCache: IconCache,
         ): WidgetItem {
             val providerInfo =
                 createAppWidgetProviderInfo(
                     ComponentName.createRelative(
                         TEST_PACKAGE,
-                        /*cls=*/ ".WidgetProvider_" + widgetSize.x + "x" + widgetSize.y
+                        /*cls=*/ ".WidgetProvider_" + widgetSize.x + "x" + widgetSize.y,
                     )
                 )
             val widgetInfo =
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/picker/util/WidgetsTableUtilsTest.java b/tests/multivalentTests/src/com/android/launcher3/widget/picker/util/WidgetsTableUtilsTest.java
index 2f5fcfe..a17e472 100644
--- a/tests/multivalentTests/src/com/android/launcher3/widget/picker/util/WidgetsTableUtilsTest.java
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/picker/util/WidgetsTableUtilsTest.java
@@ -15,8 +15,6 @@
  */
 package com.android.launcher3.widget.picker.util;
 
-import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
-
 import static com.android.launcher3.util.WidgetUtils.createAppWidgetProviderInfo;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -44,10 +42,12 @@
 import com.android.launcher3.model.WidgetItem;
 import com.android.launcher3.pm.ShortcutConfigActivityInfo;
 import com.android.launcher3.util.ActivityContextWrapper;
+import com.android.launcher3.util.SandboxApplication;
 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.util.WidgetsTableUtils;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -68,6 +68,8 @@
     private static final int NUM_OF_COLS = 5;
     private static final int NUM_OF_ROWS = 5;
 
+    @Rule public SandboxApplication app = new SandboxApplication();
+
     @Mock
     private IconCache mIconCache;
 
@@ -89,9 +91,8 @@
     public void setUp() {
         MockitoAnnotations.initMocks(this);
 
-        mContext = new ActivityContextWrapper(getApplicationContext());
-
-        mTestInvariantProfile = new InvariantDeviceProfile();
+        mContext = new ActivityContextWrapper(app);
+        mTestInvariantProfile = InvariantDeviceProfile.INSTANCE.get(app);
         mTestInvariantProfile.numColumns = NUM_OF_COLS;
         mTestInvariantProfile.numRows = NUM_OF_ROWS;
 
diff --git a/tests/src/com/android/launcher3/allapps/KeyboardFocusTest.java b/tests/src/com/android/launcher3/allapps/KeyboardFocusTest.java
index 1e21ee5..44df5b8 100644
--- a/tests/src/com/android/launcher3/allapps/KeyboardFocusTest.java
+++ b/tests/src/com/android/launcher3/allapps/KeyboardFocusTest.java
@@ -23,7 +23,6 @@
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.util.BaseLauncherActivityTest;
-import com.android.launcher3.util.rule.ScreenRecordRule.ScreenRecord;
 import com.android.launcher3.views.ActivityContext;
 
 import org.junit.Test;
@@ -64,7 +63,6 @@
     }
 
     @Test
-    @ScreenRecord  //b/378167329
     public void testAllAppsExitSearchAndFocusSearchResults() {
         loadLauncherSync();
         goToState(LauncherState.ALL_APPS);
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/multivalentTests/src/com/android/launcher3/folder/PreviewItemManagerTest.kt b/tests/src/com/android/launcher3/folder/PreviewItemManagerTest.kt
similarity index 97%
rename from tests/multivalentTests/src/com/android/launcher3/folder/PreviewItemManagerTest.kt
rename to tests/src/com/android/launcher3/folder/PreviewItemManagerTest.kt
index 15accbd..c956395 100644
--- a/tests/multivalentTests/src/com/android/launcher3/folder/PreviewItemManagerTest.kt
+++ b/tests/src/com/android/launcher3/folder/PreviewItemManagerTest.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * 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.
@@ -20,6 +20,7 @@
 import android.os.Process
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn
 import com.android.launcher3.LauncherAppState
 import com.android.launcher3.LauncherPrefs
 import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_FOLDER
@@ -61,11 +62,11 @@
 import org.junit.runner.Description
 import org.junit.runner.RunWith
 import org.junit.runners.model.Statement
+import org.mockito.MockitoAnnotations
 import org.mockito.kotlin.any
 import org.mockito.kotlin.argumentCaptor
 import org.mockito.kotlin.doReturn
 import org.mockito.kotlin.eq
-import org.mockito.kotlin.spy
 import org.mockito.kotlin.verify
 import org.mockito.kotlin.whenever
 
@@ -85,6 +86,7 @@
 
     @Before
     fun setup() {
+        MockitoAnnotations.initMocks(this)
         modelHelper = LauncherModelHelper()
         context = modelHelper.sandboxContext
         context.initDaggerComponent(DaggerPreviewItemManagerTestComponent.builder())
@@ -93,10 +95,8 @@
         }
         folderIcon = FolderIcon(ActivityContextWrapper(context))
 
-        val app = spy(LauncherAppState.getInstance(context))
-        iconCache = spy(app.iconCache)
-        doReturn(iconCache).whenever(app).iconCache
-        context.putObject(LauncherAppState.INSTANCE, app)
+        iconCache = LauncherAppState.INSTANCE[context].iconCache
+        spyOn(iconCache)
         doReturn(null).whenever(iconCache).updateIconInBackground(any(), any())
 
         previewItemManager = PreviewItemManager(folderIcon)
diff --git a/tests/src/com/android/launcher3/model/LoaderTaskTest.kt b/tests/src/com/android/launcher3/model/LoaderTaskTest.kt
index cdb45fc..582cf3c 100644
--- a/tests/src/com/android/launcher3/model/LoaderTaskTest.kt
+++ b/tests/src/com/android/launcher3/model/LoaderTaskTest.kt
@@ -14,8 +14,8 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.dx.mockito.inline.extended.ExtendedMockito
+import com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn
 import com.android.launcher3.Flags
-import com.android.launcher3.InvariantDeviceProfile
 import com.android.launcher3.LauncherAppState
 import com.android.launcher3.LauncherModel
 import com.android.launcher3.LauncherModel.LoaderTransaction
@@ -68,7 +68,6 @@
 import org.mockito.kotlin.anyOrNull
 import org.mockito.kotlin.doReturn
 import org.mockito.kotlin.mock
-import org.mockito.kotlin.spy
 import org.mockito.kotlin.verify
 import org.mockito.kotlin.whenever
 import org.mockito.quality.Strictness
@@ -93,7 +92,6 @@
         )
     private lateinit var mockitoSession: MockitoSession
 
-    @Mock private lateinit var app: LauncherAppState
     @Mock private lateinit var bgAllAppsList: AllAppsList
     @Mock private lateinit var modelDelegate: ModelDelegate
     @Mock private lateinit var launcherBinder: BaseLauncherBinder
@@ -109,6 +107,9 @@
 
     @get:Rule val setFlagsRule = SetFlagsRule()
 
+    private val app: LauncherAppState
+        get() = context.appComponent.launcherAppState
+
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
@@ -119,33 +120,28 @@
                 .strictness(Strictness.LENIENT)
                 .mockStatic(FirstScreenBroadcastHelper::class.java)
                 .startMocking()
-        val idp =
-            InvariantDeviceProfile().apply {
-                numRows = 5
-                numColumns = 6
-                numDatabaseHotseatIcons = 5
-            }
-        context.putObject(InvariantDeviceProfile.INSTANCE, idp)
-        context.putObject(LauncherAppState.INSTANCE, app)
-
         doReturn(TestViewHelpers.findWidgetProvider(false))
             .`when`(context.spyService(AppWidgetManager::class.java))
             .getAppWidgetInfo(any())
-        `when`(app.context).thenReturn(context)
-        `when`(app.model).thenReturn(launcherModel)
 
         `when`(launcherModel.beginLoader(any())).thenReturn(transaction)
-        `when`(app.iconCache).thenReturn(iconCache)
         `when`(launcherModel.modelDbController)
             .thenReturn(FactitiousDbController(context, INSERTION_STATEMENT_FILE))
-        `when`(app.invariantDeviceProfile).thenReturn(idp)
         `when`(launcherBinder.newIdleLock(any())).thenReturn(idleLock)
         `when`(idleLock.awaitLocked(1000)).thenReturn(false)
         `when`(iconCache.getUpdateHandler()).thenReturn(iconCacheUpdateHandler)
         `when`(widgetsFilterDataProvider.getDefaultWidgetsFilter()).thenReturn(Predicate { true })
         context.initDaggerComponent(
-            DaggerLoaderTaskTest_TestComponent.builder().bindUserCache(userCache)
+            DaggerLoaderTaskTest_TestComponent.builder()
+                .bindUserCache(userCache)
+                .bindIconCache(iconCache)
+                .bindLauncherModel(launcherModel)
         )
+        context.appComponent.idp.apply {
+            numRows = 5
+            numColumns = 6
+            numDatabaseHotseatIcons = 5
+        }
         TestUtil.grantWriteSecurePermission()
     }
 
@@ -283,8 +279,8 @@
     @EnableFlags(Flags.FLAG_ENABLE_FIRST_SCREEN_BROADCAST_ARCHIVING_EXTRAS)
     fun `When broadcast flag on and is restore and secure setting off then send new broadcast`() {
         // Given
-        val spyContext = spy(context)
-        `when`(app.context).thenReturn(spyContext)
+        spyOn(context)
+        val spyContext = context
         whenever(
                 FirstScreenBroadcastHelper.createModelsForFirstScreenBroadcast(
                     any(),
@@ -359,8 +355,8 @@
     @EnableFlags(Flags.FLAG_ENABLE_FIRST_SCREEN_BROADCAST_ARCHIVING_EXTRAS)
     fun `When not a restore then installed item broadcast not sent`() {
         // Given
-        val spyContext = spy(context)
-        `when`(app.context).thenReturn(spyContext)
+        spyOn(context)
+        val spyContext = context
         whenever(
                 FirstScreenBroadcastHelper.createModelsForFirstScreenBroadcast(
                     any(),
@@ -400,8 +396,8 @@
     @DisableFlags(Flags.FLAG_ENABLE_FIRST_SCREEN_BROADCAST_ARCHIVING_EXTRAS)
     fun `When broadcast flag off then installed item broadcast not sent`() {
         // Given
-        val spyContext = spy(context)
-        `when`(app.context).thenReturn(spyContext)
+        spyOn(context)
+        val spyContext = context
         whenever(
                 FirstScreenBroadcastHelper.createModelsForFirstScreenBroadcast(
                     any(),
@@ -446,8 +442,8 @@
     @EnableFlags(Flags.FLAG_ENABLE_FIRST_SCREEN_BROADCAST_ARCHIVING_EXTRAS)
     fun `When failsafe secure setting on then installed item broadcast not sent`() {
         // Given
-        val spyContext = spy(context)
-        `when`(app.context).thenReturn(spyContext)
+        spyOn(context)
+        val spyContext = context
         whenever(
                 FirstScreenBroadcastHelper.createModelsForFirstScreenBroadcast(
                     any(),
@@ -646,6 +642,10 @@
         interface Builder : LauncherAppComponent.Builder {
             @BindsInstance fun bindUserCache(userCache: UserCache): Builder
 
+            @BindsInstance fun bindLauncherModel(model: LauncherModel): Builder
+
+            @BindsInstance fun bindIconCache(iconCache: IconCache): Builder
+
             override fun build(): TestComponent
         }
     }
diff --git a/tests/src/com/android/launcher3/nonquickstep/DeviceProfileDumpTest.kt b/tests/src/com/android/launcher3/nonquickstep/DeviceProfileDumpTest.kt
index 2e2b6cd..05cf926 100644
--- a/tests/src/com/android/launcher3/nonquickstep/DeviceProfileDumpTest.kt
+++ b/tests/src/com/android/launcher3/nonquickstep/DeviceProfileDumpTest.kt
@@ -19,7 +19,6 @@
 import com.android.launcher3.AbstractDeviceProfileTest
 import com.android.launcher3.DeviceProfile
 import com.android.launcher3.Flags
-import com.android.launcher3.InvariantDeviceProfile
 import com.android.launcher3.util.rule.setFlags
 import org.junit.Before
 import org.junit.Test
@@ -46,7 +45,7 @@
     @Test
     fun dumpPortraitGesture() {
         initializeDevice(instance.deviceName, isGestureMode = true, isLandscape = false)
-        val dp = getDeviceProfileForGrid(instance.gridName)
+        val dp = context.appComponent.idp.getDeviceProfile(context)
         dp.isTaskbarPresentInApps = instance.isTaskbarPresentInApps
 
         assertDump(dp, instance.filename("Portrait"))
@@ -55,7 +54,7 @@
     @Test
     fun dumpPortrait3Button() {
         initializeDevice(instance.deviceName, isGestureMode = false, isLandscape = false)
-        val dp = getDeviceProfileForGrid(instance.gridName)
+        val dp = context.appComponent.idp.getDeviceProfile(context)
         dp.isTaskbarPresentInApps = instance.isTaskbarPresentInApps
 
         assertDump(dp, instance.filename("Portrait3Button"))
@@ -64,7 +63,7 @@
     @Test
     fun dumpLandscapeGesture() {
         initializeDevice(instance.deviceName, isGestureMode = true, isLandscape = true)
-        val dp = getDeviceProfileForGrid(instance.gridName)
+        val dp = context.appComponent.idp.getDeviceProfile(context)
         dp.isTaskbarPresentInApps = instance.isTaskbarPresentInApps
 
         val testName =
@@ -79,7 +78,7 @@
     @Test
     fun dumpLandscape3Button() {
         initializeDevice(instance.deviceName, isGestureMode = false, isLandscape = true)
-        val dp = getDeviceProfileForGrid(instance.gridName)
+        val dp = context.appComponent.idp.getDeviceProfile(context)
         dp.isTaskbarPresentInApps = instance.isTaskbarPresentInApps
 
         val testName =
@@ -101,26 +100,25 @@
                     deviceSpecFolded = deviceSpecs["twopanel-phone"]!!,
                     isLandscape = isLandscape,
                     isGestureMode = isGestureMode,
+                    gridName = instance.gridName,
                 )
             "tablet" ->
                 initializeVarsForTablet(
                     deviceSpec = deviceSpec,
                     isLandscape = isLandscape,
                     isGestureMode = isGestureMode,
+                    gridName = instance.gridName,
                 )
             else ->
                 initializeVarsForPhone(
                     deviceSpec = deviceSpec,
                     isVerticalBar = isLandscape,
                     isGestureMode = isGestureMode,
+                    gridName = instance.gridName,
                 )
         }
     }
 
-    private fun getDeviceProfileForGrid(gridName: String): DeviceProfile {
-        return InvariantDeviceProfile(context, gridName).getDeviceProfile(context)
-    }
-
     private fun assertDump(dp: DeviceProfile, filename: String) {
         assertDump(dp, folderName, filename)
     }
diff --git a/tests/src/com/android/launcher3/ui/workspace/ThemeIconsTest.java b/tests/src/com/android/launcher3/ui/workspace/ThemeIconsTest.java
index a123170..38970fe 100644
--- a/tests/src/com/android/launcher3/ui/workspace/ThemeIconsTest.java
+++ b/tests/src/com/android/launcher3/ui/workspace/ThemeIconsTest.java
@@ -23,7 +23,6 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
-import android.content.ContentProviderClient;
 import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.net.Uri;
@@ -37,6 +36,7 @@
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.allapps.AllAppsRecyclerView;
 import com.android.launcher3.celllayout.FavoriteItemsTransaction;
+import com.android.launcher3.dagger.LauncherComponentProvider;
 import com.android.launcher3.icons.mono.ThemedIconDrawable;
 import com.android.launcher3.popup.ArrowPopup;
 import com.android.launcher3.util.BaseLauncherActivityTest;
@@ -139,7 +139,7 @@
         return icon;
     }
 
-    private void setThemeEnabled(boolean isEnabled) throws Exception {
+    private void setThemeEnabled(boolean isEnabled) {
         Uri uri = new Uri.Builder()
                 .scheme(ContentResolver.SCHEME_CONTENT)
                 .authority(targetContext().getPackageName() + ".grid_control")
@@ -147,11 +147,10 @@
                 .build();
         ContentValues values = new ContentValues();
         values.put("boolean_value", isEnabled);
-        try (ContentProviderClient client = targetContext().getContentResolver()
-                .acquireContentProviderClient(uri)) {
-            int result = client.update(uri, values, null);
-            assertTrue(result > 0);
-        }
+
+        int result = LauncherComponentProvider.get(targetContext()).getGridCustomizationsProxy()
+                .update(uri, values, null, null);
+        assertTrue(result > 0);
     }
 
     private void switchToAllApps() {
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/BaseOverview.java b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
index ef72a0f..214f158 100644
--- a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
+++ b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
@@ -157,12 +157,7 @@
              LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
                      "dismissing all tasks")) {
             final BySelector clearAllSelector = mLauncher.getOverviewObjectSelector("clear_all");
-            for (int i = 0;
-                    i < FLINGS_FOR_DISMISS_LIMIT
-                            && !verifyActiveContainer().hasObject(clearAllSelector);
-                    ++i) {
-                flingForwardImpl();
-            }
+            flingForwardUntilClearAllVisibleImpl();
 
             final Runnable clickClearAll = () -> mLauncher.clickLauncherObject(
                     mLauncher.waitForObjectInContainer(verifyActiveContainer(),
@@ -183,6 +178,26 @@
     }
 
     /**
+     * Scrolls until Clear-all button is visible.
+     */
+    public void flingForwardUntilClearAllVisible() {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
+            flingForwardUntilClearAllVisibleImpl();
+        }
+    }
+
+    private void flingForwardUntilClearAllVisibleImpl() {
+        try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                "flinging forward to clear all")) {
+            final BySelector clearAllSelector = mLauncher.getOverviewObjectSelector("clear_all");
+            for (int i = 0; i < FLINGS_FOR_DISMISS_LIMIT && !verifyActiveContainer().hasObject(
+                    clearAllSelector); ++i) {
+                flingForwardImpl();
+            }
+        }
+    }
+
+    /**
      * Touch to the right of current task. This should dismiss overview and go back to Workspace.
      */
     public Workspace touchOutsideFirstTask() {
diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
index 1158521..8fbb5e3 100644
--- a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
+++ b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
@@ -125,11 +125,11 @@
         return right - left;
     }
 
-    int getTaskCenterX() {
+    public int getTaskCenterX() {
         return mTask.getVisibleCenter().x;
     }
 
-    int getTaskCenterY() {
+    public int getTaskCenterY() {
         return mTask.getVisibleCenter().y;
     }