Merge "Add support for different container and space calculation logic." into main
diff --git a/Android.bp b/Android.bp
index 13a926b..e358005 100644
--- a/Android.bp
+++ b/Android.bp
@@ -19,6 +19,17 @@
 
 min_launcher3_sdk_version = "30"
 
+// Targets that don't inherit framework aconfig libs (i.e., those that don't set
+// `platform_apis: true`) must manually link them.
+java_defaults {
+    name: "launcher-non-platform-apis-defaults",
+    static_libs: [
+        "android.os.flags-aconfig-java",
+        "android.appwidget.flags-aconfig-java",
+        "com.android.window.flags.window-aconfig-java",
+    ]
+}
+
 // Common source files used to build launcher (java and kotlin)
 // All sources are split so they can be reused in many other libraries/apps in other folders
 
@@ -141,7 +152,6 @@
     static_libs: [
         "LauncherPluginLib",
         "launcher_quickstep_log_protos_lite",
-        "android.os.flags-aconfig-java",
         "androidx-constraintlayout_constraintlayout",
         "androidx.recyclerview_recyclerview",
         "androidx.dynamicanimation_dynamicanimation",
@@ -163,8 +173,6 @@
         "kotlinx_coroutines",
         "com_android_launcher3_flags_lib",
         "com_android_wm_shell_flags_lib",
-        "android.appwidget.flags-aconfig-java",
-        "com.android.window.flags.window-aconfig-java",
     ],
     manifest: "AndroidManifest-common.xml",
     sdk_version: "current",
@@ -179,6 +187,7 @@
 //
 android_app {
     name: "Launcher3",
+    defaults: ["launcher-non-platform-apis-defaults"],
 
     static_libs: [
         "Launcher3ResLib",
@@ -190,7 +199,7 @@
     ],
 
     optimize: {
-        proguard_flags_files: ["proguard.flags"],
+        proguard_flags_files: [":launcher-proguard-rules"],
         // Proguard is disable for testing. Derivarive prjects to keep proguard enabled
         enabled: false,
     },
@@ -302,7 +311,9 @@
 
     static_libs: ["Launcher3QuickStepLib"],
     optimize: {
-        enabled: false,
+        proguard_flags_files: [":launcher-proguard-rules"],
+        enabled: true,
+        shrink_resources: true,
     },
 
     platform_apis: true,
@@ -349,6 +360,7 @@
     optimize: {
         proguard_flags_files: ["proguard.flags"],
         enabled: true,
+        shrink_resources: true,
     },
 
     privileged: true,
@@ -385,6 +397,7 @@
     optimize: {
         proguard_flags_files: ["proguard.flags"],
         enabled: true,
+        shrink_resources: true,
     },
 
     privileged: true,
diff --git a/aconfig/launcher.aconfig b/aconfig/launcher.aconfig
index 15ac9e3..21b9863 100644
--- a/aconfig/launcher.aconfig
+++ b/aconfig/launcher.aconfig
@@ -23,13 +23,6 @@
 }
 
 flag {
-    name: "enable_grid_only_overview"
-    namespace: "launcher"
-    description: "Enable a grid-only overview without a focused task."
-    bug: "257950105"
-}
-
-flag {
     name: "enable_cursor_hover_states"
     namespace: "launcher"
     description: "Enables cursor hover states for certain elements."
@@ -44,13 +37,6 @@
 }
 
 flag {
-    name: "enable_overview_icon_menu"
-    namespace: "launcher"
-    description: "Enable updated overview icon and menu within task."
-    bug: "257950105"
-}
-
-flag {
     name: "enable_focus_outline"
     namespace: "launcher"
     description: "Enables focus states outline for launcher."
@@ -238,13 +224,6 @@
 }
 
 flag {
-    name: "enable_refactor_task_thumbnail"
-    namespace: "launcher"
-    description: "Enables rewritten version of TaskThumbnailViews in Overview"
-    bug: "331753115"
-}
-
-flag {
   name: "enable_handle_delayed_gesture_callbacks"
   namespace: "launcher"
   description: "Enables additional handling for delayed mid-gesture callbacks"
@@ -323,3 +302,17 @@
     description: "Search bar persists at the bottom of the screen across Launcher states"
     bug: "346408388"
 }
+
+flag {
+    name: "multiline_search_bar"
+    namespace: "launcher"
+    description: "Search bar can wrap to multi-line"
+    bug: "341795751"
+}
+
+flag {
+    name: "enable_new_archiving_icon"
+    namespace: "launcher"
+    description: "Archived apps will use new icon in app title"
+    bug: "350758155"
+}
diff --git a/aconfig/launcher_overview.aconfig b/aconfig/launcher_overview.aconfig
new file mode 100644
index 0000000..f9327fe
--- /dev/null
+++ b/aconfig/launcher_overview.aconfig
@@ -0,0 +1,23 @@
+package: "com.android.launcher3"
+container: "system_ext"
+
+flag {
+    name: "enable_grid_only_overview"
+    namespace: "launcher_overview"
+    description: "Enable a grid-only overview without a focused task."
+    bug: "257950105"
+}
+
+flag {
+    name: "enable_overview_icon_menu"
+    namespace: "launcher_overview"
+    description: "Enable updated overview icon and menu within task."
+    bug: "257950105"
+}
+
+flag {
+    name: "enable_refactor_task_thumbnail"
+    namespace: "launcher_overview"
+    description: "Enables rewritten version of TaskThumbnailViews in Overview"
+    bug: "331753115"
+}
diff --git a/go/quickstep/res/values-fr-rCA/strings.xml b/go/quickstep/res/values-fr-rCA/strings.xml
index 2cc9d8f..e48faeb 100644
--- a/go/quickstep/res/values-fr-rCA/strings.xml
+++ b/go/quickstep/res/values-fr-rCA/strings.xml
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="app_share_drop_target_label" msgid="5804774105974539508">"Partager application"</string>
+    <string name="app_share_drop_target_label" msgid="5804774105974539508">"Partager appli"</string>
     <string name="action_listen" msgid="2370304050784689486">"Écouter"</string>
     <string name="action_translate" msgid="8028378961867277746">"Traduire"</string>
     <string name="action_search" msgid="6269564710943755464">"Lentille"</string>
@@ -9,12 +9,12 @@
     <string name="dialog_cancel" msgid="6464336969134856366">"ANNULER"</string>
     <string name="dialog_settings" msgid="6564397136021186148">"PARAMÈTRES"</string>
     <string name="niu_actions_confirmation_title" msgid="3863451714863526143">"Traduire ou écouter le texte à l\'écran"</string>
-    <string name="niu_actions_confirmation_text" msgid="2105271481950866089">"Des renseignements comme du texte sur votre écran, des adresses Web et des captures d\'écran peuvent être partagés avec Google.\n\nPour modifier les renseignements que vous partagez, accédez à "<b>"Paramètres &gt; Applications &gt; Applications par défaut &gt; Application d\'assistant numérique"</b>"."</string>
+    <string name="niu_actions_confirmation_text" msgid="2105271481950866089">"Des renseignements comme du texte sur votre écran, des adresses Web et des captures d\'écran peuvent être partagés avec Google.\n\nPour modifier les renseignements que vous partagez, accédez à "<b>"Paramètres &gt; Applis &gt; Applis par défaut &gt; Appli d\'assistant numérique"</b>"."</string>
     <string name="assistant_not_selected_title" msgid="5017072974603345228">"Choisir un assistant pour utiliser cette fonctionnalité"</string>
-    <string name="assistant_not_selected_text" msgid="3244613673884359276">"Pour écouter ou traduire le texte affiché sur votre écran, choisissez l\'application d\'un assistant numérique dans les paramètres"</string>
+    <string name="assistant_not_selected_text" msgid="3244613673884359276">"Pour écouter ou traduire le texte affiché sur votre écran, choisissez l\'appli d\'un assistant numérique dans les paramètres"</string>
     <string name="assistant_not_supported_title" msgid="1675788067597484142">"Modifier votre assistant pour utiliser cette fonctionnalité"</string>
-    <string name="assistant_not_supported_text" msgid="1708031078549268884">"Pour écouter ou traduire le texte affiché sur votre écran, modifiez l\'application de votre assistant numérique dans les paramètres"</string>
+    <string name="assistant_not_supported_text" msgid="1708031078549268884">"Pour écouter ou traduire le texte affiché sur votre écran, modifiez l\'appli de votre assistant numérique dans les paramètres"</string>
     <string name="tooltip_listen" msgid="7634466447860989102">"Touchez ce bouton pour écouter le texte affiché sur cet écran"</string>
     <string name="tooltip_translate" msgid="4184845868901542567">"Touchez ce bouton pour traduire le texte affiché sur cet écran"</string>
-    <string name="toast_p2p_app_not_shareable" msgid="7229739094132131536">"Cette application ne peut pas être partagée"</string>
+    <string name="toast_p2p_app_not_shareable" msgid="7229739094132131536">"Cette appli ne peut pas être partagée"</string>
 </resources>
diff --git a/go/quickstep/src/com/android/quickstep/TaskOverlayFactoryGo.java b/go/quickstep/src/com/android/quickstep/TaskOverlayFactoryGo.java
index 26ca06a..0fb9718 100644
--- a/go/quickstep/src/com/android/quickstep/TaskOverlayFactoryGo.java
+++ b/go/quickstep/src/com/android/quickstep/TaskOverlayFactoryGo.java
@@ -31,6 +31,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.SharedPreferences;
+import android.graphics.Bitmap;
 import android.graphics.Color;
 import android.graphics.Matrix;
 import android.graphics.drawable.ColorDrawable;
@@ -47,6 +48,7 @@
 import android.widget.TextView;
 
 import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.BaseActivity;
@@ -56,9 +58,8 @@
 import com.android.quickstep.util.AssistContentRequester;
 import com.android.quickstep.util.RecentsOrientedState;
 import com.android.quickstep.views.GoOverviewActionsView;
-import com.android.quickstep.views.TaskView.TaskContainer;
+import com.android.quickstep.views.TaskContainer;
 import com.android.systemui.shared.recents.model.Task;
-import com.android.systemui.shared.recents.model.ThumbnailData;
 
 import java.lang.annotation.Retention;
 
@@ -131,7 +132,7 @@
          * Called when the current task is interactive for the user
          */
         @Override
-        public void initOverlay(Task task, ThumbnailData thumbnail, Matrix matrix,
+        public void initOverlay(Task task, @Nullable Bitmap thumbnail, Matrix matrix,
                 boolean rotated) {
             if (mDialog != null && mDialog.isShowing()) {
                 // Redraw the dialog in case the layout changed
diff --git a/quickstep/res/layout/task.xml b/quickstep/res/layout/task.xml
index cc3b30e..46c1332 100644
--- a/quickstep/res/layout/task.xml
+++ b/quickstep/res/layout/task.xml
@@ -28,7 +28,7 @@
     launcher:focusBorderColor="?androidprv:attr/materialColorOutline"
     launcher:hoverBorderColor="?androidprv:attr/materialColorPrimary">
 
-    <include layout="@layout/task_thumbnail" />
+    <include layout="@layout/task_thumbnail_deprecated" />
 
     <!-- Filtering affects only alpha instead of the visibility since visibility can be altered
          separately through RecentsView#resetFromSplitSelectionState() -->
diff --git a/quickstep/res/layout/task_desktop.xml b/quickstep/res/layout/task_desktop.xml
index 89e9b3d..453057c 100644
--- a/quickstep/res/layout/task_desktop.xml
+++ b/quickstep/res/layout/task_desktop.xml
@@ -36,15 +36,6 @@
         android:layout_width="match_parent"
         android:layout_height="match_parent" />
 
-    <!--
-         TODO(b249371338): DesktopTaskView extends from TaskView. TaskView expects TaskThumbnailView
-         and IconView with these ids to be present. Need to refactor RecentsView to accept child
-         views that do not inherint from TaskView only or create a generic TaskView that have
-         N number of tasks.
-     -->
-    <include layout="@layout/task_thumbnail"
-        android:visibility="gone" />
-
     <ViewStub
         android:id="@+id/icon"
         android:inflatedId="@id/icon"
diff --git a/quickstep/res/layout/task_grouped.xml b/quickstep/res/layout/task_grouped.xml
index 87a0f70..708aa3c 100644
--- a/quickstep/res/layout/task_grouped.xml
+++ b/quickstep/res/layout/task_grouped.xml
@@ -33,9 +33,9 @@
     launcher:focusBorderColor="?androidprv:attr/materialColorOutline"
     launcher:hoverBorderColor="?androidprv:attr/materialColorPrimary">
 
-    <include layout="@layout/task_thumbnail"/>
+    <include layout="@layout/task_thumbnail_deprecated"/>
 
-    <include layout="@layout/task_thumbnail"
+    <include layout="@layout/task_thumbnail_deprecated"
         android:id="@+id/bottomright_snapshot" />
 
     <!-- Filtering affects only alpha instead of the visibility since visibility can be altered
diff --git a/quickstep/res/layout/task_thumbnail.xml b/quickstep/res/layout/task_thumbnail.xml
index f1a3d62..34640e6 100644
--- a/quickstep/res/layout/task_thumbnail.xml
+++ b/quickstep/res/layout/task_thumbnail.xml
@@ -13,8 +13,29 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<com.android.quickstep.views.TaskThumbnailViewDeprecated
+<com.android.quickstep.task.thumbnail.TaskThumbnailView
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/snapshot"
     android:layout_width="match_parent"
-    android:layout_height="match_parent" />
\ No newline at end of file
+    android:layout_height="match_parent">
+
+    <ImageView
+        android:id="@+id/task_thumbnail"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:importantForAccessibility="no"
+        android:visibility="gone"/>
+
+    <com.android.quickstep.task.thumbnail.LiveTileView
+        android:id="@+id/task_thumbnail_live_tile"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:visibility="gone"/>
+
+    <View
+        android:id="@+id/task_thumbnail_scrim"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:background="@color/overview_foreground_scrim_color"
+        android:alpha="0" />
+
+</com.android.quickstep.task.thumbnail.TaskThumbnailView>
\ No newline at end of file
diff --git a/quickstep/res/layout/task_thumbnail_deprecated.xml b/quickstep/res/layout/task_thumbnail_deprecated.xml
new file mode 100644
index 0000000..f1a3d62
--- /dev/null
+++ b/quickstep/res/layout/task_thumbnail_deprecated.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+     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.
+-->
+<com.android.quickstep.views.TaskThumbnailViewDeprecated
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/snapshot"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent" />
\ No newline at end of file
diff --git a/quickstep/res/raw-h480dp/all_set_page_bg.json b/quickstep/res/raw-h480dp/all_set_page_bg.json
deleted file mode 100644
index f2998a0..0000000
--- a/quickstep/res/raw-h480dp/all_set_page_bg.json
+++ /dev/null
@@ -1 +0,0 @@
-{"v":"5.8.1","fr":60,"ip":0,"op":181,"w":701,"h":841,"nm":"SUW_WelcomeScreen_FoldableOpen_Portrait_Dynamic","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".primary","cl":"primary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":180,"s":[55]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.07,"y":0.949},"o":{"x":0.167,"y":0.167},"t":0,"s":[181.172,148.425,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.773,"y":0.038},"t":95,"s":[181.172,-68.377,0],"to":[0,0,0],"ti":[0,0,0]},{"t":180,"s":[181.172,148.425,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-3514.717,-358.642,0],"ix":1,"l":2},"s":{"a":0,"k":[21.6,21.6,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[100.594,-128.921],[118.85,-93.491],[148.984,-67.406],[148.684,-27.55],[163.244,9.552],[144.457,44.702],[140.106,84.321],[107.136,106.715],[84.872,139.773],[45.271,144.279],[10.194,163.205],[-26.964,148.792],[-66.818,149.249],[-93.023,119.218],[-128.524,101.101],[-137.771,62.332],[-160.786,29.793],[-150.957,-8.833],[-156.215,-48.341],[-129.561,-77.974],[-115.856,-115.401],[-78.484,-129.253],[-48.956,-156.023],[-9.427,-150.921],[29.159,-160.903],[61.789,-138.015]],"o":[[-100.594,128.921],[-118.85,93.491],[-148.984,67.406],[-148.684,27.55],[-163.244,-9.552],[-144.456,-44.702],[-140.106,-84.321],[-107.136,-106.715],[-84.872,-139.773],[-45.271,-144.279],[-10.194,-163.205],[26.964,-148.792],[66.818,-149.249],[93.023,-119.218],[128.524,-101.101],[137.771,-62.332],[160.786,-29.793],[150.957,8.833],[156.215,48.341],[129.561,77.974],[115.856,115.4],[78.484,129.253],[48.956,156.023],[9.427,150.921],[-29.159,160.903],[-61.789,138.015]],"v":[[975.226,761.299],[707.165,899.424],[509.8,1127.42],[208.253,1125.148],[-72.46,1235.308],[-338.41,1093.162],[-638.164,1060.25],[-807.592,810.792],[-1057.715,642.348],[-1091.808,342.727],[-1235.002,77.338],[-1125.948,-203.807],[-1129.407,-505.342],[-902.191,-703.604],[-765.123,-972.207],[-471.796,-1042.166],[-225.603,-1216.305],[66.637,-1141.935],[365.557,-1181.715],[589.761,-980.053],[872.928,-876.361],[977.734,-593.606],[1180.277,-370.197],[1141.675,-71.124],[1217.196,220.821],[1044.029,467.699]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[-3509.952,-363.731],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Polystar 1","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.713725490196,0.556862745098,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":9.3,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":0,"op":181,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".tertiary","cl":"tertiary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.07],"y":[0.619]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[67]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[-0.263]},"t":95,"s":[82]},{"t":180,"s":[67]}],"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.07],"y":[0.927]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[458.803]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[-0.05]},"t":95,"s":[536.803]},{"t":180,"s":[458.803]}],"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.07],"y":[1.1]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[707.143]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[0.069]},"t":95,"s":[639.643]},{"t":180,"s":[707.143]}],"ix":4}},"a":{"k":[{"s":[164.438,1433.781,0],"t":0,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[164.438,1433.781,0],"t":180,"i":{"x":1,"y":1},"o":{"x":0,"y":0}}],"l":2},"s":{"a":0,"k":[-30,30,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[2361.125,4541.989],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"tr","p":{"a":0,"k":[164.438,1481.781],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.901960784314,0.764705882353,0.423529411765,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":5,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":0,"op":181,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".black","cl":"black","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":90,"ix":10},"p":{"a":0,"k":[350.5,420.5,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[420.5,-350.5],[-420.5,-350.5],[-420.5,350.5],[420.5,350.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":181,"st":0,"bm":0}],"markers":[]}
\ No newline at end of file
diff --git a/quickstep/res/raw-sw600dp-land/all_set_page_bg.json b/quickstep/res/raw-sw600dp-land/all_set_page_bg.json
deleted file mode 100644
index 63b64da..0000000
--- a/quickstep/res/raw-sw600dp-land/all_set_page_bg.json
+++ /dev/null
@@ -1 +0,0 @@
-{"v":"5.8.1","fr":60,"ip":0,"op":181,"w":841,"h":701,"nm":"SUW_WelcomeScreen_FoldableOpen_Dynamic","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".primary","cl":"primary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":180,"s":[55]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.07,"y":0.939},"o":{"x":0.167,"y":0.167},"t":0,"s":[140.975,228.318,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.773,"y":0.045},"t":95,"s":[140.975,47.65,0],"to":[0,0,0],"ti":[0,0,0]},{"t":180,"s":[140.975,228.318,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-3514.717,-358.642,0],"ix":1,"l":2},"s":{"a":0,"k":[18,18,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[100.594,-128.921],[118.85,-93.491],[148.984,-67.406],[148.684,-27.55],[163.244,9.552],[144.457,44.702],[140.106,84.321],[107.136,106.715],[84.872,139.773],[45.271,144.279],[10.194,163.205],[-26.964,148.792],[-66.818,149.249],[-93.023,119.218],[-128.524,101.101],[-137.771,62.332],[-160.786,29.793],[-150.957,-8.833],[-156.215,-48.341],[-129.561,-77.974],[-115.856,-115.401],[-78.484,-129.253],[-48.956,-156.023],[-9.427,-150.921],[29.159,-160.903],[61.789,-138.015]],"o":[[-100.594,128.921],[-118.85,93.491],[-148.984,67.406],[-148.684,27.55],[-163.244,-9.552],[-144.456,-44.702],[-140.106,-84.321],[-107.136,-106.715],[-84.872,-139.773],[-45.271,-144.279],[-10.194,-163.205],[26.964,-148.792],[66.818,-149.249],[93.023,-119.218],[128.524,-101.101],[137.771,-62.332],[160.786,-29.793],[150.957,8.833],[156.215,48.341],[129.561,77.974],[115.856,115.4],[78.484,129.253],[48.956,156.023],[9.427,150.921],[-29.159,160.903],[-61.789,138.015]],"v":[[975.226,761.299],[707.165,899.424],[509.8,1127.42],[208.253,1125.148],[-72.46,1235.308],[-338.41,1093.162],[-638.164,1060.25],[-807.592,810.792],[-1057.715,642.348],[-1091.808,342.727],[-1235.002,77.338],[-1125.948,-203.807],[-1129.407,-505.342],[-902.191,-703.604],[-765.123,-972.207],[-471.796,-1042.166],[-225.603,-1216.305],[66.637,-1141.935],[365.557,-1181.715],[589.761,-980.053],[872.928,-876.361],[977.734,-593.606],[1180.277,-370.197],[1141.675,-71.124],[1217.196,220.821],[1044.029,467.699]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[-3509.952,-363.731],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Polystar 1","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.713725490196,0.556862745098,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"k":[{"s":[11.111],"t":0,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[11.111],"t":180,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}]},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":0,"op":181,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".tertiary","cl":"tertiary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.07],"y":[0.619]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[67]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[-0.263]},"t":95,"s":[82]},{"t":180,"s":[67]}],"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.07],"y":[0.913]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[639]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[-0.06]},"t":95,"s":[704]},{"t":180,"s":[639]}],"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.07],"y":[1.12]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[527.25]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[0.083]},"t":95,"s":[471]},{"t":180,"s":[527.25]}],"ix":4}},"a":{"k":[{"s":[164.438,1433.781,0],"t":0,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[164.438,1433.781,0],"t":180,"i":{"x":1,"y":1},"o":{"x":0,"y":0}}],"l":2},"s":{"a":0,"k":[-25,25,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[2361.125,4541.989],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"tr","p":{"a":0,"k":[164.438,1481.781],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.901960784314,0.764705882353,0.423529411765,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"k":[{"s":[6],"t":0,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[6],"t":180,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}]},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":0,"op":181,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".black","cl":"black","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[420.5,350.5,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[420.5,-350.5],[-420.5,-350.5],[-420.5,350.5],[420.5,350.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":181,"st":0,"bm":0}],"markers":[]}
\ No newline at end of file
diff --git a/quickstep/res/raw-sw600dp/all_set_page_bg.json b/quickstep/res/raw-sw600dp/all_set_page_bg.json
deleted file mode 100644
index f2998a0..0000000
--- a/quickstep/res/raw-sw600dp/all_set_page_bg.json
+++ /dev/null
@@ -1 +0,0 @@
-{"v":"5.8.1","fr":60,"ip":0,"op":181,"w":701,"h":841,"nm":"SUW_WelcomeScreen_FoldableOpen_Portrait_Dynamic","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".primary","cl":"primary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":180,"s":[55]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.07,"y":0.949},"o":{"x":0.167,"y":0.167},"t":0,"s":[181.172,148.425,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.773,"y":0.038},"t":95,"s":[181.172,-68.377,0],"to":[0,0,0],"ti":[0,0,0]},{"t":180,"s":[181.172,148.425,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-3514.717,-358.642,0],"ix":1,"l":2},"s":{"a":0,"k":[21.6,21.6,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[100.594,-128.921],[118.85,-93.491],[148.984,-67.406],[148.684,-27.55],[163.244,9.552],[144.457,44.702],[140.106,84.321],[107.136,106.715],[84.872,139.773],[45.271,144.279],[10.194,163.205],[-26.964,148.792],[-66.818,149.249],[-93.023,119.218],[-128.524,101.101],[-137.771,62.332],[-160.786,29.793],[-150.957,-8.833],[-156.215,-48.341],[-129.561,-77.974],[-115.856,-115.401],[-78.484,-129.253],[-48.956,-156.023],[-9.427,-150.921],[29.159,-160.903],[61.789,-138.015]],"o":[[-100.594,128.921],[-118.85,93.491],[-148.984,67.406],[-148.684,27.55],[-163.244,-9.552],[-144.456,-44.702],[-140.106,-84.321],[-107.136,-106.715],[-84.872,-139.773],[-45.271,-144.279],[-10.194,-163.205],[26.964,-148.792],[66.818,-149.249],[93.023,-119.218],[128.524,-101.101],[137.771,-62.332],[160.786,-29.793],[150.957,8.833],[156.215,48.341],[129.561,77.974],[115.856,115.4],[78.484,129.253],[48.956,156.023],[9.427,150.921],[-29.159,160.903],[-61.789,138.015]],"v":[[975.226,761.299],[707.165,899.424],[509.8,1127.42],[208.253,1125.148],[-72.46,1235.308],[-338.41,1093.162],[-638.164,1060.25],[-807.592,810.792],[-1057.715,642.348],[-1091.808,342.727],[-1235.002,77.338],[-1125.948,-203.807],[-1129.407,-505.342],[-902.191,-703.604],[-765.123,-972.207],[-471.796,-1042.166],[-225.603,-1216.305],[66.637,-1141.935],[365.557,-1181.715],[589.761,-980.053],[872.928,-876.361],[977.734,-593.606],[1180.277,-370.197],[1141.675,-71.124],[1217.196,220.821],[1044.029,467.699]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[-3509.952,-363.731],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Polystar 1","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.713725490196,0.556862745098,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":9.3,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":0,"op":181,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".tertiary","cl":"tertiary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.07],"y":[0.619]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[67]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[-0.263]},"t":95,"s":[82]},{"t":180,"s":[67]}],"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.07],"y":[0.927]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[458.803]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[-0.05]},"t":95,"s":[536.803]},{"t":180,"s":[458.803]}],"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.07],"y":[1.1]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[707.143]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[0.069]},"t":95,"s":[639.643]},{"t":180,"s":[707.143]}],"ix":4}},"a":{"k":[{"s":[164.438,1433.781,0],"t":0,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[164.438,1433.781,0],"t":180,"i":{"x":1,"y":1},"o":{"x":0,"y":0}}],"l":2},"s":{"a":0,"k":[-30,30,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[2361.125,4541.989],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"tr","p":{"a":0,"k":[164.438,1481.781],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.901960784314,0.764705882353,0.423529411765,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":5,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":0,"op":181,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".black","cl":"black","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":90,"ix":10},"p":{"a":0,"k":[350.5,420.5,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[420.5,-350.5],[-420.5,-350.5],[-420.5,350.5],[420.5,350.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":181,"st":0,"bm":0}],"markers":[]}
\ No newline at end of file
diff --git a/quickstep/res/raw-sw720dp-land/all_set_page_bg.json b/quickstep/res/raw-sw720dp-land/all_set_page_bg.json
deleted file mode 100644
index a994b0f..0000000
--- a/quickstep/res/raw-sw720dp-land/all_set_page_bg.json
+++ /dev/null
@@ -1 +0,0 @@
-{"v":"5.8.1","fr":60,"ip":0,"op":180,"w":1280,"h":800,"nm":"SUW_WelcomeScreen_Tablet_Landscape_Dynamic","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"Null 5","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[288,540,0],"ix":2,"l":2},"a":{"a":0,"k":[50,50,0],"ix":1,"l":2},"s":{"a":0,"k":[25,25,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":600,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".primary","cl":"primary","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":180,"s":[56]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.07,"y":0.986},"o":{"x":0.167,"y":0.167},"t":0,"s":[375.832,-1006.545,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.773,"y":0.01},"t":95,"s":[375.832,-1811,0],"to":[0,0,0],"ti":[0,0,0]},{"t":180,"s":[375.832,-1006.545,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-3514.717,-358.642,0],"ix":1,"l":2},"s":{"a":0,"k":[110,110,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[75.615,-96.908],[89.338,-70.276],[111.99,-50.668],[111.764,-20.709],[122.709,7.18],[108.586,33.602],[105.316,63.383],[80.533,80.216],[63.797,105.066],[34.03,108.453],[7.663,122.679],[-20.269,111.845],[-50.226,112.189],[-69.924,89.614],[-96.61,75.997],[-103.56,46.854],[-120.861,22.395],[-113.472,-6.639],[-117.425,-36.337],[-97.389,-58.612],[-87.087,-86.745],[-58.996,-97.158],[-36.8,-117.281],[-7.086,-113.445],[21.918,-120.948],[46.446,-103.744]],"o":[[-75.615,96.909],[-89.338,70.276],[-111.99,50.668],[-111.764,20.709],[-122.709,-7.18],[-108.586,-33.602],[-105.316,-63.383],[-80.533,-80.216],[-63.797,-105.066],[-34.03,-108.453],[-7.663,-122.679],[20.269,-111.845],[50.226,-112.188],[69.924,-89.614],[96.61,-75.997],[103.56,-46.854],[120.861,-22.395],[113.472,6.64],[117.425,36.337],[97.389,58.612],[87.088,86.745],[58.995,97.158],[36.8,117.281],[7.087,113.445],[-21.918,120.948],[-46.446,103.744]],"v":[[733.209,572.105],[531.711,675.932],[383.354,847.313],[156.685,845.606],[-54.323,928.412],[-254.235,821.562],[-479.555,796.823],[-606.913,609.309],[-794.927,482.691],[-820.554,257.47],[-928.191,57.981],[-846.217,-153.353],[-848.817,-380.013],[-678.021,-529.044],[-574.99,-730.949],[-354.499,-783.537],[-169.439,-914.435],[50.234,-858.532],[274.928,-888.434],[443.46,-736.847],[656.313,-658.903],[735.094,-446.359],[887.344,-278.426],[858.327,-53.616],[915.095,165.835],[784.928,351.409]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.956862745098,0.729411764706,0.619607843137,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[-3509.952,-363.731],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Polystar 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":720,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".tertiary","cl":"tertiary","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.07],"y":[1.248]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[57]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[0.172]},"t":95,"s":[75]},{"t":180,"s":[57]}],"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.07],"y":[1.032]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[2618]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[0.022]},"t":95,"s":[2442]},{"t":180,"s":[2618]}],"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.07],"y":[1.034]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[891]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[0.024]},"t":95,"s":[694]},{"t":180,"s":[891]}],"ix":4}},"a":{"a":0,"k":[164.438,1433.781,0],"ix":1,"l":2},"s":{"a":0,"k":[120,120,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[3079.125,4685.989],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.752941176471,0.788235294118,0.752941176471,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[164.438,1481.781],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"bm":0}],"markers":[]}
\ No newline at end of file
diff --git a/quickstep/res/raw-sw720dp/all_set_page_bg.json b/quickstep/res/raw-sw720dp/all_set_page_bg.json
deleted file mode 100644
index 1030ffa..0000000
--- a/quickstep/res/raw-sw720dp/all_set_page_bg.json
+++ /dev/null
@@ -1 +0,0 @@
-{"v":"5.8.1","fr":60,"ip":0,"op":180,"w":800,"h":1280,"nm":"SUW_WelcomeScreen_Tablet_Portrait_Dynamic","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"Null 5","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[288,528,0],"ix":2,"l":2},"a":{"a":0,"k":[50,50,0],"ix":1,"l":2},"s":{"a":0,"k":[25,25,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":600,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".primary","cl":"primary","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":180,"s":[56]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.07,"y":0.986},"o":{"x":0.167,"y":0.167},"t":0,"s":[999.832,-2238.545,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.773,"y":0.01},"t":95,"s":[999.832,-3043,0],"to":[0,0,0],"ti":[0,0,0]},{"t":180,"s":[999.832,-2238.545,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-3514.717,-358.642,0],"ix":1,"l":2},"s":{"a":0,"k":[200,200,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[75.615,-96.908],[89.338,-70.276],[111.99,-50.668],[111.764,-20.709],[122.709,7.18],[108.586,33.602],[105.316,63.383],[80.533,80.216],[63.797,105.066],[34.03,108.453],[7.663,122.679],[-20.269,111.845],[-50.226,112.189],[-69.924,89.614],[-96.61,75.997],[-103.56,46.854],[-120.861,22.395],[-113.472,-6.639],[-117.425,-36.337],[-97.389,-58.612],[-87.087,-86.745],[-58.996,-97.158],[-36.8,-117.281],[-7.086,-113.445],[21.918,-120.948],[46.446,-103.744]],"o":[[-75.615,96.909],[-89.338,70.276],[-111.99,50.668],[-111.764,20.709],[-122.709,-7.18],[-108.586,-33.602],[-105.316,-63.383],[-80.533,-80.216],[-63.797,-105.066],[-34.03,-108.453],[-7.663,-122.679],[20.269,-111.845],[50.226,-112.188],[69.924,-89.614],[96.61,-75.997],[103.56,-46.854],[120.861,-22.395],[113.472,6.64],[117.425,36.337],[97.389,58.612],[87.088,86.745],[58.995,97.158],[36.8,117.281],[7.087,113.445],[-21.918,120.948],[-46.446,103.744]],"v":[[733.209,572.105],[531.711,675.932],[383.354,847.313],[156.685,845.606],[-54.323,928.412],[-254.235,821.562],[-479.555,796.823],[-606.913,609.309],[-794.927,482.691],[-820.554,257.47],[-928.191,57.981],[-846.217,-153.353],[-848.817,-380.013],[-678.021,-529.044],[-574.99,-730.949],[-354.499,-783.537],[-169.439,-914.435],[50.234,-858.532],[274.928,-888.434],[443.46,-736.847],[656.313,-658.903],[735.094,-446.359],[887.344,-278.426],[858.327,-53.616],[915.095,165.835],[784.928,351.409]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.956862745098,0.729411764706,0.619607843137,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[-3509.952,-363.731],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Polystar 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":720,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".tertiary","cl":"tertiary","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.07],"y":[1.248]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[-39]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[0.172]},"t":95,"s":[-21]},{"t":180,"s":[-39]}],"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.07],"y":[1.032]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[1490]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[0.022]},"t":95,"s":[1314]},{"t":180,"s":[1490]}],"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.07],"y":[1.034]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[2967]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[0.024]},"t":95,"s":[2770]},{"t":180,"s":[2967]}],"ix":4}},"a":{"a":0,"k":[164.438,1433.781,0],"ix":1,"l":2},"s":{"a":0,"k":[168,168,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[3079.125,4685.989],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.752941176471,0.788235294118,0.752941176471,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[164.438,1481.781],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"bm":0}],"markers":[]}
\ No newline at end of file
diff --git a/quickstep/res/raw-w600dp-h900dp/all_set_page_bg.json b/quickstep/res/raw-w600dp-h900dp/all_set_page_bg.json
new file mode 100644
index 0000000..b1a3bbe
--- /dev/null
+++ b/quickstep/res/raw-w600dp-h900dp/all_set_page_bg.json
@@ -0,0 +1 @@
+{"v":"5.8.1","fr":60,"ip":0,"op":180,"w":800,"h":1280,"nm":"SUW_WelcomeScreen_Portrait_Dynamic","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"Null 5","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[288,528,0],"ix":2,"l":2},"a":{"a":0,"k":[50,50,0],"ix":1,"l":2},"s":{"a":0,"k":[25,25,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":600,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".primary","cl":"primary","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":180,"s":[56]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.07,"y":0.986},"o":{"x":0.167,"y":0.167},"t":0,"s":[999.832,-2238.545,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.773,"y":0.01},"t":95,"s":[999.832,-3043,0],"to":[0,0,0],"ti":[0,0,0]},{"t":180,"s":[999.832,-2238.545,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-3514.717,-358.642,0],"ix":1,"l":2},"s":{"a":0,"k":[200,200,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[75.615,-96.908],[89.338,-70.276],[111.99,-50.668],[111.764,-20.709],[122.709,7.18],[108.586,33.602],[105.316,63.383],[80.533,80.216],[63.797,105.066],[34.03,108.453],[7.663,122.679],[-20.269,111.845],[-50.226,112.189],[-69.924,89.614],[-96.61,75.997],[-103.56,46.854],[-120.861,22.395],[-113.472,-6.639],[-117.425,-36.337],[-97.389,-58.612],[-87.087,-86.745],[-58.996,-97.158],[-36.8,-117.281],[-7.086,-113.445],[21.918,-120.948],[46.446,-103.744]],"o":[[-75.615,96.909],[-89.338,70.276],[-111.99,50.668],[-111.764,20.709],[-122.709,-7.18],[-108.586,-33.602],[-105.316,-63.383],[-80.533,-80.216],[-63.797,-105.066],[-34.03,-108.453],[-7.663,-122.679],[20.269,-111.845],[50.226,-112.188],[69.924,-89.614],[96.61,-75.997],[103.56,-46.854],[120.861,-22.395],[113.472,6.64],[117.425,36.337],[97.389,58.612],[87.088,86.745],[58.995,97.158],[36.8,117.281],[7.087,113.445],[-21.918,120.948],[-46.446,103.744]],"v":[[733.209,572.105],[531.711,675.932],[383.354,847.313],[156.685,845.606],[-54.323,928.412],[-254.235,821.562],[-479.555,796.823],[-606.913,609.309],[-794.927,482.691],[-820.554,257.47],[-928.191,57.981],[-846.217,-153.353],[-848.817,-380.013],[-678.021,-529.044],[-574.99,-730.949],[-354.499,-783.537],[-169.439,-914.435],[50.234,-858.532],[274.928,-888.434],[443.46,-736.847],[656.313,-658.903],[735.094,-446.359],[887.344,-278.426],[858.327,-53.616],[915.095,165.835],[784.928,351.409]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.956862745098,0.729411764706,0.619607843137,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[-3509.952,-363.731],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Polystar 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":720,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".tertiary","cl":"tertiary","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.07],"y":[1.248]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[-39]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[0.172]},"t":95,"s":[-21]},{"t":180,"s":[-39]}],"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.07],"y":[1.032]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[1490]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[0.022]},"t":95,"s":[1314]},{"t":180,"s":[1490]}],"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.07],"y":[1.034]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[2967]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[0.024]},"t":95,"s":[2770]},{"t":180,"s":[2967]}],"ix":4}},"a":{"a":0,"k":[164.438,1433.781,0],"ix":1,"l":2},"s":{"a":0,"k":[168,168,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[3079.125,4685.989],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.752941176471,0.788235294118,0.752941176471,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[164.438,1481.781],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"bm":0}],"markers":[]}
\ No newline at end of file
diff --git a/quickstep/res/raw-w840dp-h480dp/all_set_page_bg.json b/quickstep/res/raw-w840dp-h480dp/all_set_page_bg.json
index ae1b560..81de7a2 100644
--- a/quickstep/res/raw-w840dp-h480dp/all_set_page_bg.json
+++ b/quickstep/res/raw-w840dp-h480dp/all_set_page_bg.json
@@ -1 +1 @@
-{"v":"5.8.1","fr":60,"ip":0,"op":181,"w":841,"h":701,"nm":"SUW_WelcomeScreen_FelixOpen_Dynamic","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".primary","cl":"primary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":180,"s":[55]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.07,"y":0.939},"o":{"x":0.167,"y":0.167},"t":0,"s":[140.975,228.318,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.773,"y":0.045},"t":95,"s":[140.975,47.65,0],"to":[0,0,0],"ti":[0,0,0]},{"t":180,"s":[140.975,228.318,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-3514.717,-358.642,0],"ix":1,"l":2},"s":{"a":0,"k":[18,18,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[100.594,-128.921],[118.85,-93.491],[148.984,-67.406],[148.684,-27.55],[163.244,9.552],[144.457,44.702],[140.106,84.321],[107.136,106.715],[84.872,139.773],[45.271,144.279],[10.194,163.205],[-26.964,148.792],[-66.818,149.249],[-93.023,119.218],[-128.524,101.101],[-137.771,62.332],[-160.786,29.793],[-150.957,-8.833],[-156.215,-48.341],[-129.561,-77.974],[-115.856,-115.401],[-78.484,-129.253],[-48.956,-156.023],[-9.427,-150.921],[29.159,-160.903],[61.789,-138.015]],"o":[[-100.594,128.921],[-118.85,93.491],[-148.984,67.406],[-148.684,27.55],[-163.244,-9.552],[-144.456,-44.702],[-140.106,-84.321],[-107.136,-106.715],[-84.872,-139.773],[-45.271,-144.279],[-10.194,-163.205],[26.964,-148.792],[66.818,-149.249],[93.023,-119.218],[128.524,-101.101],[137.771,-62.332],[160.786,-29.793],[150.957,8.833],[156.215,48.341],[129.561,77.974],[115.856,115.4],[78.484,129.253],[48.956,156.023],[9.427,150.921],[-29.159,160.903],[-61.789,138.015]],"v":[[975.226,761.299],[707.165,899.424],[509.8,1127.42],[208.253,1125.148],[-72.46,1235.308],[-338.41,1093.162],[-638.164,1060.25],[-807.592,810.792],[-1057.715,642.348],[-1091.808,342.727],[-1235.002,77.338],[-1125.948,-203.807],[-1129.407,-505.342],[-902.191,-703.604],[-765.123,-972.207],[-471.796,-1042.166],[-225.603,-1216.305],[66.637,-1141.935],[365.557,-1181.715],[589.761,-980.053],[872.928,-876.361],[977.734,-593.606],[1180.277,-370.197],[1141.675,-71.124],[1217.196,220.821],[1044.029,467.699]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[-3509.952,-363.731],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Polystar 1","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.713725490196,0.556862745098,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"k":[{"s":[11.111],"t":0,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[11.111],"t":180,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}]},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":0,"op":181,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".tertiary","cl":"tertiary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.07],"y":[0.619]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[67]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[-0.263]},"t":95,"s":[82]},{"t":180,"s":[67]}],"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.07],"y":[0.913]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[639]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[-0.06]},"t":95,"s":[704]},{"t":180,"s":[639]}],"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.07],"y":[1.12]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[527.25]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[0.083]},"t":95,"s":[471]},{"t":180,"s":[527.25]}],"ix":4}},"a":{"k":[{"s":[164.438,1433.781,0],"t":0,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[164.438,1433.781,0],"t":180,"i":{"x":1,"y":1},"o":{"x":0,"y":0}}],"l":2},"s":{"a":0,"k":[-25,25,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[2361.125,4541.989],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"tr","p":{"a":0,"k":[164.438,1481.781],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.901960784314,0.764705882353,0.423529411765,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"k":[{"s":[6],"t":0,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[6],"t":180,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}]},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":0,"op":181,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".black","cl":"black","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[420.5,350.5,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[420.5,-350.5],[-420.5,-350.5],[-420.5,350.5],[420.5,350.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":181,"st":0,"bm":0}],"markers":[]}
\ No newline at end of file
+{"v":"5.8.1","fr":60,"ip":0,"op":180,"w":1280,"h":800,"nm":"SUW_WelcomeScreen_Landscape_Dynamic","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"Null 5","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[288,540,0],"ix":2,"l":2},"a":{"a":0,"k":[50,50,0],"ix":1,"l":2},"s":{"a":0,"k":[25,25,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":600,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".primary","cl":"primary","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":180,"s":[56]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.07,"y":0.986},"o":{"x":0.167,"y":0.167},"t":0,"s":[375.832,-1006.545,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.773,"y":0.01},"t":95, "s":[375.832,-1811,0],"to":[0,0,0],"ti":[0,0,0]},{"t":180,"s":[375.832,-1006.545,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-3514.717,-358.642,0],"ix":1,"l":2},"s":{"a":0,"k":[110,110,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[75.615,-96.908],[89.338,-70.276],[111.99,-50.668],[111.764,-20.709],[122.709,7.18],[108.586,33.602],[105.316,63.383],[80.533,80.216],[63.797,105.066],[34.03,108.453],[7.663,122.679],[-20.269,111.845],[-50.226,112.189],[-69.924,89.614],[-96.61,75.997],[-103.56,46.854],[-120.861,22.395],[-113.472,-6.639],[-117.425,-36.337],[-97.389,-58.612],[-87.087,-86.745],[-58.996,-97.158],[-36.8,-117.281],[-7.086,-113.445],[21.918,-120.948],[46.446,-103.744]],"o":[[-75.615,96.909],[-89.338,70.276],[-111.99,50.668],[-111.764,20.709],[-122.709,-7.18],[-108.586,-33.602],[-105.316,-63.383],[-80.533,-80.216],[-63.797,-105.066],[-34.03,-108.453],[-7.663,-122.679],[20.269,-111.845],[50.226,-112.188],[69.924,-89.614],[96.61,-75.997],[103.56,-46.854],[120.861,-22.395],[113.472,6.64],[117.425,36.337],[97.389,58.612],[87.088,86.745],[58.995,97.158],[36.8,117.281],[7.087,113.445],[-21.918,120.948],[-46.446,103.744]],"v":[[733.209,572.105],[531.711,675.932],[383.354,847.313],[156.685,845.606],[-54.323,928.412],[-254.235,821.562],[-479.555,796.823],[-606.913,609.309],[-794.927,482.691],[-820.554,257.47],[-928.191,57.981],[-846.217,-153.353],[-848.817,-380.013],[-678.021,-529.044],[-574.99,-730.949],[-354.499,-783.537],[-169.439,-914.435],[50.234,-858.532],[274.928,-888.434],[443.46,-736.847],[656.313,-658.903],[735.094,-446.359],[887.344,-278.426],[858.327,-53.616],[915.095,165.835],[784.928,351.409]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.956862745098,0.729411764706,0.619607843137,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[-3509.952,-363.731],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Polystar 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":720,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".tertiary","cl":"tertiary","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.07],"y":[1.248]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[57]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[0.172]},"t":95,"s":[75]},{"t":180,"s":[57]}],"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.07],"y":[1.032]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[2618]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[0.022]},"t":95,"s":[2442]},{"t":180,"s":[2618]}],"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.07],"y":[1.034]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[891]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.719],"y":[0.024]},"t":95,"s":[694]},{"t":180,"s":[891]}],"ix":4}},"a":{"a":0,"k":[164.438,1433.781,0],"ix":1,"l":2},"s":{"a":0,"k":[120,120,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[3079.125,4685.989],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.752941176471,0.788235294118,0.752941176471,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[164.438,1481.781],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"bm":0}],"markers":[]}
\ No newline at end of file
diff --git a/quickstep/res/raw-land/all_set_page_bg.json b/quickstep/res/raw-w840dp/all_set_page_bg.json
similarity index 100%
rename from quickstep/res/raw-land/all_set_page_bg.json
rename to quickstep/res/raw-w840dp/all_set_page_bg.json
diff --git a/quickstep/res/values-fr-rCA/strings.xml b/quickstep/res/values-fr-rCA/strings.xml
index edfb59e..9510494 100644
--- a/quickstep/res/values-fr-rCA/strings.xml
+++ b/quickstep/res/values-fr-rCA/strings.xml
@@ -23,34 +23,34 @@
     <string name="recent_task_option_freeform" msgid="48863056265284071">"Forme libre"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"Ordinateur de bureau"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"Aucun élément récent"</string>
-    <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"Paramètres d\'utilisation de l\'application"</string>
+    <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"Paramètres d\'utilisation de l\'appli"</string>
     <string name="recents_clear_all" msgid="5328176793634888831">"Tout effacer"</string>
-    <string name="accessibility_recent_apps" msgid="4058661986695117371">"Applications récentes"</string>
+    <string name="accessibility_recent_apps" msgid="4058661986695117371">"Applis récentes"</string>
     <string name="task_view_closed" msgid="9170038230110856166">"Tâche fermée"</string>
     <string name="task_contents_description_with_remaining_time" msgid="4479688746574672685">"<xliff:g id="TASK_DESCRIPTION">%1$s</xliff:g> : <xliff:g id="REMAINING_TIME">%2$s</xliff:g>"</string>
     <string name="shorter_duration_less_than_one_minute" msgid="4722015666335015336">"&lt; 1 min"</string>
     <string name="time_left_for_app" msgid="3111996412933644358">"Il reste <xliff:g id="TIME">%1$s</xliff:g> aujourd\'hui"</string>
-    <string name="title_app_suggestions" msgid="4185902664111965088">"Suggestions d\'applications"</string>
-    <string name="all_apps_prediction_tip" msgid="2672336544844936186">"Vos prédictions d\'applications"</string>
-    <string name="hotseat_edu_title_migrate" msgid="306578144424489980">"Obtenir des suggestions d\'applications dans la rangée du bas de votre écran d\'accueil"</string>
-    <string name="hotseat_edu_title_migrate_landscape" msgid="3633942953997845243">"Retrouvez des suggestions d\'applications dans la rangée des favoris de votre écran d\'accueil"</string>
-    <string name="hotseat_edu_message_migrate" msgid="8927179260533775320">"Accédez facilement aux applications que vous utilisez le plus, directement à l\'écran d\'accueil. Les suggestions changeront en fonction de vos habitudes. Les applications dans la rangée du bas seront déplacées vers votre écran d\'accueil."</string>
-    <string name="hotseat_edu_message_migrate_landscape" msgid="4248943380443387697">"Accédez facilement aux applications que vous utilisez le plus, directement à l\'écran d\'accueil. Les suggestions changeront en fonction de vos habitudes. Les applications dans la rangée des favoris seront déplacées vers votre écran d\'accueil."</string>
-    <string name="hotseat_edu_accept" msgid="1611544083278999837">"Obtenir des suggestions d\'applications"</string>
+    <string name="title_app_suggestions" msgid="4185902664111965088">"Suggestions d\'applis"</string>
+    <string name="all_apps_prediction_tip" msgid="2672336544844936186">"Vos prédictions d\'applis"</string>
+    <string name="hotseat_edu_title_migrate" msgid="306578144424489980">"Obtenir des suggestions d\'applis dans la rangée du bas de votre écran d\'accueil"</string>
+    <string name="hotseat_edu_title_migrate_landscape" msgid="3633942953997845243">"Retrouvez des suggestions d\'applis dans la rangée des favoris de votre écran d\'accueil"</string>
+    <string name="hotseat_edu_message_migrate" msgid="8927179260533775320">"Accédez facilement aux applis que vous utilisez le plus, directement à l\'écran d\'accueil. Les suggestions changeront en fonction de vos habitudes. Les applis dans la rangée du bas seront déplacées vers votre écran d\'accueil."</string>
+    <string name="hotseat_edu_message_migrate_landscape" msgid="4248943380443387697">"Accédez facilement aux applis que vous utilisez le plus, directement à l\'écran d\'accueil. Les suggestions changeront en fonction de vos habitudes. Les applis dans la rangée des favoris seront déplacées vers votre écran d\'accueil."</string>
+    <string name="hotseat_edu_accept" msgid="1611544083278999837">"Obtenir des suggestions d\'applis"</string>
     <string name="hotseat_edu_dismiss" msgid="2781161822780201689">"Non merci"</string>
     <string name="hotseat_prediction_settings" msgid="6246554993566070818">"Paramètres"</string>
-    <string name="hotseat_auto_enrolled" msgid="522100018967146807">"Les applications les plus utilisées s\'affichent ici et changent en fonction des habitudes"</string>
-    <string name="hotseat_tip_no_empty_slots" msgid="1325212677738179185">"Faites glisser des applications hors de la rangée du bas pour obtenir des suggestions d\'applications"</string>
-    <string name="hotseat_tip_gaps_filled" msgid="3035673010274223538">"Applications suggérées ajoutées à l\'espace vide"</string>
-    <string name="hotsaet_tip_prediction_enabled" msgid="2233554377501347650">"Les suggestions d\'applications sont activées"</string>
-    <string name="hotsaet_tip_prediction_disabled" msgid="1506426298884658491">"Les suggestions d\'applications sont désactivées"</string>
-    <string name="hotseat_prediction_content_description" msgid="4582028296938078419">"Application prédite : <xliff:g id="TITLE">%1$s</xliff:g>"</string>
+    <string name="hotseat_auto_enrolled" msgid="522100018967146807">"Les applis les plus utilisées s\'affichent ici et changent en fonction des habitudes"</string>
+    <string name="hotseat_tip_no_empty_slots" msgid="1325212677738179185">"Faites glisser des applis hors de la rangée du bas pour obtenir des suggestions d\'applis"</string>
+    <string name="hotseat_tip_gaps_filled" msgid="3035673010274223538">"Applis suggérées ajoutées à l\'espace vide"</string>
+    <string name="hotsaet_tip_prediction_enabled" msgid="2233554377501347650">"Les suggestions d\'applis sont activées"</string>
+    <string name="hotsaet_tip_prediction_disabled" msgid="1506426298884658491">"Les suggestions d\'applis sont désactivées"</string>
+    <string name="hotseat_prediction_content_description" msgid="4582028296938078419">"Appli prédite : <xliff:g id="TITLE">%1$s</xliff:g>"</string>
     <string name="gesture_tutorial_rotation_prompt_title" msgid="7537946781362766964">"Faites pivoter votre appareil"</string>
     <string name="gesture_tutorial_rotation_prompt" msgid="1664493449851960691">"Veuillez faire pivoter votre appareil pour terminer le tutoriel de navigation par gestes"</string>
     <string name="back_gesture_feedback_swipe_too_far_from_edge" msgid="4175100312909721217">"Assurez-vous de balayer l\'écran à partir de l\'extrémité droite ou gauche"</string>
     <string name="back_gesture_feedback_cancelled" msgid="762621530959111290">"Assurez-vous de balayer l\'écran à partir de l\'extrémité droite ou gauche vers le centre, puis allons-y"</string>
     <string name="back_gesture_feedback_complete_with_overview_follow_up" msgid="9176400654037014471">"Vous avez appris à balayer de la droite pour revenir en arrière. Apprenez comment changer d\'appli."</string>
-    <string name="back_gesture_feedback_complete_with_follow_up" msgid="8653374779579748392">"Vous avez appris le geste de retour en arrière. Maintenant, apprenez comment changer d\'application."</string>
+    <string name="back_gesture_feedback_complete_with_follow_up" msgid="8653374779579748392">"Vous avez appris le geste de retour en arrière. Maintenant, apprenez comment changer d\'appli."</string>
     <string name="back_gesture_feedback_complete_without_follow_up" msgid="197189945858268342">"Vous avez appris le geste de retour en arrière"</string>
     <string name="back_gesture_feedback_swipe_in_nav_bar" msgid="9157480023651452969">"Assurez-vous de ne pas balayer trop près du bas de l\'écran"</string>
     <string name="back_gesture_tutorial_confirm_subtitle" msgid="5181305411668713250">"Modifiez la sensibilité du geste de retour dans Paramètres"</string>
@@ -74,11 +74,11 @@
     <string name="overview_gesture_feedback_home_detected" msgid="663432226180397138">"Essayez de tenir la fenêtre plus longtemps avant de relâcher"</string>
     <string name="overview_gesture_feedback_wrong_swipe_direction" msgid="1191055451018584958">"Assurez-vous de balayer l\'écran vers le haut, puis de faire une pause"</string>
     <string name="overview_gesture_feedback_complete_with_follow_up" msgid="3544611727467765026">"Vous avez appris à utiliser les gestes. Pour les désactiver, accédez au menu Paramètres."</string>
-    <string name="overview_gesture_feedback_complete_without_follow_up" msgid="2903050864432331629">"Vous avez appris le geste de changement d\'application"</string>
-    <string name="overview_gesture_intro_title" msgid="2902054412868489378">"Balayez pour basculer entre les applications"</string>
-    <string name="overview_gesture_intro_subtitle" msgid="4968091015637850859">"Pour changer d\'application, balayez l\'écran de bas en haut, maintenez le doigt dessus, puis relâchez-le."</string>
+    <string name="overview_gesture_feedback_complete_without_follow_up" msgid="2903050864432331629">"Vous avez appris le geste de changement d\'appli"</string>
+    <string name="overview_gesture_intro_title" msgid="2902054412868489378">"Balayez pour basculer entre les applis"</string>
+    <string name="overview_gesture_intro_subtitle" msgid="4968091015637850859">"Pour changer d\'appli, balayez l\'écran de bas en haut, maintenez le doigt dessus, puis relâchez-le."</string>
     <string name="overview_gesture_spoken_intro_subtitle" msgid="3853371838260201751">"Pour changer d\'appli, balayez l\'écran de bas en haut avec deux doigts, maintenez-les et relâchez-les."</string>
-    <string name="overview_gesture_tutorial_title" msgid="4125835002668708720">"Changer d\'application"</string>
+    <string name="overview_gesture_tutorial_title" msgid="4125835002668708720">"Changer d\'appli"</string>
     <string name="overview_gesture_tutorial_subtitle" msgid="5253549754058973071">"Balayez l\'écran de bas en haut, maintenez le doigt en place, puis relâchez-le"</string>
     <string name="overview_gesture_tutorial_success" msgid="1910267697807973076">"Bien joué!"</string>
     <string name="gesture_tutorial_confirm_title" msgid="6201516182040074092">"Terminé"</string>
@@ -98,14 +98,14 @@
     <string name="action_split" msgid="2098009717623550676">"Partager"</string>
     <string name="action_save_app_pair" msgid="5974823919237645229">"Enr. paire d\'applis"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Toucher une autre appli pour partager l\'écran"</string>
-    <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Choisir une autre application pour utiliser l\'Écran divisé"</string>
+    <string name="toast_contextual_split_select_app" msgid="433510957123687090">"Choisir une autre appli pour utiliser l\'Écran divisé"</string>
     <string name="toast_split_select_app_cancel" msgid="1532690483356445639"><b>"Annuler"</b></string>
     <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"Quitter la sélection d\'écran divisé"</string>
-    <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Choisir une autre application pour utiliser l\'écran partagé"</string>
-    <string name="blocked_by_policy" msgid="2071401072261365546">"L\'application ou votre organisation n\'autorise pas cette action"</string>
-    <string name="split_widgets_not_supported" msgid="1355743038053053866">"Les widgets ne sont actuellement pas pris en charge. Veuillez sélectionner une autre application"</string>
+    <string name="toast_split_app_unsupported" msgid="2360229567007828914">"Choisir une autre appli pour utiliser l\'écran partagé"</string>
+    <string name="blocked_by_policy" msgid="2071401072261365546">"L\'appli ou votre organisation n\'autorise pas cette action"</string>
+    <string name="split_widgets_not_supported" msgid="1355743038053053866">"Les widgets ne sont actuellement pas pris en charge. Veuillez sélectionner une autre appli"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"Ignorer le tutoriel sur la navigation?"</string>
-    <string name="skip_tutorial_dialog_subtitle" msgid="544063326241955662">"Vous trouverez le tutoriel dans l\'application <xliff:g id="NAME">%1$s</xliff:g>"</string>
+    <string name="skip_tutorial_dialog_subtitle" msgid="544063326241955662">"Vous trouverez le tutoriel dans l\'appli <xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="gesture_tutorial_action_button_label_cancel" msgid="3809842569351264108">"Annuler"</string>
     <string name="gesture_tutorial_action_button_label_skip" msgid="394452764989751960">"Ignorer"</string>
     <string name="accessibility_rotate_button" msgid="4771825231336502943">"Faire pivoter l\'écran"</string>
@@ -137,7 +137,7 @@
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Séparateur de la barre des tâches"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Déplacer vers le coin supérieur gauche de l\'écran"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Déplacer vers le coin inférieur droit de l\'écran"</string>
-    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Afficher # autre application.}one{Afficher # autre application.}other{Afficher # autres applications.}}"</string>
+    <string name="quick_switch_overflow" msgid="6935266023013283353">"{count,plural, =1{Afficher # autre appli.}one{Afficher # autre appli.}other{Afficher # autres applis.}}"</string>
     <string name="quick_switch_desktop" msgid="4834587349322698616">"{count,plural, =1{Afficher # appli de bureau.}one{Afficher # appli de bureau.}other{Afficher # applis de bureau.}}"</string>
     <string name="quick_switch_split_task" msgid="5598194724255333896">"<xliff:g id="APP_NAME_1">%1$s</xliff:g> et <xliff:g id="APP_NAME_2">%2$s</xliff:g>"</string>
     <string name="bubble_bar_bubble_fallback_description" msgid="7811684548953452009">"Bulle"</string>
diff --git a/quickstep/res/values-nb/strings.xml b/quickstep/res/values-nb/strings.xml
index dd3d16e..6aa755a 100644
--- a/quickstep/res/values-nb/strings.xml
+++ b/quickstep/res/values-nb/strings.xml
@@ -94,7 +94,7 @@
     <string name="default_device_name" msgid="6660656727127422487">"enheten"</string>
     <string name="allset_navigation_settings" msgid="4713404605961476027"><annotation id="link">"Innstillinger for systemnavigasjon"</annotation></string>
     <string name="action_share" msgid="2648470652637092375">"Del"</string>
-    <string name="action_screenshot" msgid="8171125848358142917">"Skjermdump"</string>
+    <string name="action_screenshot" msgid="8171125848358142917">"Skjermbilde"</string>
     <string name="action_split" msgid="2098009717623550676">"Del opp"</string>
     <string name="action_save_app_pair" msgid="5974823919237645229">"Lagre apptilkobling"</string>
     <string name="toast_split_select_app" msgid="8464310533320556058">"Trykk på en annen app for å bruke delt skjerm"</string>
diff --git a/quickstep/res/values-ta/strings.xml b/quickstep/res/values-ta/strings.xml
index ed3ebee..47d8055 100644
--- a/quickstep/res/values-ta/strings.xml
+++ b/quickstep/res/values-ta/strings.xml
@@ -122,7 +122,7 @@
     <string name="taskbar_edu_close" msgid="887022990168191073">"மூடுக"</string>
     <string name="taskbar_edu_done" msgid="6880178093977704569">"முடிந்தது"</string>
     <string name="taskbar_button_home" msgid="2151398979630664652">"முகப்பு"</string>
-    <string name="taskbar_button_a11y" msgid="5241161324875094465">"அணுகல்தன்மை"</string>
+    <string name="taskbar_button_a11y" msgid="5241161324875094465">"மாற்றுத்திறன் வசதி"</string>
     <string name="taskbar_button_back" msgid="8558862226461164514">"பின்செல்லும்"</string>
     <string name="taskbar_button_ime_switcher" msgid="1730244360907588541">"IME சுவிட்ச்சர்"</string>
     <string name="taskbar_button_recents" msgid="7273376136216613134">"சமீபத்தியவை"</string>
diff --git a/quickstep/src/com/android/launcher3/WidgetPickerActivity.java b/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
index 7cdca74..44d8a5c 100644
--- a/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
+++ b/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
@@ -20,6 +20,7 @@
 import static android.view.WindowInsets.Type.navigationBars;
 import static android.view.WindowInsets.Type.statusBars;
 
+import static com.android.launcher3.Flags.enablePredictiveBackGesture;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 
@@ -33,6 +34,9 @@
 import android.view.View;
 import android.view.WindowInsetsController;
 import android.view.WindowManager;
+import android.window.BackEvent;
+import android.window.OnBackAnimationCallback;
+import android.window.OnBackInvokedDispatcher;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -44,16 +48,17 @@
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.popup.PopupDataProvider;
 import com.android.launcher3.util.ComponentKey;
-import com.android.launcher3.widget.BaseWidgetSheet;
 import com.android.launcher3.widget.WidgetCell;
 import com.android.launcher3.widget.model.WidgetsListBaseEntry;
 import com.android.launcher3.widget.model.WidgetsListContentEntry;
 import com.android.launcher3.widget.picker.WidgetsFullSheet;
 
 import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.Set;
 import java.util.function.Function;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
@@ -79,6 +84,15 @@
      */
     private static final String EXTRA_ADDED_APP_WIDGETS = "added_app_widgets";
     /**
+     * Intent extra for the string representing the title displayed within the picker header.
+     */
+    private static final String EXTRA_PICKER_TITLE = "picker_title";
+    /**
+     * Intent extra for the string representing the description displayed within the picker header.
+     */
+    private static final String EXTRA_PICKER_DESCRIPTION = "picker_description";
+
+    /**
      * A unique identifier of the surface hosting the widgets;
      * <p>"widgets" is reserved for home screen surface.</p>
      * <p>"widgets_hub" is reserved for glanceable hub surface.</p>
@@ -86,6 +100,12 @@
     private static final String EXTRA_UI_SURFACE = "ui_surface";
     private static final Pattern UI_SURFACE_PATTERN =
             Pattern.compile("^(widgets|widgets_hub)$");
+
+    /**
+     * User ids that should be filtered out of the widget lists created by this activity.
+     */
+    private static final String EXTRA_USER_ID_FILTER = "filtered_user_ids";
+
     private SimpleDragLayer<WidgetPickerActivity> mDragLayer;
     private WidgetsModel mModel;
     private LauncherAppState mApp;
@@ -100,6 +120,16 @@
     // Widgets existing on the host surface.
     @NonNull
     private List<AppWidgetProviderInfo> mAddedWidgets = new ArrayList<>();
+    @Nullable
+    private String mTitle;
+    @Nullable
+    private String mDescription;
+
+    /** A set of user ids that should be filtered out from the selected widgets. */
+    @NonNull
+    Set<Integer> mFilteredUserIds = new HashSet<>();
+    @Nullable
+    private WidgetsFullSheet mWidgetSheet;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
@@ -124,7 +154,22 @@
         refreshAndBindWidgets();
     }
 
+    @Override
+    protected void registerBackDispatcher() {
+        if (!enablePredictiveBackGesture()) {
+            super.registerBackDispatcher();
+            return;
+        }
+
+        getOnBackInvokedDispatcher().registerOnBackInvokedCallback(
+                OnBackInvokedDispatcher.PRIORITY_DEFAULT,
+                new BackAnimationCallback());
+    }
+
     private void parseIntentExtras() {
+        mTitle = getIntent().getStringExtra(EXTRA_PICKER_TITLE);
+        mDescription = getIntent().getStringExtra(EXTRA_PICKER_DESCRIPTION);
+
         // A value of 0 for either size means that no filtering will occur in that dimension. If
         // both values are 0, then no size filtering will occur.
         mDesiredWidgetWidth =
@@ -145,6 +190,12 @@
         if (addedWidgets != null) {
             mAddedWidgets = addedWidgets;
         }
+        ArrayList<Integer> filteredUsers = getIntent().getIntegerArrayListExtra(
+                EXTRA_USER_ID_FILTER);
+        mFilteredUserIds.clear();
+        if (filteredUsers != null) {
+            mFilteredUserIds.addAll(filteredUsers);
+        }
     }
 
     @NonNull
@@ -223,7 +274,7 @@
     /** Updates the model with widgets, applies filters and launches the widgets sheet once
      * widgets are available */
     private void refreshAndBindWidgets() {
-        MODEL_EXECUTOR.getHandler().postDelayed(() -> {
+        MODEL_EXECUTOR.execute(() -> {
             LauncherAppState app = LauncherAppState.getInstance(this);
             mModel.update(app, null);
             final List<WidgetsListBaseEntry> allWidgets =
@@ -253,18 +304,19 @@
                         mUiSurface, allWidgetItems);
                 mWidgetPredictionsRequester.request(mAddedWidgets, this::bindRecommendedWidgets);
             }
-        }, mDeviceProfile.bottomSheetOpenDuration);
+        });
     }
 
     private void bindWidgets(List<WidgetsListBaseEntry> widgets) {
         MAIN_EXECUTOR.execute(() -> mPopupDataProvider.setAllWidgets(widgets));
     }
 
-    private void openWidgetsSheet() {
+   private void openWidgetsSheet() {
         MAIN_EXECUTOR.execute(() -> {
-            BaseWidgetSheet widgetSheet = WidgetsFullSheet.show(this, true);
-            widgetSheet.disableNavBarScrim(true);
-            widgetSheet.addOnCloseListener(this::finish);
+            mWidgetSheet = WidgetsFullSheet.show(this, true);
+            mWidgetSheet.mayUpdateTitleAndDescription(mTitle, mDescription);
+            mWidgetSheet.disableNavBarScrim(true);
+            mWidgetSheet.addOnCloseListener(this::finish);
         });
     }
 
@@ -283,12 +335,64 @@
         }
     }
 
+    /**
+     * Animation callback for different predictive back animation states for the widget picker.
+     */
+    private class BackAnimationCallback implements OnBackAnimationCallback {
+        @Nullable
+        OnBackAnimationCallback mActiveOnBackAnimationCallback;
+
+        @Override
+        public void onBackStarted(@NonNull BackEvent backEvent) {
+            if (mActiveOnBackAnimationCallback != null) {
+                mActiveOnBackAnimationCallback.onBackCancelled();
+            }
+            if (mWidgetSheet != null) {
+                mActiveOnBackAnimationCallback = mWidgetSheet;
+                mActiveOnBackAnimationCallback.onBackStarted(backEvent);
+            }
+        }
+
+        @Override
+        public void onBackInvoked() {
+            if (mActiveOnBackAnimationCallback == null) {
+                return;
+            }
+            mActiveOnBackAnimationCallback.onBackInvoked();
+            mActiveOnBackAnimationCallback = null;
+        }
+
+        @Override
+        public void onBackProgressed(@NonNull BackEvent backEvent) {
+            if (mActiveOnBackAnimationCallback == null) {
+                return;
+            }
+            mActiveOnBackAnimationCallback.onBackProgressed(backEvent);
+        }
+
+        @Override
+        public void onBackCancelled() {
+            if (mActiveOnBackAnimationCallback == null) {
+                return;
+            }
+            mActiveOnBackAnimationCallback.onBackCancelled();
+            mActiveOnBackAnimationCallback = null;
+        }
+    };
+
     private WidgetAcceptabilityVerdict isWidgetAcceptable(WidgetItem widget) {
         final AppWidgetProviderInfo info = widget.widgetInfo;
         if (info == null) {
             return rejectWidget(widget, "shortcut");
         }
 
+        if (mFilteredUserIds.contains(widget.user.getIdentifier())) {
+            return rejectWidget(
+                    widget,
+                    "widget user: %d is being filtered",
+                    widget.user.getIdentifier());
+        }
+
         if (mWidgetCategoryFilter > 0 && (info.widgetCategory & mWidgetCategoryFilter) == 0) {
             return rejectWidget(
                     widget,
diff --git a/quickstep/src/com/android/launcher3/model/WellbeingModel.java b/quickstep/src/com/android/launcher3/model/WellbeingModel.java
index 28bc01c..fb17f15 100644
--- a/quickstep/src/com/android/launcher3/model/WellbeingModel.java
+++ b/quickstep/src/com/android/launcher3/model/WellbeingModel.java
@@ -83,10 +83,8 @@
 
     private final Handler mWorkerHandler;
     private final ContentObserver mContentObserver;
-    private final SimpleBroadcastReceiver mWellbeingAppChangeReceiver =
-            new SimpleBroadcastReceiver(t -> restartObserver());
-    private final SimpleBroadcastReceiver mAppAddRemoveReceiver =
-            new SimpleBroadcastReceiver(this::onAppPackageChanged);
+    private final SimpleBroadcastReceiver mWellbeingAppChangeReceiver;
+    private final SimpleBroadcastReceiver mAppAddRemoveReceiver;
 
     private final Object mModelLock = new Object();
     // Maps the action Id to the corresponding RemoteAction
@@ -101,6 +99,11 @@
         mWorkerHandler = new Handler(TextUtils.isEmpty(mWellbeingProviderPkg)
                 ? Executors.UI_HELPER_EXECUTOR.getLooper()
                 : Executors.getPackageExecutor(mWellbeingProviderPkg).getLooper());
+        mWellbeingAppChangeReceiver =
+                new SimpleBroadcastReceiver(mWorkerHandler, t -> restartObserver());
+        mAppAddRemoveReceiver =
+                new SimpleBroadcastReceiver(mWorkerHandler, this::onAppPackageChanged);
+
 
         mContentObserver = new ContentObserver(mWorkerHandler) {
             @Override
@@ -135,8 +138,8 @@
     public void close() {
         if (!TextUtils.isEmpty(mWellbeingProviderPkg)) {
             mWorkerHandler.post(() -> {
-                mWellbeingAppChangeReceiver.unregisterReceiverSafelySync(mContext);
-                mAppAddRemoveReceiver.unregisterReceiverSafelySync(mContext);
+                mWellbeingAppChangeReceiver.unregisterReceiverSafely(mContext);
+                mAppAddRemoveReceiver.unregisterReceiverSafely(mContext);
                 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 41fcf61..8c98bab 100644
--- a/quickstep/src/com/android/launcher3/model/WidgetPredictionsRequester.java
+++ b/quickstep/src/com/android/launcher3/model/WidgetPredictionsRequester.java
@@ -59,6 +59,11 @@
 public class WidgetPredictionsRequester {
     private static final int NUM_OF_RECOMMENDED_WIDGETS_PREDICATION = 20;
     private static final String BUNDLE_KEY_ADDED_APP_WIDGETS = "added_app_widgets";
+    // container/screenid/[positionx,positiony]/[spanx,spany]
+    // Matches the format passed used by PredictionHelper; But, position and size values aren't
+    // used, so, we pass default values.
+    @VisibleForTesting
+    static final String LAUNCH_LOCATION = "workspace/1/[0,0]/[2,2]";
 
     @Nullable
     private AppPredictor mAppPredictor;
@@ -86,7 +91,7 @@
      */
     public void request(List<AppWidgetProviderInfo> existingWidgets,
             Consumer<List<ItemInfo>> callback) {
-        Bundle bundle = buildBundleForPredictionSession(existingWidgets, mUiSurface);
+        Bundle bundle = buildBundleForPredictionSession(existingWidgets);
         Predicate<WidgetItem> filter = notOnUiSurfaceFilter(existingWidgets);
 
         MODEL_EXECUTOR.execute(() -> {
@@ -112,17 +117,14 @@
      * Returns a bundle that can be passed in a prediction session
      *
      * @param addedWidgets widgets that are already added by the user in the ui surface
-     * @param uiSurface    a unique identifier of the surface hosting widgets; format
-     *                     "widgets_xx"; note - "widgets" is reserved for home screen surface.
      */
     @VisibleForTesting
-    static Bundle buildBundleForPredictionSession(List<AppWidgetProviderInfo> addedWidgets,
-            String uiSurface) {
+    static Bundle buildBundleForPredictionSession(List<AppWidgetProviderInfo> addedWidgets) {
         Bundle bundle = new Bundle();
         ArrayList<AppTargetEvent> addedAppTargetEvents = new ArrayList<>();
         for (AppWidgetProviderInfo info : addedWidgets) {
             ComponentName componentName = info.provider;
-            AppTargetEvent appTargetEvent = buildAppTargetEvent(uiSurface, info, componentName);
+            AppTargetEvent appTargetEvent = buildAppTargetEvent(info, componentName);
             addedAppTargetEvents.add(appTargetEvent);
         }
         bundle.putParcelableArrayList(BUNDLE_KEY_ADDED_APP_WIDGETS, addedAppTargetEvents);
@@ -134,13 +136,13 @@
      * predictor.
      * Also see {@link PredictionHelper}
      */
-    private static AppTargetEvent buildAppTargetEvent(String uiSurface, AppWidgetProviderInfo info,
+    private static AppTargetEvent buildAppTargetEvent(AppWidgetProviderInfo info,
             ComponentName componentName) {
         AppTargetId appTargetId = new AppTargetId("widget:" + componentName.getPackageName());
         AppTarget appTarget = new AppTarget.Builder(appTargetId, componentName.getPackageName(),
                 /*user=*/ info.getProfile()).setClassName(componentName.getClassName()).build();
-        return new AppTargetEvent.Builder(appTarget, AppTargetEvent.ACTION_PIN)
-                .setLaunchLocation(uiSurface).build();
+        return new AppTargetEvent.Builder(appTarget, AppTargetEvent.ACTION_PIN).setLaunchLocation(
+                LAUNCH_LOCATION).build();
     }
 
     /**
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
index 358d703..46501c4 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
@@ -245,11 +245,20 @@
         }
 
         void updateThumbnailInBackground(Task task, Consumer<ThumbnailData> callback) {
-            mModel.getThumbnailCache().updateThumbnailInBackground(task, callback);
+            mModel.getThumbnailCache().getThumbnailInBackground(task,
+                    thumbnailData -> {
+                        task.thumbnail = thumbnailData;
+                        callback.accept(thumbnailData);
+                    });
         }
 
         void updateIconInBackground(Task task, Consumer<Task> callback) {
-            mModel.getIconCache().updateIconInBackground(task, callback);
+            mModel.getIconCache().getIconInBackground(task, (icon, contentDescription, title) -> {
+                task.icon = icon;
+                task.titleDescription = contentDescription;
+                task.title = title;
+                callback.accept(task);
+            });
         }
 
         void onCloseComplete() {
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
index 5d47212..0ba5de1 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
@@ -46,6 +46,7 @@
 import androidx.core.content.res.ResourcesCompat;
 
 import com.android.app.animation.Interpolators;
+import com.android.internal.jank.Cuj;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimatedFloat;
@@ -53,6 +54,7 @@
 import com.android.launcher3.testing.shared.TestProtocol;
 import com.android.quickstep.util.DesktopTask;
 import com.android.quickstep.util.GroupTask;
+import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
 
 import java.util.HashMap;
 import java.util.List;
@@ -331,6 +333,8 @@
             @Override
             public void onAnimationStart(Animator animation) {
                 super.onAnimationStart(animation);
+                InteractionJankMonitorWrapper.begin(
+                        KeyboardQuickSwitchView.this, Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_OPEN);
                 setClipToPadding(false);
                 setOutlineProvider(new ViewOutlineProvider() {
                     @Override
@@ -366,12 +370,19 @@
             }
 
             @Override
+            public void onAnimationCancel(Animator animation) {
+                super.onAnimationCancel(animation);
+                InteractionJankMonitorWrapper.cancel(Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_OPEN);
+            }
+
+            @Override
             public void onAnimationEnd(Animator animation) {
                 super.onAnimationEnd(animation);
                 setClipToPadding(true);
                 setOutlineProvider(outlineProvider);
                 invalidateOutline();
                 mOpenAnimation = null;
+                InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_OPEN);
             }
         });
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
index 73819b3..d411ba6 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
@@ -16,6 +16,7 @@
 package com.android.launcher3.taskbar;
 
 import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
 import android.view.KeyEvent;
 import android.view.animation.AnimationUtils;
 import android.window.RemoteTransition;
@@ -23,6 +24,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.android.internal.jank.Cuj;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimatorListeners;
 import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext;
@@ -30,6 +32,7 @@
 import com.android.quickstep.util.SlideInRemoteTransition;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.ThumbnailData;
+import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
 import com.android.systemui.shared.system.QuickStepContract;
 
 import java.io.PrintWriter;
@@ -93,18 +96,28 @@
 
     protected void closeQuickSwitchView(boolean animate) {
         if (isCloseAnimationRunning()) {
-            // Let currently-running animation finish.
             if (!animate) {
                 mCloseAnimation.end();
             }
+            // Let currently-running animation finish.
             return;
         }
         if (!animate) {
+            InteractionJankMonitorWrapper.begin(
+                    mKeyboardQuickSwitchView, Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_CLOSE);
             onCloseComplete();
             return;
         }
         mCloseAnimation = mKeyboardQuickSwitchView.getCloseAnimation();
 
+        mCloseAnimation.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationStart(Animator animation) {
+                super.onAnimationStart(animation);
+                InteractionJankMonitorWrapper.begin(
+                        mKeyboardQuickSwitchView, Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_CLOSE);
+            }
+        });
         mCloseAnimation.addListener(AnimatorListeners.forEndCallback(this::onCloseComplete));
         mCloseAnimation.start();
     }
@@ -142,16 +155,26 @@
             return -1;
         }
 
+        Runnable onStartCallback = () -> InteractionJankMonitorWrapper.begin(
+                mKeyboardQuickSwitchView, Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH);
+        Runnable onFinishCallback = () -> InteractionJankMonitorWrapper.end(
+                Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH);
         TaskbarActivityContext context = mControllers.taskbarActivityContext;
         RemoteTransition remoteTransition = new RemoteTransition(new SlideInRemoteTransition(
                 Utilities.isRtl(mControllers.taskbarActivityContext.getResources()),
                 context.getDeviceProfile().overviewPageSpacing,
                 QuickStepContract.getWindowCornerRadius(context),
                 AnimationUtils.loadInterpolator(
-                        context, android.R.interpolator.fast_out_extra_slow_in)),
+                        context, android.R.interpolator.fast_out_extra_slow_in),
+                onStartCallback,
+                onFinishCallback),
                 "SlideInTransition");
         mControllers.taskbarActivityContext.handleGroupTaskLaunch(
-                task, remoteTransition, mOnDesktop);
+                task,
+                remoteTransition,
+                mOnDesktop,
+                onStartCallback,
+                onFinishCallback);
         return -1;
     }
 
@@ -159,6 +182,7 @@
         mCloseAnimation = null;
         mOverlayContext.getDragLayer().removeView(mKeyboardQuickSwitchView);
         mControllerCallbacks.onCloseComplete();
+        InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_CLOSE);
     }
 
     protected void onDestroy() {
diff --git a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
index b647a3e..0fa3fbc 100644
--- a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
@@ -247,7 +247,9 @@
                 ? context.getColor(R.color.taskbar_nav_icon_light_color)
                 : context.getColor(R.color.taskbar_nav_icon_dark_color);
 
-        mTaskbarTransitions = new TaskbarTransitions(mContext, mNavButtonsView);
+        if (enableTaskbarOnPhones() && mContext.isPhoneButtonNavMode()) {
+            mTaskbarTransitions = new TaskbarTransitions(mContext, mNavButtonsView);
+        }
     }
 
     /**
@@ -359,7 +361,9 @@
                 R.bool.floating_rotation_button_position_left);
         mControllers.rotationButtonController.setRotationButton(mFloatingRotationButton,
                 mRotationButtonListener);
-        mTaskbarTransitions.init();
+        if (enableTaskbarOnPhones() && mContext.isPhoneButtonNavMode()) {
+            mTaskbarTransitions.init();
+        }
 
         applyState();
         mPropertyHolders.forEach(StatePropertyHolder::endAnimation);
@@ -621,7 +625,9 @@
     }
 
     public void setWallpaperVisible(boolean isVisible) {
-        mTaskbarTransitions.setWallpaperVisibility(isVisible);
+        if (enableTaskbarOnPhones() && mContext.isPhoneButtonNavMode()) {
+            mTaskbarTransitions.setWallpaperVisibility(isVisible);
+        }
     }
 
     public void onTransitionModeUpdated(int barMode, boolean checkBarModes) {
@@ -632,25 +638,32 @@
     }
 
     public void checkNavBarModes() {
-        boolean isBarHidden = (mSysuiStateFlags & SYSUI_STATE_NAV_BAR_HIDDEN) != 0;
-        mTaskbarTransitions.transitionTo(mTransitionMode, !isBarHidden);
-    }
-
-    public void finishBarAnimations() {
-        mTaskbarTransitions.finishAnimations();
-    }
-
-    public void touchAutoDim(boolean reset) {
-        mTaskbarTransitions.setAutoDim(false);
-        mHandler.removeCallbacks(mAutoDim);
-        if (reset) {
-            mHandler.postDelayed(mAutoDim, AUTODIM_TIMEOUT_MS);
+        if (enableTaskbarOnPhones() && mContext.isPhoneButtonNavMode()) {
+            boolean isBarHidden = (mSysuiStateFlags & SYSUI_STATE_NAV_BAR_HIDDEN) != 0;
+            mTaskbarTransitions.transitionTo(mTransitionMode, !isBarHidden);
         }
     }
 
-    public void transitionTo(@BarTransitions.TransitionMode int barMode,
-            boolean animate) {
-        mTaskbarTransitions.transitionTo(barMode, animate);
+    public void finishBarAnimations() {
+        if (enableTaskbarOnPhones() && mContext.isPhoneButtonNavMode()) {
+            mTaskbarTransitions.finishAnimations();
+        }
+    }
+
+    public void touchAutoDim(boolean reset) {
+        if (enableTaskbarOnPhones() && mContext.isPhoneButtonNavMode()) {
+            mTaskbarTransitions.setAutoDim(false);
+            mHandler.removeCallbacks(mAutoDim);
+            if (reset) {
+                mHandler.postDelayed(mAutoDim, AUTODIM_TIMEOUT_MS);
+            }
+        }
+    }
+
+    public void transitionTo(@BarTransitions.TransitionMode int barMode, boolean animate) {
+        if (enableTaskbarOnPhones() && mContext.isPhoneButtonNavMode()) {
+            mTaskbarTransitions.transitionTo(barMode, animate);
+        }
     }
 
     /** Use to set the translationY for the all nav+contextual buttons */
@@ -752,7 +765,9 @@
 
     private void onDarkIntensityChanged() {
         updateNavButtonColor();
-        mTaskbarTransitions.onDarkIntensityChanged(mTaskbarNavButtonDarkIntensity.value);
+        if (enableTaskbarOnPhones() && mContext.isPhoneButtonNavMode()) {
+            mTaskbarTransitions.onDarkIntensityChanged(mTaskbarNavButtonDarkIntensity.value);
+        }
     }
 
     protected ImageView addButton(@DrawableRes int drawableId, @TaskbarButton int buttonType,
@@ -1100,7 +1115,9 @@
                 + mOnBackgroundNavButtonColorOverrideMultiplier.value);
 
         mNavButtonsView.dumpLogs(prefix + "\t", pw);
-        mTaskbarTransitions.dumpLogs(prefix + "\t", pw);
+        if (enableTaskbarOnPhones() && mContext.isPhoneButtonNavMode()) {
+            mTaskbarTransitions.dumpLogs(prefix + "\t", pw);
+        }
     }
 
     private static String getStateString(int flags) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 3481e52..3048243 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -43,6 +43,8 @@
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING;
 import static com.android.wm.shell.Flags.enableTinyTaskbar;
 
+import static java.lang.invoke.MethodHandles.Lookup.PROTECTED;
+
 import android.animation.AnimatorSet;
 import android.animation.ValueAnimator;
 import android.app.ActivityOptions;
@@ -93,6 +95,7 @@
 import com.android.launcher3.model.data.AppPairInfo;
 import com.android.launcher3.model.data.FolderInfo;
 import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.TaskItemInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.popup.PopupContainerWithArrow;
 import com.android.launcher3.popup.PopupDataProvider;
@@ -466,7 +469,12 @@
      * Show Taskbar upon receiving broadcast
      */
     public void showTaskbarFromBroadcast() {
-        mControllers.taskbarStashController.showTaskbarFromBroadcast();
+        // If user is in middle of taskbar education handle go to next step of education
+        if (mControllers.taskbarEduTooltipController.isBeforeTooltipFeaturesStep()) {
+            mControllers.taskbarEduTooltipController.hide();
+            mControllers.taskbarEduTooltipController.maybeShowFeaturesEdu();
+        }
+        mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(false);
     }
 
     /** Toggles Taskbar All Apps overlay. */
@@ -805,6 +813,11 @@
      */
     public void setUIController(@NonNull TaskbarUIController uiController) {
         mControllers.setUiController(uiController);
+        if (mControllers.bubbleControllers.isEmpty()) {
+            // if the bubble bar was visible in a previous configuration of taskbar and is being
+            // recreated now without bubbles, clean up any bubble bar adjustments from hotseat
+            bubbleBarVisibilityChanged(/* isVisible= */ false);
+        }
     }
 
     /**
@@ -1143,6 +1156,11 @@
                 mControllers.uiController.onTaskbarIconLaunched(api);
                 mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true);
             }
+        } else if (tag instanceof TaskItemInfo info) {
+            UI_HELPER_EXECUTOR.execute(() ->
+                    SystemUiProxy.INSTANCE.get(this).showDesktopApp(info.getTaskId()));
+            mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(
+                    /* stash= */ true);
         } else if (tag instanceof WorkspaceItemInfo) {
             // Tapping a launchable icon on Taskbar
             WorkspaceItemInfo info = (WorkspaceItemInfo) tag;
@@ -1226,22 +1244,44 @@
         }
     }
 
+    public void handleGroupTaskLaunch(
+            GroupTask task,
+            @Nullable RemoteTransition remoteTransition,
+            boolean onDesktop) {
+        handleGroupTaskLaunch(task, remoteTransition, onDesktop, null, null);
+    }
+
     /**
      * Launches the given GroupTask with the following behavior:
      * - If the GroupTask is a DesktopTask, launch the tasks in that Desktop.
      * - If {@code onDesktop}, bring the given GroupTask to the front.
      * - If the GroupTask is a single task, launch it via startActivityFromRecents.
      * - Otherwise, we assume the GroupTask is a Split pair and launch them together.
+     * <p>
+     * Given start and/or finish callbacks, they will be run before an after the app launch
+     * respectively in cases where we can't use the remote transition, otherwise we will assume that
+     * these callbacks are included in the remote transition.
      */
-    public void handleGroupTaskLaunch(GroupTask task, @Nullable RemoteTransition remoteTransition,
-            boolean onDesktop) {
+    public void handleGroupTaskLaunch(
+            GroupTask task,
+            @Nullable RemoteTransition remoteTransition,
+            boolean onDesktop,
+            @Nullable Runnable onStartCallback,
+            @Nullable Runnable onFinishCallback) {
         if (task instanceof DesktopTask) {
             UI_HELPER_EXECUTOR.execute(() ->
                     SystemUiProxy.INSTANCE.get(this).showDesktopApps(getDisplay().getDisplayId(),
                             remoteTransition));
         } else if (onDesktop) {
-            UI_HELPER_EXECUTOR.execute(() ->
-                    SystemUiProxy.INSTANCE.get(this).showDesktopApp(task.task1.key.id));
+            UI_HELPER_EXECUTOR.execute(() -> {
+                if (onStartCallback != null) {
+                    onStartCallback.run();
+                }
+                SystemUiProxy.INSTANCE.get(this).showDesktopApp(task.task1.key.id);
+                if (onFinishCallback != null) {
+                    onFinishCallback.run();
+                }
+            });
         } else if (task.task2 == null) {
             UI_HELPER_EXECUTOR.execute(() -> {
                 ActivityOptions activityOptions =
@@ -1528,7 +1568,8 @@
         return mIsNavBarKidsMode && isThreeButtonNav();
     }
 
-    protected boolean isNavBarForceVisible() {
+    @VisibleForTesting(otherwise = PROTECTED)
+    public boolean isNavBarForceVisible() {
         return mIsNavBarForceVisible;
     }
 
@@ -1670,6 +1711,10 @@
         return mControllers.uiController.canToggleHomeAllApps();
     }
 
+    boolean isIconAlignedWithHotseat() {
+        return mControllers.uiController.isIconAlignedWithHotseat();
+    }
+
     @VisibleForTesting
     public TaskbarControllers getControllers() {
         return mControllers;
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
index 58c5e83..0645972 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
@@ -282,6 +282,11 @@
         }
         uiController.dumpLogs(prefix + "\t", pw);
         rotationButtonController.dumpLogs(prefix + "\t", pw);
+        if (bubbleControllers.isPresent()) {
+            bubbleControllers.get().dump(pw);
+        } else {
+            pw.println(String.format("%s\t%s", prefix, "Bubble controllers are empty."));
+        }
     }
 
     /**
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltip.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltip.kt
index c45c667..7f9d8a3 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltip.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltip.kt
@@ -27,6 +27,7 @@
 import android.view.ViewGroup
 import android.view.ViewGroup.LayoutParams.MATCH_PARENT
 import android.view.animation.Interpolator
+import android.window.OnBackInvokedDispatcher
 import androidx.core.view.updateLayoutParams
 import com.android.app.animation.Interpolators.EMPHASIZED_ACCELERATE
 import com.android.app.animation.Interpolators.EMPHASIZED_DECELERATE
@@ -66,11 +67,14 @@
     /** Container where the tooltip's body should be inflated. */
     lateinit var content: ViewGroup
         private set
+
     private lateinit var arrow: View
 
     /** Callback invoked when the tooltip is being closed. */
     var onCloseCallback: () -> Unit = {}
     private var openCloseAnimator: AnimatorSet? = null
+    /** Used to set whether users can tap outside the current tooltip window to dismiss it */
+    var allowTouchDismissal = true
 
     /** Animates the tooltip into view. */
     fun show() {
@@ -134,14 +138,25 @@
     override fun isOfType(type: Int): Boolean = type and TYPE_TASKBAR_EDUCATION_DIALOG != 0
 
     override fun onControllerInterceptTouchEvent(ev: MotionEvent?): Boolean {
-        if (ev?.action == ACTION_DOWN && !activityContext.dragLayer.isEventOverView(this, ev)) {
+        if (
+            ev?.action == ACTION_DOWN &&
+                !activityContext.dragLayer.isEventOverView(this, ev) &&
+                allowTouchDismissal
+        ) {
             close(true)
         }
         return false
     }
 
+    override fun onAttachedToWindow() {
+        super.onAttachedToWindow()
+        findOnBackInvokedDispatcher()
+            ?.registerOnBackInvokedCallback(OnBackInvokedDispatcher.PRIORITY_DEFAULT, this)
+    }
+
     override fun onDetachedFromWindow() {
         super.onDetachedFromWindow()
+        findOnBackInvokedDispatcher()?.unregisterOnBackInvokedCallback(this)
         Settings.Secure.putInt(mContext.contentResolver, LAUNCHER_TASKBAR_EDUCATION_SHOWING, 0)
     }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt
index 5cbd5c9..d57c483 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt
@@ -86,10 +86,13 @@
                 !activityContext.isPhoneMode &&
                 !activityContext.isTinyTaskbar
         }
+
     private val isOpen: Boolean
         get() = tooltip?.isOpen ?: false
+
     val isBeforeTooltipFeaturesStep: Boolean
         get() = isTooltipEnabled && tooltipStep <= TOOLTIP_STEP_FEATURES
+
     private lateinit var controllers: TaskbarControllers
 
     // Keep track of whether the user has seen the Search Edu
@@ -152,6 +155,7 @@
         tooltipStep = TOOLTIP_STEP_NONE
         inflateTooltip(R.layout.taskbar_edu_features)
         tooltip?.run {
+            allowTouchDismissal = false
             val splitscreenAnim = requireViewById<LottieAnimationView>(R.id.splitscreen_animation)
             val suggestionsAnim = requireViewById<LottieAnimationView>(R.id.suggestions_animation)
             val pinningAnim = requireViewById<LottieAnimationView>(R.id.pinning_animation)
@@ -216,6 +220,7 @@
         inflateTooltip(R.layout.taskbar_edu_pinning)
 
         tooltip?.run {
+            allowTouchDismissal = true
             requireViewById<LottieAnimationView>(R.id.standalone_pinning_animation)
                 .supportLightTheme()
 
@@ -260,6 +265,7 @@
         userHasSeenSearchEdu = true
         inflateTooltip(R.layout.taskbar_edu_search)
         tooltip?.run {
+            allowTouchDismissal = true
             requireViewById<LottieAnimationView>(R.id.search_edu_animation).supportLightTheme()
             val eduSubtitle: TextView = requireViewById(R.id.search_edu_text)
             showDisclosureText(eduSubtitle)
@@ -332,7 +338,9 @@
     }
 
     /** Closes the current [tooltip]. */
-    fun hide() = tooltip?.close(true)
+    fun hide() {
+        tooltip?.close(true)
+    }
 
     /** Initializes [tooltip] with content from [contentResId]. */
     private fun inflateTooltip(@LayoutRes contentResId: Int) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarHoverToolTipController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarHoverToolTipController.java
index 0443197..dd14109 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarHoverToolTipController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarHoverToolTipController.java
@@ -40,6 +40,7 @@
 import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.apppairs.AppPairIcon;
 import com.android.launcher3.compat.AccessibilityManagerCompat;
 import com.android.launcher3.folder.FolderIcon;
 import com.android.launcher3.views.ArrowTipView;
@@ -73,6 +74,8 @@
         } else if (mHoverView instanceof FolderIcon
                 && ((FolderIcon) mHoverView).mInfo.title != null) {
             mToolTipText = ((FolderIcon) mHoverView).mInfo.title.toString();
+        } else if (mHoverView instanceof AppPairIcon) {
+            mToolTipText = ((AppPairIcon) mHoverView).getTitleTextView().getText().toString();
         } else {
             mToolTipText = null;
         }
@@ -156,6 +159,10 @@
         if (mHoverView == null || mToolTipText == null) {
             return;
         }
+        // Do not show tooltip if taskbar icons are transitioning to hotseat.
+        if (mActivity.isIconAlignedWithHotseat()) {
+            return;
+        }
         if (mHoverView instanceof FolderIcon && !((FolderIcon) mHoverView).getIconVisible()) {
             return;
         }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
index b294208..f411e79 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
@@ -39,7 +39,6 @@
 import android.content.ComponentCallbacks;
 import android.content.Context;
 import android.content.Intent;
-import android.content.IntentFilter;
 import android.content.pm.ActivityInfo;
 import android.content.res.Configuration;
 import android.hardware.display.DisplayManager;
@@ -115,11 +114,12 @@
     private WindowManager mWindowManager;
     private FrameLayout mTaskbarRootLayout;
     private boolean mAddedWindow;
+    private boolean mIsSuspended;
     private final TaskbarNavButtonController mNavButtonController;
     private final ComponentCallbacks mComponentCallbacks;
 
     private final SimpleBroadcastReceiver mShutdownReceiver =
-            new SimpleBroadcastReceiver(i -> destroyExistingTaskbar());
+            new SimpleBroadcastReceiver(UI_HELPER_EXECUTOR, i -> destroyExistingTaskbar());
 
     // 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
@@ -156,7 +156,7 @@
     private boolean mUserUnlocked = false;
 
     private final SimpleBroadcastReceiver mTaskbarBroadcastReceiver =
-            new SimpleBroadcastReceiver(this::showTaskbarFromBroadcast);
+            new SimpleBroadcastReceiver(UI_HELPER_EXECUTOR, this::showTaskbarFromBroadcast);
 
     private final AllAppsActionManager mAllAppsActionManager;
 
@@ -305,17 +305,15 @@
                 .register(NAV_BAR_KIDS_MODE, mOnSettingsChangeListener);
         Log.d(TASKBAR_NOT_DESTROYED_TAG, "registering component callbacks from constructor.");
         mContext.registerComponentCallbacks(mComponentCallbacks);
-        mShutdownReceiver.registerAsync(mContext, Intent.ACTION_SHUTDOWN);
+        mShutdownReceiver.register(mContext, Intent.ACTION_SHUTDOWN);
         UI_HELPER_EXECUTOR.execute(() -> {
             mSharedState.taskbarSystemActionPendingIntent = PendingIntent.getBroadcast(
                     mContext,
                     SYSTEM_ACTION_ID_TASKBAR,
                     new Intent(ACTION_SHOW_TASKBAR).setPackage(mContext.getPackageName()),
                     PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
-            mContext.registerReceiver(
-                    mTaskbarBroadcastReceiver,
-                    new IntentFilter(ACTION_SHOW_TASKBAR),
-                    RECEIVER_NOT_EXPORTED);
+            mTaskbarBroadcastReceiver.register(
+                    mContext, RECEIVER_NOT_EXPORTED, ACTION_SHOW_TASKBAR);
         });
 
         debugWhyTaskbarNotDestroyed("TaskbarManager created");
@@ -443,6 +441,8 @@
      */
     @VisibleForTesting
     public synchronized void recreateTaskbar() {
+        if (mIsSuspended) return;
+
         Trace.beginSection("recreateTaskbar");
         try {
             DeviceProfile dp = mUserUnlocked ?
@@ -620,7 +620,7 @@
     public void destroy() {
         debugWhyTaskbarNotDestroyed("TaskbarManager#destroy()");
         removeActivityCallbacksAndListeners();
-        mTaskbarBroadcastReceiver.unregisterReceiverSafelyAsync(mContext);
+        mTaskbarBroadcastReceiver.unregisterReceiverSafely(mContext);
         destroyExistingTaskbar();
         removeTaskbarRootViewFromWindow();
         if (mUserUnlocked) {
@@ -632,7 +632,7 @@
                 .unregister(NAV_BAR_KIDS_MODE, mOnSettingsChangeListener);
         Log.d(TASKBAR_NOT_DESTROYED_TAG, "unregistering component callbacks from destroy().");
         mContext.unregisterComponentCallbacks(mComponentCallbacks);
-        mShutdownReceiver.unregisterReceiverSafelyAsync(mContext);
+        mShutdownReceiver.unregisterReceiverSafely(mContext);
     }
 
     public @Nullable TaskbarActivityContext getCurrentActivityContext() {
@@ -648,8 +648,22 @@
         }
     }
 
+    /**
+     * Removes Taskbar from the window manager and prevents recreation if {@code true}.
+     * <p>
+     * Suspending is for testing purposes only; avoid calling this method in production.
+     */
     @VisibleForTesting
-    void addTaskbarRootViewToWindow() {
+    public void setSuspended(boolean isSuspended) {
+        mIsSuspended = isSuspended;
+        if (mIsSuspended) {
+            removeTaskbarRootViewFromWindow();
+        } else {
+            addTaskbarRootViewToWindow();
+        }
+    }
+
+    private void addTaskbarRootViewToWindow() {
         if (enableTaskbarNoRecreate() && !mAddedWindow && mTaskbarActivityContext != null) {
             mWindowManager.addView(mTaskbarRootLayout,
                     mTaskbarActivityContext.getWindowLayoutParams());
@@ -657,8 +671,7 @@
         }
     }
 
-    @VisibleForTesting
-    void removeTaskbarRootViewFromWindow() {
+    private void removeTaskbarRootViewFromWindow() {
         if (enableTaskbarNoRecreate() && mAddedWindow) {
             mWindowManager.removeViewImmediate(mTaskbarRootLayout);
             mAddedWindow = false;
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java
index 0b7ae39..5024cd8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java
@@ -196,26 +196,26 @@
         final TaskbarRecentAppsController recentAppsController =
                 mControllers.taskbarRecentAppsController;
         hotseatItemInfos = recentAppsController.updateHotseatItemInfos(hotseatItemInfos);
-        Set<String> runningPackages = recentAppsController.getRunningAppPackages();
-        Set<String> minimizedPackages = recentAppsController.getMinimizedAppPackages();
+        Set<Integer> runningTaskIds = recentAppsController.getRunningTaskIds();
+        Set<Integer> minimizedTaskIds = recentAppsController.getMinimizedTaskIds();
 
         if (mDeferUpdatesForSUW) {
             ItemInfo[] finalHotseatItemInfos = hotseatItemInfos;
             mDeferredUpdates = () ->
                     commitHotseatItemUpdates(finalHotseatItemInfos,
-                            recentAppsController.getShownTasks(), runningPackages,
-                            minimizedPackages);
+                            recentAppsController.getShownTasks(), runningTaskIds,
+                            minimizedTaskIds);
         } else {
             commitHotseatItemUpdates(hotseatItemInfos,
-                    recentAppsController.getShownTasks(), runningPackages, minimizedPackages);
+                    recentAppsController.getShownTasks(), runningTaskIds, minimizedTaskIds);
         }
     }
 
     private void commitHotseatItemUpdates(ItemInfo[] hotseatItemInfos, List<GroupTask> recentTasks,
-            Set<String> runningPackages, Set<String> minimizedPackages) {
+            Set<Integer> runningTaskIds, Set<Integer> minimizedTaskIds) {
         mContainer.updateHotseatItems(hotseatItemInfos, recentTasks);
         mControllers.taskbarViewController.updateIconViewsRunningStates(
-                runningPackages, minimizedPackages);
+                runningTaskIds, minimizedTaskIds);
     }
 
     /**
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java
index d26a36d..872a4d0 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java
@@ -24,6 +24,7 @@
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_BACK_BUTTON_TAP;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_HOME_BUTTON_LONGPRESS;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_HOME_BUTTON_TAP;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_IME_SWITCHER_BUTTON_LONGPRESS;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_IME_SWITCHER_BUTTON_TAP;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_OVERVIEW_BUTTON_LONGPRESS;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_OVERVIEW_BUTTON_TAP;
@@ -37,6 +38,7 @@
 import android.util.Log;
 import android.view.HapticFeedbackConstants;
 import android.view.View;
+import android.view.inputmethod.Flags;
 
 import androidx.annotation.IntDef;
 import androidx.annotation.Nullable;
@@ -147,7 +149,7 @@
                 break;
             case BUTTON_IME_SWITCH:
                 logEvent(LAUNCHER_TASKBAR_IME_SWITCHER_BUTTON_TAP);
-                showIMESwitcher();
+                onImeSwitcherPress();
                 break;
             case BUTTON_A11Y:
                 logEvent(LAUNCHER_TASKBAR_A11Y_BUTTON_TAP);
@@ -166,8 +168,12 @@
         if (buttonType == BUTTON_SPACE) {
             return false;
         }
-        // Provide the same haptic feedback that the system offers for virtual keys.
-        view.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
+
+        // Provide the same haptic feedback that the system offers for long press.
+        // The haptic feedback from long pressing on the home button is handled by circle to search.
+        if (buttonType != BUTTON_HOME) {
+            view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+        }
         switch (buttonType) {
             case BUTTON_HOME:
                 logEvent(LAUNCHER_TASKBAR_HOME_BUTTON_LONGPRESS);
@@ -179,11 +185,19 @@
                 return true;
             case BUTTON_BACK:
                 logEvent(LAUNCHER_TASKBAR_BACK_BUTTON_LONGPRESS);
-                return backRecentsLongpress(buttonType);
+                backRecentsLongpress(buttonType);
+                return true;
             case BUTTON_RECENTS:
                 logEvent(LAUNCHER_TASKBAR_OVERVIEW_BUTTON_LONGPRESS);
-                return backRecentsLongpress(buttonType);
+                backRecentsLongpress(buttonType);
+                return true;
             case BUTTON_IME_SWITCH:
+                if (Flags.imeSwitcherRevamp()) {
+                    logEvent(LAUNCHER_TASKBAR_IME_SWITCHER_BUTTON_LONGPRESS);
+                    onImeSwitcherLongPress();
+                    return true;
+                }
+                return false;
             default:
                 return false;
         }
@@ -299,10 +313,14 @@
         mSystemUiProxy.onBackPressed();
     }
 
-    private void showIMESwitcher() {
+    private void onImeSwitcherPress() {
         mSystemUiProxy.onImeSwitcherPressed();
     }
 
+    private void onImeSwitcherLongPress() {
+        mSystemUiProxy.onImeSwitcherLongPress();
+    }
+
     private void notifyA11yClick(boolean longClick) {
         if (longClick) {
             mSystemUiProxy.notifyAccessibilityButtonLongClicked();
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
index fc3b4c7..5c08116 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
@@ -18,17 +18,17 @@
 import androidx.annotation.VisibleForTesting
 import com.android.launcher3.Flags.enableRecentsInTaskbar
 import com.android.launcher3.model.data.ItemInfo
+import com.android.launcher3.model.data.TaskItemInfo
+import com.android.launcher3.model.data.WorkspaceItemInfo
 import com.android.launcher3.statehandlers.DesktopVisibilityController
 import com.android.launcher3.taskbar.TaskbarControllers.LoggableTaskbarController
 import com.android.launcher3.util.CancellableTask
 import com.android.quickstep.RecentsModel
 import com.android.quickstep.util.DesktopTask
 import com.android.quickstep.util.GroupTask
-import com.android.systemui.shared.recents.model.Task
 import com.android.window.flags.Flags.enableDesktopWindowingMode
 import com.android.window.flags.Flags.enableDesktopWindowingTaskbarRunningApps
 import java.io.PrintWriter
-import java.util.function.Consumer
 
 /**
  * Provides recent apps functionality, when the Taskbar Recent Apps section is enabled. Behavior:
@@ -60,9 +60,13 @@
     // Initialized in init.
     private lateinit var controllers: TaskbarControllers
 
-    private var shownHotseatItems: List<ItemInfo> = emptyList()
+    var shownHotseatItems: List<ItemInfo> = emptyList()
+        private set
+
     private var allRecentTasks: List<GroupTask> = emptyList()
     private var desktopTask: DesktopTask? = null
+    // Keeps track of the order in which running tasks appear.
+    private var orderedRunningTaskIds = emptyList<Int>()
     var shownTasks: List<GroupTask> = emptyList()
         private set
 
@@ -72,9 +76,9 @@
     private val isInDesktopMode: Boolean
         get() = desktopVisibilityController?.areDesktopTasksVisible() ?: false
 
-    val runningAppPackages: Set<String>
+    val runningTaskIds: Set<Int>
         /**
-         * Returns the package names of apps that should be indicated as "running" to the user.
+         * Returns the task IDs of apps that should be indicated as "running" to the user.
          * Specifically, we return all the open tasks if we are in Desktop mode, else emptySet().
          */
         get() {
@@ -82,22 +86,19 @@
                 return emptySet()
             }
             val tasks = desktopTask?.tasks ?: return emptySet()
-            return tasks.map { task -> task.key.packageName }.toSet()
+            return tasks.map { task -> task.key.id }.toSet()
         }
 
-    val minimizedAppPackages: Set<String>
+    val minimizedTaskIds: Set<Int>
         /**
-         * Returns the package names of apps that should be indicated as "minimized" to the user.
-         * Specifically, we return all the running packages where all the tasks in that package are
-         * minimized (not visible).
+         * Returns the task IDs for the tasks that should be indicated as "minimized" to the user.
          */
         get() {
             if (!canShowRunningApps || !isInDesktopMode) {
                 return emptySet()
             }
             val desktopTasks = desktopTask?.tasks ?: return emptySet()
-            val packageToTasks = desktopTasks.groupBy { it.key.packageName }
-            return packageToTasks.filterValues { tasks -> tasks.all { !it.isVisible } }.keys
+            return desktopTasks.filter { !it.isVisible }.map { task -> task.key.id }.toSet()
         }
 
     private val recentTasksChangedListener =
@@ -139,59 +140,118 @@
                 .filter { itemInfo -> !itemInfo.isPredictedItem }
                 .toMutableList()
 
+        if (isInDesktopMode && canShowRunningApps) {
+            shownHotseatItems =
+                updateHotseatItemsFromRunningTasks(
+                    getOrderedAndWrappedDesktopTasks(),
+                    shownHotseatItems
+                )
+        }
+
         onRecentsOrHotseatChanged()
 
         return shownHotseatItems.toTypedArray()
     }
 
+    private fun getOrderedAndWrappedDesktopTasks(): List<GroupTask> {
+        val tasks = desktopTask?.tasks ?: emptyList()
+        // Kind of hacky, we wrap each single task in the Desktop as a GroupTask.
+        val orderFromId = orderedRunningTaskIds.withIndex().associate { (index, id) -> id to index }
+        val sortedTasks = tasks.sortedWith(compareBy(nullsLast()) { orderFromId[it.key.id] })
+        return sortedTasks.map { GroupTask(it) }
+    }
+
     private fun reloadRecentTasksIfNeeded() {
         if (!recentsModel.isTaskListValid(taskListChangeId)) {
             taskListChangeId =
                 recentsModel.getTasks { tasks ->
                     allRecentTasks = tasks
+                    val oldRunningTaskdIds = runningTaskIds
+                    val oldMinimizedTaskIds = minimizedTaskIds
                     desktopTask = allRecentTasks.filterIsInstance<DesktopTask>().firstOrNull()
-                    onRecentsOrHotseatChanged()
-                    controllers.taskbarViewController.commitRunningAppsToUI()
+                    val runningTasksChanged = oldRunningTaskdIds != runningTaskIds
+                    val minimizedTasksChanged = oldMinimizedTaskIds != minimizedTaskIds
+                    if (
+                        onRecentsOrHotseatChanged() || runningTasksChanged || minimizedTasksChanged
+                    ) {
+                        controllers.taskbarViewController.commitRunningAppsToUI()
+                    }
                 }
         }
     }
 
-    private fun onRecentsOrHotseatChanged() {
+    /**
+     * Updates [shownTasks] when Recents or Hotseat changes.
+     *
+     * @return Whether [shownTasks] changed.
+     */
+    private fun onRecentsOrHotseatChanged(): Boolean {
+        val oldShownTasks = shownTasks
+        orderedRunningTaskIds = updateOrderedRunningTaskIds()
         shownTasks =
             if (isInDesktopMode) {
                 computeShownRunningTasks()
             } else {
                 computeShownRecentTasks()
             }
+        val shownTasksChanged = oldShownTasks != shownTasks
+        if (!shownTasksChanged) {
+            return shownTasksChanged
+        }
 
         for (groupTask in shownTasks) {
             for (task in groupTask.tasks) {
-                val callback =
-                    Consumer<Task> { controllers.taskbarViewController.onTaskUpdated(it) }
-                val cancellableTask = recentsModel.iconCache.updateIconInBackground(task, callback)
+                val cancellableTask =
+                    recentsModel.iconCache.getIconInBackground(task) {
+                        icon,
+                        contentDescription,
+                        title ->
+                        task.icon = icon
+                        task.titleDescription = contentDescription
+                        task.title = title
+                        controllers.taskbarViewController.onTaskUpdated(task)
+                    }
                 if (cancellableTask != null) {
                     iconLoadRequests.add(cancellableTask)
                 }
             }
         }
+        return shownTasksChanged
+    }
+
+    private fun updateOrderedRunningTaskIds(): MutableList<Int> {
+        val desktopTaskAsList = getOrderedAndWrappedDesktopTasks()
+        val desktopTaskIds = desktopTaskAsList.map { it.task1.key.id }
+        var newOrder =
+            orderedRunningTaskIds
+                .filter { it in desktopTaskIds } // Only keep the tasks that are still running
+                .toMutableList()
+        // Add new tasks not already listed
+        newOrder.addAll(desktopTaskIds.filter { it !in newOrder })
+        return newOrder
     }
 
     private fun computeShownRunningTasks(): List<GroupTask> {
         if (!canShowRunningApps) {
             return emptyList()
         }
-        val tasks = desktopTask?.tasks ?: emptyList()
-        // Kind of hacky, we wrap each single task in the Desktop as a GroupTask.
-        var desktopTaskAsList = tasks.map { GroupTask(it) }
-        // TODO(b/315344726 Multi-instance support): dedupe Tasks of the same package too.
-        desktopTaskAsList = dedupeHotseatTasks(desktopTaskAsList, shownHotseatItems)
-        val desktopPackages = desktopTaskAsList.map { it.packageNames }
-        // Remove any missing Tasks.
-        val newShownTasks = shownTasks.filter { it.packageNames in desktopPackages }.toMutableList()
-        val newShownPackages = newShownTasks.map { it.packageNames }
+        val desktopTaskAsList = getOrderedAndWrappedDesktopTasks()
+        val desktopTaskIds = desktopTaskAsList.map { it.task1.key.id }
+        val shownTaskIds = shownTasks.map { it.task1.key.id }
+        // TODO(b/315344726 Multi-instance support): only show one icon per package once we support
+        //  taskbar multi-instance menus
+        val shownHotseatItemTaskIds =
+            shownHotseatItems.mapNotNull { it as? TaskItemInfo }.map { it.taskId }
+        // Remove any newly-missing Tasks, and actual group-tasks
+        val newShownTasks =
+            shownTasks
+                .filter { !it.hasMultipleTasks() }
+                .filter { it.task1.key.id in desktopTaskIds }
+                .toMutableList()
         // Add any new Tasks, maintaining the order from previous shownTasks.
-        newShownTasks.addAll(desktopTaskAsList.filter { it.packageNames !in newShownPackages })
-        return newShownTasks.toList()
+        newShownTasks.addAll(desktopTaskAsList.filter { it.task1.key.id !in shownTaskIds })
+        // Remove any tasks already covered by Hotseat icons
+        return newShownTasks.filter { it.task1.key.id !in shownHotseatItemTaskIds }
     }
 
     private fun computeShownRecentTasks(): List<GroupTask> {
@@ -220,6 +280,25 @@
         }
     }
 
+    /**
+     * Returns the hotseat items updated so that any item that points to a package with a running
+     * task also references that task.
+     */
+    private fun updateHotseatItemsFromRunningTasks(
+        groupTasks: List<GroupTask>,
+        shownHotseatItems: List<ItemInfo>
+    ): List<ItemInfo> =
+        shownHotseatItems.map { itemInfo ->
+            if (itemInfo is TaskItemInfo) {
+                itemInfo
+            } else {
+                val foundTask =
+                    groupTasks.find { task -> task.task1.key.packageName == itemInfo.targetPackage }
+                        ?: return@map itemInfo
+                TaskItemInfo(foundTask.task1.key.id, itemInfo as WorkspaceItemInfo)
+            }
+        }
+
     override fun dumpLogs(prefix: String, pw: PrintWriter) {
         pw.println("$prefix TaskbarRecentAppsController:")
         pw.println("$prefix\tcanShowRunningApps=$canShowRunningApps")
@@ -228,8 +307,8 @@
         pw.println("$prefix\tallRecentTasks=${allRecentTasks.map { it.packageNames }}")
         pw.println("$prefix\tdesktopTask=${desktopTask?.packageNames}")
         pw.println("$prefix\tshownTasks=${shownTasks.map { it.packageNames }}")
-        pw.println("$prefix\trunningTasks=$runningAppPackages")
-        pw.println("$prefix\tminimizedTasks=$minimizedAppPackages")
+        pw.println("$prefix\trunningTaskIds=$runningTaskIds")
+        pw.println("$prefix\tminimizedTaskIds=$minimizedTaskIds")
     }
 
     private val GroupTask.packageNames: List<String>
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
index fa2d907..267e19c 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
@@ -31,6 +31,7 @@
 import static com.android.launcher3.util.FlagDebugUtils.formatFlagChange;
 import static com.android.quickstep.util.SystemActionConstants.SYSTEM_ACTION_ID_TASKBAR;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DIALOG_SHOWING;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SHOWING;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SWITCHER_SHOWING;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE;
@@ -285,18 +286,6 @@
     }
 
     /**
-     * Show Taskbar upon receiving broadcast
-     */
-    public void showTaskbarFromBroadcast() {
-        // If user is in middle of taskbar education handle go to next step of education
-        if (mControllers.taskbarEduTooltipController.isBeforeTooltipFeaturesStep()) {
-            mControllers.taskbarEduTooltipController.hide();
-            mControllers.taskbarEduTooltipController.maybeShowFeaturesEdu();
-        }
-        updateAndAnimateTransientTaskbar(false);
-    }
-
-    /**
      * Initializes the controller
      */
     public void init(
@@ -1018,7 +1007,7 @@
         long startDelay = 0;
 
         updateStateForFlag(FLAG_STASHED_IN_APP_SYSUI, hasAnyFlag(systemUiStateFlags,
-                SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE));
+                SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE | SYSUI_STATE_DIALOG_SHOWING));
 
         boolean stashForBubbles = hasAnyFlag(FLAG_IN_OVERVIEW)
                 && hasAnyFlag(systemUiStateFlags, SYSUI_STATE_BUBBLES_EXPANDED)
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
index f24bc21..a2278ec 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
@@ -43,8 +43,8 @@
 import com.android.quickstep.util.GroupTask;
 import com.android.quickstep.util.TISBindHelper;
 import com.android.quickstep.views.RecentsView;
+import com.android.quickstep.views.TaskContainer;
 import com.android.quickstep.views.TaskView;
-import com.android.quickstep.views.TaskView.TaskContainer;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
 
@@ -270,7 +270,7 @@
                                     foundTask,
                                     taskContainer.getIconView().getDrawable(),
                                     taskContainer.getSnapshotView(),
-                                    taskContainer.getThumbnail(),
+                                    taskContainer.getSplitAnimationThumbnail(),
                                     null /* intent */,
                                     null /* user */,
                                     info);
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
index e59a016..527e3a3 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
@@ -63,6 +63,7 @@
 import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.TaskItemInfo;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.ItemInfoMatcher;
 import com.android.launcher3.util.LauncherBindableItemsContainer;
@@ -515,35 +516,38 @@
         return mTaskbarView.getTaskbarDividerView();
     }
 
-    /** Updates which icons are marked as running given the Set of currently running packages. */
-    public void updateIconViewsRunningStates(Set<String> runningPackages,
-            Set<String> minimizedPackages) {
+    /**
+     * Updates which icons are marked as running or minimized given the Sets of currently running
+     * and minimized tasks.
+     */
+    public void updateIconViewsRunningStates(Set<Integer> runningTaskIds,
+            Set<Integer> minimizedTaskIds) {
         for (View iconView : getIconViews()) {
             if (iconView instanceof BubbleTextView btv) {
                 btv.updateRunningState(
-                        getRunningAppState(btv, runningPackages, minimizedPackages));
+                        getRunningAppState(btv, runningTaskIds, minimizedTaskIds));
             }
         }
     }
 
     private BubbleTextView.RunningAppState getRunningAppState(
             BubbleTextView btv,
-            Set<String> runningPackages,
-            Set<String> minimizedPackages) {
+            Set<Integer> runningTaskIds,
+            Set<Integer> minimizedTaskIds) {
         Object tag = btv.getTag();
-        if (tag instanceof ItemInfo itemInfo) {
-            if (minimizedPackages.contains(itemInfo.getTargetPackage())) {
+        if (tag instanceof TaskItemInfo itemInfo) {
+            if (minimizedTaskIds.contains(itemInfo.getTaskId())) {
                 return BubbleTextView.RunningAppState.MINIMIZED;
             }
-            if (runningPackages.contains(itemInfo.getTargetPackage())) {
+            if (runningTaskIds.contains(itemInfo.getTaskId())) {
                 return BubbleTextView.RunningAppState.RUNNING;
             }
         }
         if (tag instanceof GroupTask groupTask && !groupTask.hasMultipleTasks()) {
-            if (minimizedPackages.contains(groupTask.task1.key.getPackageName())) {
+            if (minimizedTaskIds.contains(groupTask.task1.key.id)) {
                 return BubbleTextView.RunningAppState.MINIMIZED;
             }
-            if (runningPackages.contains(groupTask.task1.key.getPackageName())) {
+            if (runningTaskIds.contains(groupTask.task1.key.id)) {
                 return BubbleTextView.RunningAppState.RUNNING;
             }
         }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
index f6b1328..7426dc7 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
@@ -356,6 +356,13 @@
             }
         }
 
+        // if a bubble was updated upstream, but removed before the update was received, add it back
+        if (update.updatedBubble != null && !mBubbles.containsKey(update.updatedBubble.getKey())) {
+            mBubbles.put(update.updatedBubble.getKey(), update.updatedBubble);
+            mBubbleBarViewController.addBubble(
+                    update.updatedBubble, isExpanding, suppressAnimation);
+        }
+
         if (update.addedBubble != null && isCollapsed) {
             // If we're collapsed, the most recently added bubble will be selected.
             bubbleToSelect = update.addedBubble;
@@ -394,7 +401,8 @@
             BubbleBarBubble bb = mBubbles.get(update.updatedBubble.getKey());
             // If we're not stashed, we're visible so animate
             bb.getView().updateDotVisibility(!mBubbleStashController.isStashed() /* animate */);
-            mBubbleBarViewController.animateBubbleNotification(bb, /* isExpanding= */ false);
+            mBubbleBarViewController.animateBubbleNotification(
+                    bb, /* isExpanding= */ false, /* isUpdate= */ true);
         }
         if (update.bubbleKeysInOrder != null && !update.bubbleKeysInOrder.isEmpty()) {
             // Create the new list
@@ -445,6 +453,13 @@
         }
     }
 
+    /**
+     * Removes the given bubble from the backing list of bubbles after it was dismissed by the user.
+     */
+    public void onBubbleDismissed(BubbleView bubble) {
+        mBubbles.remove(bubble.getBubble().getKey());
+    }
+
     /** Tells WMShell to show the currently selected bubble. */
     public void showSelectedBubble() {
         if (getSelectedBubbleKey() != null) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
index 07481a2..fd989b1 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -46,9 +46,10 @@
 import com.android.launcher3.anim.SpringAnimationBuilder;
 import com.android.launcher3.taskbar.bubbles.animation.BubbleAnimator;
 import com.android.launcher3.util.DisplayController;
-import com.android.wm.shell.Flags;
 import com.android.wm.shell.common.bubbles.BubbleBarLocation;
 
+import java.io.PrintWriter;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.function.Consumer;
 
@@ -182,6 +183,8 @@
 
     @Nullable
     private BubbleView mDraggedBubbleView;
+    @Nullable
+    private BubbleView mDismissedByDragBubbleView;
     private float mAlphaDuringDrag = 1f;
 
     private Controller mController;
@@ -256,10 +259,6 @@
         if (!isIconSizeOrPaddingUpdated(newIconSize, newBubbleBarPadding)) {
             return;
         }
-        if (!Flags.animateBubbleSizeChange()) {
-            setIconSizeAndPadding(newIconSize, newBubbleBarPadding);
-            return;
-        }
         if (mScalePaddingAnimator != null && mScalePaddingAnimator.isRunning()) {
             mScalePaddingAnimator.cancel();
         }
@@ -767,6 +766,10 @@
     public void removeBubble(View bubble) {
         if (isExpanded()) {
             // TODO b/347062801 - animate the bubble bar if the last bubble is removed
+            final boolean dismissedByDrag = mDraggedBubbleView == bubble;
+            if (dismissedByDrag) {
+                mDismissedByDragBubbleView = mDraggedBubbleView;
+            }
             int bubbleCount = getChildCount();
             mBubbleAnimator = new BubbleAnimator(mIconSize, mExpandedBarIconsSpacing,
                     bubbleCount, mBubbleBarLocation.isOnLeft(isLayoutRtl()));
@@ -786,8 +789,11 @@
 
                 @Override
                 public void onAnimationUpdate(float animatedFraction) {
-                    bubble.setScaleX(1 - animatedFraction);
-                    bubble.setScaleY(1 - animatedFraction);
+                    // don't update the scale if this bubble was dismissed by drag
+                    if (!dismissedByDrag) {
+                        bubble.setScaleX(1 - animatedFraction);
+                        bubble.setScaleY(1 - animatedFraction);
+                    }
                     updateBubblesLayoutProperties(mBubbleBarLocation);
                     invalidate();
                 }
@@ -818,6 +824,7 @@
         updateWidth();
         updateBubbleAccessibilityStates();
         updateContentDescription();
+        mDismissedByDragBubbleView = null;
     }
 
     private void updateWidth() {
@@ -864,7 +871,7 @@
         float elevationState = (1 - widthState);
         for (int i = 0; i < bubbleCount; i++) {
             BubbleView bv = (BubbleView) getChildAt(i);
-            if (bv == mDraggedBubbleView) {
+            if (bv == mDraggedBubbleView || bv == mDismissedByDragBubbleView) {
                 // Skip the dragged bubble. Its translation is managed by the drag controller.
                 continue;
             }
@@ -889,22 +896,29 @@
             float fullElevationForChild = (MAX_BUBBLES * mBubbleElevation) - i;
             bv.setZ(fullElevationForChild * elevationState);
 
+            // only update the dot scale if we're expanding or collapsing
+            // TODO b/351904597: update the dot for the first bubble after removal and reorder
+            // since those might happen when the bar is collapsed and will need their dot back
+            if (mWidthAnimator.isRunning()) {
+                bv.setDotScale(widthState);
+            }
+
             if (mIsBarExpanded) {
                 // If bar is on the right, account for bubble bar expanding and shifting left
                 final float expandedBarShift = onLeft ? 0 : currentWidth - expandedWidth;
                 // where the bubble will end up when the animation ends
                 final float targetX = expandedX + expandedBarShift;
                 bv.setTranslationX(widthState * (targetX - collapsedX) + collapsedX);
-                // When we're expanded, we're not stacked so we're not behind the stack
-                bv.setBehindStack(false, animate);
+                // When we're expanded, the badge is visible for all bubbles
+                bv.updateBadgeVisibility(/* show= */ true);
                 bv.setAlpha(1);
             } else {
                 // If bar is on the right, account for bubble bar expanding and shifting left
                 final float collapsedBarShift = onLeft ? 0 : currentWidth - collapsedWidth;
                 final float targetX = collapsedX + collapsedBarShift;
                 bv.setTranslationX(widthState * (expandedX - targetX) + targetX);
-                // If we're not the first bubble we're behind the stack
-                bv.setBehindStack(i > 0, animate);
+                // The badge is always visible for the first bubble
+                bv.updateBadgeVisibility(/* show= */ i == 0);
                 // If we're fully collapsed, hide all bubbles except for the first 2. If there are
                 // only 2 bubbles, hide the second bubble as well because it's the overflow.
                 if (widthState == 0) {
@@ -967,8 +981,7 @@
         return translationX - getScaleIconShift();
     }
 
-    private float getCollapsedBubbleTranslationX(int bubbleIndex, int bubbleCount,
-            boolean onLeft) {
+    private float getCollapsedBubbleTranslationX(int bubbleIndex, int bubbleCount, boolean onLeft) {
         if (bubbleIndex < 0 || bubbleIndex >= bubbleCount) {
             return 0;
         }
@@ -979,7 +992,9 @@
                     bubbleIndex == 0 && bubbleCount > MAX_VISIBLE_BUBBLES_COLLAPSED
                             ? mIconOverlapAmount : 0);
         } else {
-            translationX = mBubbleBarPadding + (bubbleIndex == 0 ? 0 : mIconOverlapAmount);
+            translationX = mBubbleBarPadding + (
+                    bubbleIndex == 0 || bubbleCount <= MAX_VISIBLE_BUBBLES_COLLAPSED
+                            ? 0 : mIconOverlapAmount);
         }
         return translationX - getScaleIconShift();
     }
@@ -1046,6 +1061,8 @@
         mDraggedBubbleView = view;
         if (view != null) {
             view.setZ(mDragElevation);
+            // we started dragging a bubble. reset the bubble that was previously dismissed by drag
+            mDismissedByDragBubbleView = null;
         }
         setIsDragging(view != null);
     }
@@ -1295,6 +1312,37 @@
         });
     }
 
+    /** Dumps the current state of BubbleBarView. */
+    public void dump(PrintWriter pw) {
+        pw.println("BubbleBarView state:");
+        pw.println("  visibility: " + getVisibility());
+        pw.println("  translation Y: " + getTranslationY());
+        pw.println("  bubbles in bar (childCount = " + getChildCount() + ")");
+        for (BubbleView bubbleView: getBubbles()) {
+            BubbleBarItem bubble = bubbleView.getBubble();
+            String key = bubble == null ? "null" : bubble.getKey();
+            pw.println("    bubble key: " + key);
+        }
+        pw.println("  isExpanded: " + isExpanded());
+        pw.println("  mIsAnimatingNewBubble: " + mIsAnimatingNewBubble);
+        if (mBubbleAnimator != null) {
+            pw.println("  mBubbleAnimator.isRunning(): " + mBubbleAnimator.isRunning());
+            pw.println("  mBubbleAnimator is null");
+        }
+        pw.println("  mDragging: " + mDragging);
+    }
+
+    private List<BubbleView> getBubbles() {
+        List<BubbleView> bubbles = new ArrayList<>();
+        for (int i = 0; i < getChildCount(); i++) {
+            View child = getChildAt(i);
+            if (child instanceof BubbleView bubble) {
+                bubbles.add(bubble);
+            }
+        }
+        return bubbles;
+    }
+
     /** Interface for BubbleBarView to communicate with its controller. */
     interface Controller {
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index dbc78db..24b9139 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -43,6 +43,7 @@
 import com.android.quickstep.SystemUiProxy;
 import com.android.wm.shell.common.bubbles.BubbleBarLocation;
 
+import java.io.PrintWriter;
 import java.util.List;
 import java.util.Objects;
 import java.util.function.Consumer;
@@ -88,6 +89,8 @@
 
     private BubbleBarViewAnimator mBubbleBarViewAnimator;
 
+    private TimeSource mTimeSource = System::currentTimeMillis;
+
     @Nullable
     private BubbleBarBoundsChangeListener mBoundsChangeListener;
 
@@ -399,7 +402,7 @@
         addedBubble.getView().setOnClickListener(mBubbleClickListener);
         mBubbleDragController.setupBubbleView(addedBubble.getView());
         if (!suppressAnimation) {
-            animateBubbleNotification(addedBubble, isExpanding);
+            animateBubbleNotification(addedBubble, isExpanding, /* isUpdate= */ false);
         }
     }
 
@@ -427,18 +430,19 @@
                 }
                 return;
             }
-            animateBubbleNotification(bubble, isExpanding);
+            animateBubbleNotification(bubble, isExpanding, /* isUpdate= */ false);
         } else {
             Log.w(TAG, "addBubble, bubble was null!");
         }
     }
 
     /** Animates the bubble bar to notify the user about a bubble change. */
-    public void animateBubbleNotification(BubbleBarBubble bubble, boolean isExpanding) {
+    public void animateBubbleNotification(BubbleBarBubble bubble, boolean isExpanding,
+            boolean isUpdate) {
         boolean isInApp = mTaskbarStashController.isInApp();
         // if this is the first bubble, animate to the initial state. one bubble is the overflow
         // so check for at most 2 children.
-        if (mBarView.getChildCount() <= 2) {
+        if (mBarView.getChildCount() <= 2 && !isUpdate) {
             mBubbleBarViewAnimator.animateToInitialState(bubble, isInApp, isExpanding);
             return;
         }
@@ -526,6 +530,12 @@
         mSystemUiProxy.stopBubbleDrag(location, mBarView.getRestingTopPositionOnScreen());
     }
 
+    /** Notifies {@link BubbleBarView} that the dragged bubble was dismissed. */
+    public void onBubbleDragDismissed(BubbleView bubble) {
+        mBubbleBarController.onBubbleDismissed(bubble);
+        mBarView.removeBubble(bubble);
+    }
+
     /**
      * Notifies {@link BubbleBarView} that drag and all animations are finished.
      */
@@ -568,7 +578,7 @@
      * @param bubble dismissed bubble item
      */
     public void onDismissBubbleWhileDragging(@NonNull BubbleBarItem bubble) {
-        mSystemUiProxy.dragBubbleToDismiss(bubble.getKey());
+        mSystemUiProxy.dragBubbleToDismiss(bubble.getKey(), mTimeSource.currentTimeMillis());
     }
 
     /**
@@ -592,4 +602,24 @@
         /** Called when bounds have changed */
         void onBoundsChanged();
     }
+
+    /** Interface for getting the current timestamp. */
+    interface TimeSource {
+        long currentTimeMillis();
+    }
+
+    /** Dumps the state of BubbleBarViewController. */
+    public void dump(PrintWriter pw) {
+        pw.println("Bubble bar view controller state:");
+        pw.println("  mHiddenForSysui: " + mHiddenForSysui);
+        pw.println("  mHiddenForNoBubbles: " + mHiddenForNoBubbles);
+        pw.println("  mShouldShowEducation: " + mShouldShowEducation);
+        pw.println("  mBubbleBarTranslationY.value: " + mBubbleBarTranslationY.value);
+        pw.println("  mBubbleBarSwipeUpTranslationY: " + mBubbleBarSwipeUpTranslationY);
+        if (mBarView != null) {
+            mBarView.dump(pw);
+        } else {
+            pw.println("  Bubble bar view is null!");
+        }
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java
index 32d6375..03140fe 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java
@@ -18,6 +18,8 @@
 import com.android.launcher3.taskbar.TaskbarControllers;
 import com.android.launcher3.util.RunnableList;
 
+import java.io.PrintWriter;
+
 /**
  * Hosts various bubble controllers to facilitate passing between one another.
  */
@@ -94,4 +96,9 @@
         bubbleStashedHandleViewController.onDestroy();
         bubbleBarController.onDestroy();
     }
+
+    /** Dumps bubble controllers state. */
+    public void dump(PrintWriter pw) {
+        bubbleBarViewController.dump(pw);
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java
index efc747c..8316b5b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java
@@ -153,6 +153,7 @@
             @Override
             protected void onDragDismiss() {
                 mBubblePinController.onDragEnd();
+                mBubbleBarViewController.onBubbleDragDismissed(bubbleView);
                 mBubbleBarViewController.onBubbleDragEnd();
             }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
index 185f85f..74f58ac 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
@@ -36,6 +36,8 @@
 import com.android.wm.shell.common.bubbles.BubbleBarLocation;
 import com.android.wm.shell.shared.animation.PhysicsAnimator;
 
+import java.io.PrintWriter;
+
 /**
  * Coordinates between controllers such as BubbleBarView and BubbleHandleViewController to
  * create a cohesive animation between stashed/unstashed states.
@@ -456,4 +458,13 @@
     public void setHandleTranslationY(float ty) {
         mHandleViewController.setTranslationYForSwipe(ty);
     }
+
+    /** Dumps the state of BubbleStashController. */
+    public void dump(PrintWriter pw) {
+        pw.println("Bubble stash controller state:");
+        pw.println("  mIsStashed: " + mIsStashed);
+        pw.println("  mBubblesShowingOnOverview: " + mBubblesShowingOnOverview);
+        pw.println("  mBubblesShowingOnHome: " + mBubblesShowingOnHome);
+        pw.println("  mIsSysuiLocked: " + mIsSysuiLocked);
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
index 2f92fbb..4c468bb 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
@@ -20,6 +20,7 @@
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
 import android.graphics.Outline;
+import android.graphics.Path;
 import android.graphics.Rect;
 import android.text.TextUtils;
 import android.util.AttributeSet;
@@ -35,8 +36,6 @@
 import com.android.launcher3.icons.IconNormalizer;
 import com.android.wm.shell.animation.Interpolators;
 
-import java.util.EnumSet;
-
 // TODO: (b/276978250) This is will be similar to WMShell's BadgedImageView, it'd be nice to share.
 
 /**
@@ -47,25 +46,9 @@
 
     public static final int DEFAULT_PATH_SIZE = 100;
 
-    /**
-     * Flags that suppress the visibility of the 'new' dot or the app badge, for one reason or
-     * another. If any of these flags are set, the dot will not be shown.
-     * If {@link SuppressionFlag#BEHIND_STACK} then the app badge will not be shown.
-     */
-    enum SuppressionFlag {
-        // TODO: (b/277815200) implement flyout
-        // Suppressed because the flyout is visible - it will morph into the dot via animation.
-        FLYOUT_VISIBLE,
-        // Suppressed because this bubble is behind others in the collapsed stack.
-        BEHIND_STACK,
-    }
-
-    private final EnumSet<SuppressionFlag> mSuppressionFlags =
-            EnumSet.noneOf(SuppressionFlag.class);
-
     private final ImageView mBubbleIcon;
     private final ImageView mAppIcon;
-    private final int mBubbleSize;
+    private int mBubbleSize;
 
     private float mDragTranslationX;
     private float mOffsetX;
@@ -107,8 +90,6 @@
         setLayoutDirection(LAYOUT_DIRECTION_LTR);
 
         LayoutInflater.from(context).inflate(R.layout.bubble_view, this);
-
-        mBubbleSize = getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_size);
         mBubbleIcon = findViewById(R.id.icon_view);
         mAppIcon = findViewById(R.id.app_icon_view);
 
@@ -125,11 +106,21 @@
     }
 
     private void getOutline(Outline outline) {
+        updateBubbleSizeAndDotRender();
         final int normalizedSize = IconNormalizer.getNormalizedCircleSize(mBubbleSize);
         final int inset = (mBubbleSize - normalizedSize) / 2;
         outline.setOval(inset, inset, inset + normalizedSize, inset + normalizedSize);
     }
 
+    private void updateBubbleSizeAndDotRender() {
+        int updatedBubbleSize = Math.min(getWidth(), getHeight());
+        if (updatedBubbleSize == mBubbleSize) return;
+        mBubbleSize = updatedBubbleSize;
+        if (mBubble == null || mBubble instanceof BubbleBarOverflow) return;
+        Path dotPath = ((BubbleBarBubble) mBubble).getDotPath();
+        mDotRenderer = new DotRenderer(mBubbleSize, dotPath, DEFAULT_PATH_SIZE);
+    }
+
     /**
      * Set translation-x while this bubble is being dragged.
      * Translation applied to the view is a sum of {@code translationX} and offset defined by
@@ -159,6 +150,12 @@
         applyDragTranslation();
     }
 
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+        updateBubbleSizeAndDotRender();
+    }
+
     private void applyDragTranslation() {
         setTranslationX(mDragTranslationX + mOffsetX);
     }
@@ -230,7 +227,7 @@
         }
     }
 
-    void updateBadgeVisibility() {
+    void updateBadgeVisibility(boolean show) {
         if (mBubble instanceof BubbleBarOverflow) {
             // The overflow bubble does not have a badge, so just bail.
             return;
@@ -241,39 +238,24 @@
                 ? -(bubble.getIcon().getWidth() - appBadgeBitmap.getWidth())
                 : 0;
         mAppIcon.setTranslationX(translationX);
-        mAppIcon.setVisibility(isBehindStack() ? GONE : VISIBLE);
-    }
-
-    /** Sets whether this bubble is in the stack & not the first bubble. **/
-    void setBehindStack(boolean behindStack, boolean animate) {
-        if (behindStack) {
-            mSuppressionFlags.add(SuppressionFlag.BEHIND_STACK);
-        } else {
-            mSuppressionFlags.remove(SuppressionFlag.BEHIND_STACK);
-        }
-        updateDotVisibility(animate);
-        updateBadgeVisibility();
-    }
-
-    /** Whether this bubble is in the stack & not the first bubble. **/
-    boolean isBehindStack() {
-        return mSuppressionFlags.contains(SuppressionFlag.BEHIND_STACK);
+        mAppIcon.setVisibility(show ? VISIBLE : GONE);
     }
 
     /** Whether the dot indicating unseen content in a bubble should be shown. */
     private boolean shouldDrawDot() {
         boolean bubbleHasUnseenContent = mBubble != null
                 && mBubble instanceof BubbleBarBubble
-                && mSuppressionFlags.isEmpty()
                 && !((BubbleBarBubble) mBubble).getInfo().isNotificationSuppressed();
-
         // Always render the dot if it's animating, since it could be animating out. Otherwise, show
         // it if the bubble wants to show it, and we aren't suppressing it.
         return bubbleHasUnseenContent || mDotIsAnimating;
     }
 
     /** How big the dot should be, fraction from 0 to 1. */
-    private void setDotScale(float fraction) {
+    void setDotScale(float fraction) {
+        if (!shouldDrawDot()) {
+            return;
+        }
         mDotScale = fraction;
         invalidate();
     }
@@ -283,14 +265,14 @@
      */
     private void animateDotScale() {
         float toScale = shouldDrawDot() ? 1f : 0f;
-        mDotIsAnimating = true;
+        boolean isDotScaleChanging = Float.compare(mDotScale, toScale) != 0;
 
-        // Don't restart the animation if we're already animating to the given value.
-        if (mAnimatingToDotScale == toScale || !shouldDrawDot()) {
-            mDotIsAnimating = false;
+        // Don't restart the animation if we're already animating to the given value or if the dot
+        // scale is not changing
+        if ((mDotIsAnimating && mAnimatingToDotScale == toScale) || !isDotScaleChanging) {
             return;
         }
-
+        mDotIsAnimating = true;
         mAnimatingToDotScale = toScale;
 
         final boolean showDot = toScale > 0f;
diff --git a/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt b/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt
index dc6365b..181cba0 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt
+++ b/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt
@@ -79,6 +79,7 @@
 import com.android.launcher3.util.OnboardingPrefs.HOTSEAT_DISCOVERY_TIP_COUNT
 import com.android.launcher3.util.OnboardingPrefs.HOTSEAT_LONGPRESS_TIP_SEEN
 import com.android.launcher3.util.OnboardingPrefs.TASKBAR_EDU_TOOLTIP_STEP
+import com.android.launcher3.util.OnboardingPrefs.TASKBAR_SEARCH_EDU_SEEN
 import com.android.launcher3.util.PluginManagerWrapper
 import com.android.launcher3.util.StartActivityParams
 import com.android.launcher3.util.UserIconInfo
@@ -394,6 +395,7 @@
                 HOTSEAT_LONGPRESS_TIP_SEEN.sharedPrefKey
             )
             addOnboardPref("Taskbar Education", TASKBAR_EDU_TOOLTIP_STEP.sharedPrefKey)
+            addOnboardPref("Taskbar Search Education", TASKBAR_SEARCH_EDU_SEEN.sharedPrefKey)
             addOnboardPref("All Apps Visited Count", ALL_APPS_VISITED_COUNT.sharedPrefKey)
         }
     }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java b/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java
index 0368f3a..3a39cf2 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java
@@ -110,6 +110,7 @@
             // taskbar icons disappearing before hotseat icons show up.
             float scrimUpperBoundFromSplit =
                     QuickstepTransitionManager.getTaskbarToHomeDuration() / (float) config.duration;
+            scrimUpperBoundFromSplit = Math.min(scrimUpperBoundFromSplit, 1f);
             config.setInterpolator(ANIM_OVERVIEW_ACTIONS_FADE, clampToProgress(LINEAR, 0, 0.25f));
             config.setInterpolator(ANIM_SCRIM_FADE,
                     fromState == OVERVIEW_SPLIT_SELECT
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java
index 93e4fbd..31e4e33 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java
@@ -151,7 +151,7 @@
             int sysuiFlags = 0;
             TaskView tv = mOverviewPanel.getTaskViewAt(0);
             if (tv != null) {
-                sysuiFlags = tv.getFirstThumbnailViewDeprecated().getSysUiStatusNavFlags();
+                sysuiFlags = tv.getTaskContainers().getFirst().getSysUiStatusNavFlags();
             }
             mLauncher.getSystemUiController().updateUiState(UI_STATE_FULLSCREEN_TASK, sysuiFlags);
         } else {
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index fb2a982..f020c8f 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -138,8 +138,8 @@
 import com.android.quickstep.views.DesktopTaskView;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.RecentsViewContainer;
+import com.android.quickstep.views.TaskContainer;
 import com.android.quickstep.views.TaskView;
-import com.android.quickstep.views.TaskView.TaskContainer;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
@@ -152,8 +152,6 @@
 import com.android.wm.shell.common.TransactionPool;
 import com.android.wm.shell.startingsurface.SplashScreenExitAnimationUtils;
 
-import kotlin.Unit;
-
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
@@ -163,6 +161,8 @@
 import java.util.OptionalInt;
 import java.util.function.Consumer;
 
+import kotlin.Unit;
+
 /**
  * Handles the navigation gestures when Launcher is the default home activity.
  */
@@ -926,7 +926,7 @@
             TaskView runningTask = mRecentsView.getRunningTaskView();
             TaskView centermostTask = mRecentsView.getTaskViewNearestToCenterOfScreen();
             int centermostTaskFlags = centermostTask == null ? 0
-                    : centermostTask.getFirstThumbnailViewDeprecated().getSysUiStatusNavFlags();
+                    : centermostTask.getTaskContainers().getFirst().getSysUiStatusNavFlags();
             boolean swipeUpThresholdPassed = windowProgress > 1 - UPDATE_SYSUI_FLAGS_THRESHOLD;
             boolean quickswitchThresholdPassed = centermostTask != runningTask;
 
@@ -2423,58 +2423,65 @@
         if (mRecentsAnimationController == null) {
             return;
         }
+        final Runnable onFinishComplete = () -> {
+            ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                    "AbsSwipeUpHandler.onTasksAppeared: ")
+                    .append("force finish recents animation complete; clearing state callback."));
+            mStateCallback.setStateOnUiThread(STATE_GESTURE_CANCELLED | STATE_HANDLER_INVALIDATED);
+        };
+        ActiveGestureLog.CompoundString forceFinishReason = new ActiveGestureLog.CompoundString(
+                "Forcefully finishing recents animation: ");
         if (!mStateCallback.hasStates(STATE_GESTURE_COMPLETED)
                 && !hasStartedTaskBefore(appearedTaskTargets)) {
             // This is a special case, if a task is started mid-gesture that wasn't a part of a
             // previous quickswitch task launch, then cancel the animation back to the app
             RemoteAnimationTarget appearedTaskTarget = appearedTaskTargets[0];
             TaskInfo taskInfo = appearedTaskTarget.taskInfo;
-            ActiveGestureLog.INSTANCE.addLog(
-                    new ActiveGestureLog.CompoundString("Unexpected task appeared")
-                            .append(" id=")
+            ActiveGestureLog.INSTANCE.addLog(forceFinishReason
+                            .append("Unexpected task appeared id=")
                             .append(taskInfo.taskId)
                             .append(" pkg=")
                             .append(taskInfo.baseIntent.getComponent().getPackageName()));
-            finishRecentsAnimationOnTasksAppeared(null /* onFinishComplete */);
+            finishRecentsAnimationOnTasksAppeared(onFinishComplete);
             return;
         }
         ActiveGestureLog.CompoundString handleTaskFailureReason =
                 new ActiveGestureLog.CompoundString("handleTaskAppeared check failed: ");
         if (!handleTaskAppeared(appearedTaskTargets, handleTaskFailureReason)) {
-            ActiveGestureLog.INSTANCE.addLog(handleTaskFailureReason);
-            finishRecentsAnimationOnTasksAppeared(null /* onFinishComplete */);
+            ActiveGestureLog.INSTANCE.addLog(forceFinishReason.append(handleTaskFailureReason));
+            finishRecentsAnimationOnTasksAppeared(onFinishComplete);
             return;
         }
-        Optional<RemoteAnimationTarget> taskTargetOptional =
-                Arrays.stream(appearedTaskTargets)
-                        .filter(mGestureState.mLastStartedTaskIdPredicate)
-                        .findFirst();
-        if (!taskTargetOptional.isPresent()) {
-            ActiveGestureLog.INSTANCE.addLog("No appeared task matching started task id");
-            finishRecentsAnimationOnTasksAppeared(null /* onFinishComplete */);
+        RemoteAnimationTarget[] taskTargets = Arrays.stream(appearedTaskTargets)
+                .filter(mGestureState.mLastStartedTaskIdPredicate)
+                .toArray(RemoteAnimationTarget[]::new);
+        if (taskTargets.length == 0) {
+            ActiveGestureLog.INSTANCE.addLog(
+                    forceFinishReason.append("No appeared task matching started task id"));
+            finishRecentsAnimationOnTasksAppeared(onFinishComplete);
             return;
         }
-        RemoteAnimationTarget taskTarget = taskTargetOptional.get();
+        RemoteAnimationTarget taskTarget = taskTargets[0];
         TaskView taskView = mRecentsView == null
                 ? null : mRecentsView.getTaskViewByTaskId(taskTarget.taskId);
-        if (taskView == null
-                || !taskView.getFirstThumbnailViewDeprecated().shouldShowSplashView()) {
-            ActiveGestureLog.INSTANCE.addLog("Invalid task view splash state");
-            finishRecentsAnimationOnTasksAppeared(null /* onFinishComplete */);
+        if (taskView == null || taskView.getTaskContainers().stream().noneMatch(
+                TaskContainer::getShouldShowSplashView)) {
+            ActiveGestureLog.INSTANCE.addLog(forceFinishReason.append("Splash not needed"));
+            finishRecentsAnimationOnTasksAppeared(onFinishComplete);
             return;
         }
         if (mContainer == null) {
-            ActiveGestureLog.INSTANCE.addLog("Activity destroyed");
-            finishRecentsAnimationOnTasksAppeared(null /* onFinishComplete */);
+            ActiveGestureLog.INSTANCE.addLog(forceFinishReason.append("Activity destroyed"));
+            finishRecentsAnimationOnTasksAppeared(onFinishComplete);
             return;
         }
-        animateSplashScreenExit(mContainer, appearedTaskTargets, taskTarget.leash);
+        animateSplashScreenExit(mContainer, appearedTaskTargets, taskTargets);
     }
 
     private void animateSplashScreenExit(
             @NonNull T activity,
             @NonNull RemoteAnimationTarget[] appearedTaskTargets,
-            @NonNull SurfaceControl leash) {
+            @NonNull RemoteAnimationTarget[] animatingTargets) {
         ViewGroup splashView = activity.getDragLayer();
         final QuickstepLauncher quickstepLauncher = activity instanceof QuickstepLauncher
                 ? (QuickstepLauncher) activity : null;
@@ -2492,26 +2499,28 @@
         }
         surfaceApplier.scheduleApply(transaction);
 
-        SplashScreenExitAnimationUtils.startAnimations(splashView, leash,
-                mSplashMainWindowShiftLength, new TransactionPool(), new Rect(),
-                SPLASH_ANIMATION_DURATION, SPLASH_FADE_OUT_DURATION,
-                /* iconStartAlpha= */ 0, /* brandingStartAlpha= */ 0,
-                SPLASH_APP_REVEAL_DELAY, SPLASH_APP_REVEAL_DURATION,
-                new AnimatorListenerAdapter() {
-                    @Override
-                    public void onAnimationEnd(Animator animation) {
-                        // Hiding launcher which shows the app surface behind, then
-                        // finishing recents to the app. After transition finish, showing
-                        // the views on launcher again, so it can be visible when next
-                        // animation starts.
-                        splashView.setAlpha(0);
-                        if (quickstepLauncher != null) {
-                            quickstepLauncher.getDepthController()
-                                    .pauseBlursOnWindows(false);
+        for (RemoteAnimationTarget target : animatingTargets) {
+            SplashScreenExitAnimationUtils.startAnimations(splashView, target.leash,
+                    mSplashMainWindowShiftLength, new TransactionPool(), target.screenSpaceBounds,
+                    SPLASH_ANIMATION_DURATION, SPLASH_FADE_OUT_DURATION,
+                    /* iconStartAlpha= */ 0, /* brandingStartAlpha= */ 0,
+                    SPLASH_APP_REVEAL_DELAY, SPLASH_APP_REVEAL_DURATION,
+                    new AnimatorListenerAdapter() {
+                        @Override
+                        public void onAnimationEnd(Animator animation) {
+                            // Hiding launcher which shows the app surface behind, then
+                            // finishing recents to the app. After transition finish, showing
+                            // the views on launcher again, so it can be visible when next
+                            // animation starts.
+                            splashView.setAlpha(0);
+                            if (quickstepLauncher != null) {
+                                quickstepLauncher.getDepthController()
+                                        .pauseBlursOnWindows(false);
+                            }
+                            finishRecentsAnimationOnTasksAppeared(() -> splashView.setAlpha(1));
                         }
-                        finishRecentsAnimationOnTasksAppeared(() -> splashView.setAlpha(1));
-                    }
-                });
+                    });
+        }
     }
 
     private void finishRecentsAnimationOnTasksAppeared(Runnable onFinishComplete) {
diff --git a/quickstep/src/com/android/quickstep/BaseContainerInterface.java b/quickstep/src/com/android/quickstep/BaseContainerInterface.java
index b1d511c..3a8c141 100644
--- a/quickstep/src/com/android/quickstep/BaseContainerInterface.java
+++ b/quickstep/src/com/android/quickstep/BaseContainerInterface.java
@@ -42,6 +42,7 @@
 import com.android.launcher3.statemanager.BaseState;
 import com.android.launcher3.taskbar.TaskbarUIController;
 import com.android.launcher3.util.DisplayController;
+import com.android.launcher3.util.WindowBounds;
 import com.android.launcher3.views.ScrimView;
 import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
 import com.android.quickstep.util.ActivityInitListener;
@@ -51,6 +52,7 @@
 import com.android.systemui.shared.recents.model.ThumbnailData;
 
 import java.util.HashMap;
+import java.util.List;
 import java.util.function.Consumer;
 import java.util.function.Predicate;
 
@@ -269,8 +271,11 @@
         } else {
             Rect portraitInsets = dp.getInsets();
             DisplayController displayController = DisplayController.INSTANCE.get(context);
-            Rect deviceRotationInsets = displayController.getInfo().getCurrentBounds().get(
-                    orientationHandler.getRotation()).insets;
+            @Nullable List<WindowBounds> windowBounds =
+                    displayController.getInfo().getCurrentBounds();
+            Rect deviceRotationInsets = windowBounds != null
+                    ? windowBounds.get(orientationHandler.getRotation()).insets
+                    : new Rect();
             // Obtain the landscape/seascape insets, and rotate it to portrait perspective.
             orientationHandler.rotateInsets(deviceRotationInsets, outRect);
             // Then combine with portrait's insets to leave space for status bar/nav bar in
diff --git a/quickstep/src/com/android/quickstep/DesktopSystemShortcut.kt b/quickstep/src/com/android/quickstep/DesktopSystemShortcut.kt
index 9c188f3..358f644 100644
--- a/quickstep/src/com/android/quickstep/DesktopSystemShortcut.kt
+++ b/quickstep/src/com/android/quickstep/DesktopSystemShortcut.kt
@@ -23,9 +23,9 @@
 import com.android.launcher3.popup.SystemShortcut
 import com.android.quickstep.views.RecentsView
 import com.android.quickstep.views.RecentsViewContainer
-import com.android.quickstep.views.TaskView.TaskContainer
+import com.android.quickstep.views.TaskContainer
 import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource
-import com.android.wm.shell.shared.DesktopModeStatus
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
 
 /** A menu item, "Desktop", that allows the user to bring the current app into Desktop Windowing. */
 class DesktopSystemShortcut(
diff --git a/quickstep/src/com/android/quickstep/LauncherActivityInterface.java b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
index 1048ea1..b564fa7 100644
--- a/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
@@ -212,6 +212,9 @@
         if (launcher.isStarted() && (isInLiveTileMode() || launcher.hasBeenResumed())) {
             return launcher;
         }
+        if (isInMinusOne()) {
+            return launcher;
+        }
 
         return null;
     }
@@ -289,6 +292,15 @@
                 && TopTaskTracker.INSTANCE.get(launcher).getCachedTopTask(false).isHomeTask();
     }
 
+    private boolean isInMinusOne() {
+        QuickstepLauncher launcher = getCreatedContainer();
+
+        return launcher != null
+                && launcher.getStateManager().getState() == NORMAL
+                && !launcher.isStarted()
+                && TopTaskTracker.INSTANCE.get(launcher).getCachedTopTask(false).isHomeTask();
+    }
+
     @Override
     public void onLaunchTaskFailed() {
         QuickstepLauncher launcher = getCreatedContainer();
diff --git a/quickstep/src/com/android/quickstep/OrientationRectF.java b/quickstep/src/com/android/quickstep/OrientationRectF.java
index aa01b05..2b7ecb2 100644
--- a/quickstep/src/com/android/quickstep/OrientationRectF.java
+++ b/quickstep/src/com/android/quickstep/OrientationRectF.java
@@ -67,13 +67,15 @@
     }
 
     public boolean applyTransform(MotionEvent event, int deltaRotation, boolean forceTransform) {
+        if (deltaRotation == 0) {
+            return contains(event.getX(), event.getY());
+        }
         mTmpMatrix.reset();
         postDisplayRotation(deltaRotation, mHeight, mWidth, mTmpMatrix);
         if (forceTransform) {
             if (DEBUG) {
                 Log.d(TAG, "Transforming rotation due to forceTransform, "
                         + "deltaRotation: " + deltaRotation
-                        + "mRotation: " + mRotation
                         + " this: " + this);
             }
             event.applyTransform(mTmpMatrix);
diff --git a/quickstep/src/com/android/quickstep/OverviewComponentObserver.java b/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
index 9c64576..d82426f 100644
--- a/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
+++ b/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
@@ -21,6 +21,7 @@
 import static android.content.Intent.ACTION_PACKAGE_REMOVED;
 
 import static com.android.launcher3.config.FeatureFlags.SEPARATE_RECENTS_ACTIVITY;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.systemui.shared.system.PackageManagerWrapper.ACTION_PREFERRED_ACTIVITY_CHANGED;
 
 import android.content.ActivityNotFoundException;
@@ -55,10 +56,11 @@
 public final class OverviewComponentObserver {
     private static final String TAG = "OverviewComponentObserver";
 
+    // We register broadcast receivers on main thread to avoid missing updates.
     private final SimpleBroadcastReceiver mUserPreferenceChangeReceiver =
-            new SimpleBroadcastReceiver(this::updateOverviewTargets);
+            new SimpleBroadcastReceiver(MAIN_EXECUTOR, this::updateOverviewTargets);
     private final SimpleBroadcastReceiver mOtherHomeAppUpdateReceiver =
-            new SimpleBroadcastReceiver(this::updateOverviewTargets);
+            new SimpleBroadcastReceiver(MAIN_EXECUTOR, this::updateOverviewTargets);
 
     private final Context mContext;
     private final RecentsAnimationDeviceState mDeviceState;
@@ -102,7 +104,7 @@
             mConfigChangesMap.append(fallbackComponent.hashCode(), fallbackInfo.configChanges);
         } catch (PackageManager.NameNotFoundException ignored) { /* Impossible */ }
 
-        mUserPreferenceChangeReceiver.registerAsync(mContext, ACTION_PREFERRED_ACTIVITY_CHANGED);
+        mUserPreferenceChangeReceiver.register(mContext, ACTION_PREFERRED_ACTIVITY_CHANGED);
         updateOverviewTargets();
     }
 
@@ -191,7 +193,7 @@
                 unregisterOtherHomeAppUpdateReceiver();
 
                 mUpdateRegisteredPackage = defaultHome.getPackageName();
-                mOtherHomeAppUpdateReceiver.registerPkgActionsAsync(
+                mOtherHomeAppUpdateReceiver.registerPkgActions(
                         mContext, mUpdateRegisteredPackage, ACTION_PACKAGE_ADDED,
                         ACTION_PACKAGE_CHANGED, ACTION_PACKAGE_REMOVED);
             }
@@ -203,13 +205,13 @@
      * Clean up any registered receivers.
      */
     public void onDestroy() {
-        mUserPreferenceChangeReceiver.unregisterReceiverSafelyAsync(mContext);
+        mUserPreferenceChangeReceiver.unregisterReceiverSafely(mContext);
         unregisterOtherHomeAppUpdateReceiver();
     }
 
     private void unregisterOtherHomeAppUpdateReceiver() {
         if (mUpdateRegisteredPackage != null) {
-            mOtherHomeAppUpdateReceiver.unregisterReceiverSafelyAsync(mContext);
+            mOtherHomeAppUpdateReceiver.unregisterReceiverSafely(mContext);
             mUpdateRegisteredPackage = null;
         }
     }
diff --git a/quickstep/src/com/android/quickstep/RecentsActivity.java b/quickstep/src/com/android/quickstep/RecentsActivity.java
index 13e9844..18461a6 100644
--- a/quickstep/src/com/android/quickstep/RecentsActivity.java
+++ b/quickstep/src/com/android/quickstep/RecentsActivity.java
@@ -132,6 +132,13 @@
      * Init drag layer and overview panel views.
      */
     protected void setupViews() {
+        SystemUiProxy systemUiProxy = SystemUiProxy.INSTANCE.get(this);
+        // SplitSelectStateController needs to be created before setContentView()
+        mSplitSelectStateController =
+                new SplitSelectStateController(this, mHandler, getStateManager(),
+                        null /* depthController */, getStatsLogManager(),
+                        systemUiProxy, RecentsModel.INSTANCE.get(this),
+                        null /*activityBackCallback*/);
         inflateRootView(R.layout.fallback_recents_activity);
         setContentView(getRootView());
         mDragLayer = findViewById(R.id.drag_layer);
@@ -139,12 +146,6 @@
         mFallbackRecentsView = findViewById(R.id.overview_panel);
         mActionsView = findViewById(R.id.overview_actions_view);
         getRootView().getSysUiScrim().getSysUIProgress().updateValue(0);
-        SystemUiProxy systemUiProxy = SystemUiProxy.INSTANCE.get(this);
-        mSplitSelectStateController =
-                new SplitSelectStateController(this, mHandler, getStateManager(),
-                        null /* depthController */, getStatsLogManager(),
-                        systemUiProxy, RecentsModel.INSTANCE.get(this),
-                        null /*activityBackCallback*/);
         mDragLayer.recreateControllers();
         if (enableDesktopWindowingMode()) {
             mDesktopRecentsTransitionController = new DesktopRecentsTransitionController(
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
index 7adce74..cd62265 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
@@ -301,6 +301,10 @@
         return mNavBarPosition;
     }
 
+    public NavigationMode getMode() {
+        return mMode;
+    }
+
     /**
      * @return whether the current nav mode is fully gestural.
      */
diff --git a/quickstep/src/com/android/quickstep/SimpleOrientationTouchTransformer.java b/quickstep/src/com/android/quickstep/SimpleOrientationTouchTransformer.java
index 29a57fc..5264643 100644
--- a/quickstep/src/com/android/quickstep/SimpleOrientationTouchTransformer.java
+++ b/quickstep/src/com/android/quickstep/SimpleOrientationTouchTransformer.java
@@ -34,12 +34,18 @@
 
     private final Context mContext;
     private OrientationRectF mOrientationRectF;
+    private OrientationRectF mTouchingOrientationRectF;
+    private int mViewRotation;
 
     public SimpleOrientationTouchTransformer(Context context) {
+        this(context, DisplayController.INSTANCE.get(context));
+    }
+
+    @androidx.annotation.VisibleForTesting
+    public SimpleOrientationTouchTransformer(Context context, DisplayController displayController) {
         mContext = context;
-        DisplayController.INSTANCE.get(context).addChangeListener(this);
-        onDisplayInfoChanged(context, DisplayController.INSTANCE.get(context).getInfo(),
-                CHANGE_ALL);
+        displayController.addChangeListener(this);
+        onDisplayInfoChanged(context, displayController.getInfo(), CHANGE_ALL);
     }
 
     @Override
@@ -56,7 +62,29 @@
                 info.rotation);
     }
 
+    /**
+     * Called when the touch is started. This preserves the touching orientation until the touch is
+     * done (i.e. ACTION_CANCEL or ACTION_UP). So the transform won't produce inconsistent position
+     * if display is changed during the touch.
+     */
+    public void updateTouchingOrientation(int viewRotation) {
+        mViewRotation = viewRotation;
+        mTouchingOrientationRectF = new OrientationRectF(mOrientationRectF.left,
+                mOrientationRectF.top, mOrientationRectF.right, mOrientationRectF.bottom,
+                mOrientationRectF.getRotation());
+    }
+
+    /** Called when the touch is finished. */
+    public void clearTouchingOrientation() {
+        mTouchingOrientationRectF = null;
+    }
+
     public void transform(MotionEvent ev, int rotation) {
+        if (mTouchingOrientationRectF != null) {
+            mTouchingOrientationRectF.applyTransformToRotation(ev, mViewRotation,
+                    true /* forceTransform */);
+            return;
+        }
         mOrientationRectF.applyTransformToRotation(ev, rotation, true /* forceTransform */);
     }
 }
diff --git a/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java b/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
index ba33c62..f813d9a 100644
--- a/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
+++ b/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
@@ -26,6 +26,7 @@
 import android.graphics.Matrix.ScaleToFit;
 import android.graphics.Rect;
 import android.graphics.RectF;
+import android.util.Log;
 import android.view.RemoteAnimationTarget;
 
 import androidx.annotation.NonNull;
@@ -381,6 +382,8 @@
     protected class SpringAnimationRunner extends AnimationSuccessListener
             implements RectFSpringAnim.OnUpdateListener, BuilderProxy {
 
+        private static final String TAG = "SpringAnimationRunner";
+
         final Rect mCropRect = new Rect();
         final Matrix mMatrix = new Matrix();
 
@@ -481,10 +484,26 @@
                 return;
             }
             mTargetTaskView.setAlpha(mAnimationFactory.isAnimatingIntoIcon() ? 1f : alpha);
-            float width = mThumbnailStartBounds.width();
-            float height =  mThumbnailStartBounds.height();
-            float scale = Math.min(currentRect.width(), currentRect.height())
-                    / Math.min(width, height);
+            float startWidth = mThumbnailStartBounds.width();
+            float startHeight =  mThumbnailStartBounds.height();
+            float currentWidth = currentRect.width();
+            float currentHeight = currentRect.height();
+            float scale;
+
+            boolean isStartWidthValid = Float.compare(startWidth, 0f) > 0;
+            boolean isStartHeightValid = Float.compare(startHeight, 0f) > 0;
+            if (isStartWidthValid && isStartHeightValid) {
+                scale = Math.min(currentWidth, currentHeight) / Math.min(startWidth, startHeight);
+            } else {
+                Log.e(TAG, "TaskView starting bounds are invalid: " + mThumbnailStartBounds);
+                if (isStartWidthValid) {
+                    scale = currentWidth / startWidth;
+                } else if (isStartHeightValid) {
+                    scale = currentHeight / startHeight;
+                } else {
+                    scale = 1f;
+                }
+            }
 
             mTargetTaskView.setScaleX(scale);
             mTargetTaskView.setScaleY(scale);
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index 433baa9..66aa897 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.java
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -229,6 +229,17 @@
     }
 
     @Override
+    public void onImeSwitcherLongPress() {
+        if (mSystemUiProxy != null) {
+            try {
+                mSystemUiProxy.onImeSwitcherLongPress();
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call onImeSwitcherLongPress");
+            }
+        }
+    }
+
+    @Override
     public void setHomeRotationEnabled(boolean enabled) {
         if (mSystemUiProxy != null) {
             try {
@@ -831,12 +842,14 @@
 
     /**
      * Tells SysUI to dismiss the bubble with the provided key.
+     *
      * @param key the key of the bubble to dismiss.
+     * @param timestamp the timestamp when the removal happened.
      */
-    public void dragBubbleToDismiss(String key) {
+    public void dragBubbleToDismiss(String key, long timestamp) {
         if (mBubbles == null) return;
         try {
-            mBubbles.dragBubbleToDismiss(key);
+            mBubbles.dragBubbleToDismiss(key, timestamp);
         } catch (RemoteException e) {
             Log.w(TAG, "Failed call dragBubbleToDismiss");
         }
diff --git a/quickstep/src/com/android/quickstep/TaskIconCache.java b/quickstep/src/com/android/quickstep/TaskIconCache.java
index e6febff..1f6c02c 100644
--- a/quickstep/src/com/android/quickstep/TaskIconCache.java
+++ b/quickstep/src/com/android/quickstep/TaskIconCache.java
@@ -33,6 +33,7 @@
 import android.text.TextUtils;
 import android.util.SparseArray;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.WorkerThread;
 
 import com.android.launcher3.R;
@@ -48,6 +49,7 @@
 import com.android.launcher3.util.DisplayController.Info;
 import com.android.launcher3.util.FlagOp;
 import com.android.launcher3.util.Preconditions;
+import com.android.quickstep.task.thumbnail.data.TaskIconDataSource;
 import com.android.quickstep.util.TaskKeyLruCache;
 import com.android.quickstep.util.TaskVisualsChangeListener;
 import com.android.systemui.shared.recents.model.Task;
@@ -55,12 +57,11 @@
 import com.android.systemui.shared.system.PackageManagerWrapper;
 
 import java.util.concurrent.Executor;
-import java.util.function.Consumer;
 
 /**
  * Manages the caching of task icons and related data.
  */
-public class TaskIconCache implements DisplayInfoChangeListener {
+public class TaskIconCache implements TaskIconDataSource, DisplayInfoChangeListener {
 
     private final Executor mBgExecutor;
 
@@ -103,21 +104,22 @@
      * @param callback The callback to receive the task after its data has been populated.
      * @return A cancelable handle to the request
      */
-    public CancellableTask updateIconInBackground(Task task, Consumer<Task> callback) {
+    @Override
+    public CancellableTask getIconInBackground(Task task, @NonNull GetTaskIconCallback callback) {
         Preconditions.assertUIThread();
         if (task.icon != null) {
             // Nothing to load, the icon is already loaded
-            callback.accept(task);
+            callback.onTaskIconReceived(task.icon, task.titleDescription, task.title);
             return null;
         }
         CancellableTask<TaskCacheEntry> request = new CancellableTask<>(
                 () -> getCacheEntry(task),
                 MAIN_EXECUTOR,
                 result -> {
-                    task.icon = result.icon;
-                    task.titleDescription = result.contentDescription;
-                    task.title = result.title;
-                    callback.accept(task);
+                    callback.onTaskIconReceived(
+                            result.icon,
+                            result.contentDescription,
+                            result.title);
                     dispatchIconUpdate(task.key.id);
                 }
         );
@@ -280,6 +282,12 @@
         public String title = "";
     }
 
+    /** Callback used when retrieving app icons from cache. */
+    public interface GetTaskIconCallback {
+        /** Called when task icon is retrieved. */
+        void onTaskIconReceived(Drawable icon, String contentDescription, String title);
+    }
+
     void registerTaskVisualsChangeListener(TaskVisualsChangeListener newListener) {
         mTaskVisualsChangeListener = newListener;
     }
diff --git a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
index 5d0d074..80902e3 100644
--- a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
+++ b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
@@ -16,18 +16,22 @@
 
 package com.android.quickstep;
 
+import static com.android.launcher3.Flags.enableRefactorTaskThumbnail;
 import static com.android.quickstep.views.OverviewActionsView.DISABLED_NO_THUMBNAIL;
 import static com.android.quickstep.views.OverviewActionsView.DISABLED_ROTATED;
 
 import android.annotation.SuppressLint;
 import android.content.Context;
+import android.graphics.Bitmap;
 import android.graphics.Insets;
 import android.graphics.Matrix;
 import android.graphics.Rect;
+import android.graphics.RectF;
 import android.os.Build;
 import android.view.View;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 
 import com.android.launcher3.BaseActivity;
@@ -38,16 +42,16 @@
 import com.android.launcher3.util.ResourceBasedOverride;
 import com.android.launcher3.views.ActivityContext;
 import com.android.launcher3.views.Snackbar;
+import com.android.quickstep.task.util.TaskOverlayHelper;
 import com.android.quickstep.util.RecentsOrientedState;
 import com.android.quickstep.views.DesktopTaskView;
 import com.android.quickstep.views.GroupedTaskView;
 import com.android.quickstep.views.OverviewActionsView;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.RecentsViewContainer;
+import com.android.quickstep.views.TaskContainer;
 import com.android.quickstep.views.TaskView;
-import com.android.quickstep.views.TaskView.TaskContainer;
 import com.android.systemui.shared.recents.model.Task;
-import com.android.systemui.shared.recents.model.ThumbnailData;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -128,12 +132,43 @@
 
         private T mActionsView;
         protected ImageActionsApi mImageApi;
+        protected TaskOverlayHelper mHelper;
 
         protected TaskOverlay(TaskContainer taskContainer) {
             mApplicationContext = taskContainer.getTaskView().getContext().getApplicationContext();
             mTaskContainer = taskContainer;
-            mImageApi = new ImageActionsApi(
-                    mApplicationContext, mTaskContainer::getThumbnail);
+            if (enableRefactorTaskThumbnail()) {
+                mHelper = new TaskOverlayHelper(mTaskContainer.getTask(), this);
+            }
+            mImageApi = new ImageActionsApi(mApplicationContext, this::getThumbnail);
+        }
+
+        /**
+         * Initialize the overlay when a Task is bound to the TaskView.
+         */
+        public void init() {
+            if (enableRefactorTaskThumbnail()) {
+                mHelper.init();
+            }
+        }
+
+        /**
+         * Destroy the overlay when the TaskView is recycled.
+         */
+        public void destroy() {
+            if (enableRefactorTaskThumbnail()) {
+                mHelper.destroy();
+            }
+        }
+
+        protected @Nullable Bitmap getThumbnail() {
+            return enableRefactorTaskThumbnail() ? mHelper.getEnabledState().getThumbnail()
+                    : mTaskContainer.getThumbnailViewDeprecated().getThumbnail();
+        }
+
+        protected boolean isRealSnapshot() {
+            return enableRefactorTaskThumbnail() ? mHelper.getEnabledState().isRealSnapshot()
+                    : mTaskContainer.getThumbnailViewDeprecated().isRealSnapshot();
         }
 
         protected T getActionsView() {
@@ -152,14 +187,13 @@
         /**
          * Called when the current task is interactive for the user
          */
-        public void initOverlay(Task task, ThumbnailData thumbnail, Matrix matrix,
+        public void initOverlay(Task task, @Nullable Bitmap thumbnail, Matrix matrix,
                 boolean rotated) {
             getActionsView().updateDisabledFlags(DISABLED_NO_THUMBNAIL, thumbnail == null);
 
             if (thumbnail != null) {
                 getActionsView().updateDisabledFlags(DISABLED_ROTATED, rotated);
-                boolean isAllowedByPolicy = mTaskContainer.isRealSnapshot();
-                getActionsView().setCallbacks(new OverlayUICallbacksImpl(isAllowedByPolicy, task));
+                getActionsView().setCallbacks(new OverlayUICallbacksImpl(isRealSnapshot(), task));
             }
         }
 
@@ -183,8 +217,8 @@
          */
         @SuppressLint("NewApi")
         protected void saveScreenshot(Task task) {
-            if (mTaskContainer.isRealSnapshot()) {
-                mImageApi.saveScreenshot(mTaskContainer.getThumbnail(),
+            if (isRealSnapshot()) {
+                mImageApi.saveScreenshot(getThumbnail(),
                         getTaskSnapshotBounds(), getTaskSnapshotInsets(), task.key);
             } else {
                 showBlockedByPolicyMessage();
@@ -259,7 +293,36 @@
          */
         @RequiresApi(api = Build.VERSION_CODES.Q)
         public Insets getTaskSnapshotInsets() {
-            return mTaskContainer.getScaledInsets();
+            Bitmap thumbnail = getThumbnail();
+            if (thumbnail == null) {
+                return Insets.NONE;
+            }
+
+            RectF bitmapRect = new RectF(
+                    0,
+                    0,
+                    thumbnail.getWidth(),
+                    thumbnail.getHeight());
+            View snapshotView = mTaskContainer.getSnapshotView();
+            RectF viewRect = new RectF(0, 0, snapshotView.getMeasuredWidth(),
+                    snapshotView.getMeasuredHeight());
+
+            // The position helper matrix tells us how to transform the bitmap to fit the view, the
+            // inverse tells us where the view would be in the bitmaps coordinates. The insets are
+            // the difference between the bitmap bounds and the projected view bounds.
+            Matrix boundsToBitmapSpace = new Matrix();
+            Matrix thumbnailMatrix = enableRefactorTaskThumbnail()
+                    ? mHelper.getEnabledState().getThumbnailMatrix()
+                    : mTaskContainer.getThumbnailViewDeprecated().getThumbnailMatrix();
+            thumbnailMatrix.invert(boundsToBitmapSpace);
+            RectF boundsInBitmapSpace = new RectF();
+            boundsToBitmapSpace.mapRect(boundsInBitmapSpace, viewRect);
+
+            RecentsViewContainer container = RecentsViewContainer.containerFromContext(
+                    getTaskView().getContext());
+            int bottomInset = container.getDeviceProfile().isTablet
+                    ? Math.round(bitmapRect.bottom - boundsInBitmapSpace.bottom) : 0;
+            return Insets.of(0, 0, 0, bottomInset);
         }
 
         /**
diff --git a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
index b4862fd..77124bf 100644
--- a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
+++ b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
@@ -56,8 +56,8 @@
 import com.android.quickstep.views.GroupedTaskView;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.RecentsViewContainer;
+import com.android.quickstep.views.TaskContainer;
 import com.android.quickstep.views.TaskView;
-import com.android.quickstep.views.TaskView.TaskContainer;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.view.AppTransitionAnimationSpecCompat;
 import com.android.systemui.shared.recents.view.AppTransitionAnimationSpecsFuture;
diff --git a/quickstep/src/com/android/quickstep/TaskThumbnailCache.java b/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
index 38e927f..3c6c3e4 100644
--- a/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
+++ b/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
@@ -131,8 +131,7 @@
         Preconditions.assertUIThread();
         // Fetch the thumbnail for this task and put it in the cache
         if (task.thumbnail == null) {
-            updateThumbnailInBackground(task.key, lowResolution,
-                    t -> task.thumbnail = t);
+            getThumbnailInBackground(task.key, lowResolution, t -> task.thumbnail = t);
         }
     }
 
@@ -145,13 +144,13 @@
     }
 
     /**
-     * Asynchronously fetches the icon and other task data for the given {@param task}.
+     * Asynchronously fetches the thumbnail for the given {@code task}.
      *
      * @param callback The callback to receive the task after its data has been populated.
      * @return A cancelable handle to the request
      */
     @Override
-    public CancellableTask<ThumbnailData> updateThumbnailInBackground(
+    public CancellableTask<ThumbnailData> getThumbnailInBackground(
             Task task, @NonNull Consumer<ThumbnailData> callback) {
         Preconditions.assertUIThread();
 
@@ -164,10 +163,7 @@
             return null;
         }
 
-        return updateThumbnailInBackground(task.key, !mHighResLoadingState.isEnabled(), t -> {
-            task.thumbnail = t;
-            callback.accept(t);
-        });
+        return getThumbnailInBackground(task.key, !mHighResLoadingState.isEnabled(), callback);
     }
 
     /**
@@ -187,7 +183,7 @@
         return newSize > oldSize;
     }
 
-    private CancellableTask<ThumbnailData> updateThumbnailInBackground(TaskKey key,
+    private CancellableTask<ThumbnailData> getThumbnailInBackground(TaskKey key,
             boolean lowResolution, Consumer<ThumbnailData> callback) {
         Preconditions.assertUIThread();
 
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index ee93cd6..2896979 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -42,6 +42,7 @@
 import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.MOTION_DOWN;
 import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.MOTION_MOVE;
 import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.MOTION_UP;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.NAVIGATION_MODE_SWITCHED;
 import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.RECENTS_ANIMATION_START_PENDING;
 import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
 import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SYSUI_PROXY;
@@ -70,7 +71,6 @@
 import android.os.Bundle;
 import android.os.IBinder;
 import android.os.Looper;
-import android.os.RemoteException;
 import android.os.SystemClock;
 import android.os.Trace;
 import android.util.ArraySet;
@@ -102,6 +102,7 @@
 import com.android.launcher3.testing.shared.TestProtocol;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.LockedUserState;
+import com.android.launcher3.util.NavigationMode;
 import com.android.launcher3.util.PluginManagerWrapper;
 import com.android.launcher3.util.SafeCloseable;
 import com.android.launcher3.util.ScreenOnTracker;
@@ -622,6 +623,8 @@
     private InputManager mInputManager;
     private final Set<Integer> mTrackpadsConnected = new ArraySet<>();
 
+    private NavigationMode mGestureStartNavMode = null;
+
     @Override
     public void onCreate() {
         super.onCreate();
@@ -836,14 +839,26 @@
         TestLogging.recordMotionEvent(
                 TestProtocol.SEQUENCE_TIS, "TouchInteractionService.onInputEvent", event);
 
-        boolean isUserUnlocked = LockedUserState.get(this).isUserUnlocked();
-        if (!isUserUnlocked || (mDeviceState.isButtonNavMode()
-                && !isTrackpadMotionEvent(event))) {
+        if (!LockedUserState.get(this).isUserUnlocked()) {
+            ActiveGestureLog.INSTANCE.addLog(new CompoundString("TIS.onInputEvent: ")
+                    .append("Cannot process input event: user is locked"));
+            return;
+        }
+
+        NavigationMode currentNavMode = mDeviceState.getMode();
+        if (mGestureStartNavMode != null && mGestureStartNavMode != currentNavMode) {
+            ActiveGestureLog.INSTANCE.addLog(new CompoundString("TIS.onInputEvent: ")
+                            .append("Navigation mode switched mid-gesture (")
+                            .append(mGestureStartNavMode.name())
+                            .append(" -> ")
+                            .append(currentNavMode.name())
+                            .append("); cancelling gesture."),
+                    NAVIGATION_MODE_SWITCHED);
+            event.setAction(ACTION_CANCEL);
+        } else if (mDeviceState.isButtonNavMode() && !isTrackpadMotionEvent(event)) {
             ActiveGestureLog.INSTANCE.addLog(new CompoundString("TIS.onInputEvent: ")
                     .append("Cannot process input event: ")
-                    .append(!isUserUnlocked
-                            ? "user is locked"
-                            : "using 3-button nav and event is not a trackpad event"));
+                    .append("using 3-button nav and event is not a trackpad event"));
             return;
         }
 
@@ -870,6 +885,12 @@
             }
         }
 
+        if (action == ACTION_DOWN || isHoverActionWithoutConsumer) {
+            mGestureStartNavMode = currentNavMode;
+        } else if (action == ACTION_UP || action == ACTION_CANCEL) {
+            mGestureStartNavMode = null;
+        }
+
         SafeCloseable traceToken = TraceHelper.INSTANCE.allowIpcs("TIS.onInputEvent");
 
         CompoundString reasonString = action == ACTION_DOWN
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
index 0d450c6..13b6447 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
@@ -70,6 +70,9 @@
  */
 public class OtherActivityInputConsumer extends ContextWrapper implements InputConsumer {
 
+    private static final String TAG = "OtherActivityInputConsumer";
+    private static final boolean DEBUG = true;
+
     public static final String DOWN_EVT = "OtherActivityInputConsumer.DOWN";
     private static final String UP_EVT = "OtherActivityInputConsumer.UP";
 
@@ -230,6 +233,9 @@
 
                 // Start the window animation on down to give more time for launcher to draw if the
                 // user didn't start the gesture over the back button
+                if (DEBUG) {
+                    Log.d(TAG, "ACTION_DOWN: mIsDeferredDownTarget=" + mIsDeferredDownTarget);
+                }
                 if (!mIsDeferredDownTarget) {
                     startTouchTrackingForWindowAnimation(ev.getEventTime());
                 }
@@ -284,9 +290,18 @@
 
                 float horizontalDist = Math.abs(displacementX);
                 float upDist = -displacement;
-                boolean passedSlop = mGestureState.isTrackpadGesture()
-                        || (squaredHypot(displacementX, displacementY) >= mSquaredTouchSlop
-                            && !mGestureState.isInExtendedSlopRegion());
+                boolean isTrackpadGesture = mGestureState.isTrackpadGesture();
+                float squaredHypot = squaredHypot(displacementX, displacementY);
+                boolean isInExtendedSlopRegion = !mGestureState.isInExtendedSlopRegion();
+                boolean passedSlop = isTrackpadGesture
+                        || (squaredHypot >= mSquaredTouchSlop
+                        && isInExtendedSlopRegion);
+                if (DEBUG) {
+                    Log.d(TAG, "ACTION_MOVE: passedSlop=" + passedSlop
+                            + " ( " + isTrackpadGesture
+                            + " || (" + squaredHypot + " >= " + mSquaredTouchSlop
+                            + " && " + isInExtendedSlopRegion + " ))");
+                }
 
                 if (!mPassedSlopOnThisGesture && passedSlop) {
                     mPassedSlopOnThisGesture = true;
@@ -306,6 +321,9 @@
                 boolean isLikelyToStartNewTask =
                         haveNotPassedSlopOnContinuedGesture || swipeWithinQuickSwitchRange;
 
+                if (DEBUG) {
+                    Log.d(TAG, "ACTION_MOVE: mPassedPilferInputSlop=" + mPassedPilferInputSlop);
+                }
                 if (!mPassedPilferInputSlop) {
                     if (passedSlop) {
                         // Horizontal gesture is not allowed in this region
@@ -394,6 +412,10 @@
         mInteractionHandler.initWhenReady(
                 "OtherActivityInputConsumer.startTouchTrackingForWindowAnimation");
 
+        if (DEBUG) {
+            Log.d(TAG, "startTouchTrackingForWindowAnimation: isRecentsAnimationRunning="
+                    + mTaskAnimationManager.isRecentsAnimationRunning());
+        }
         if (mTaskAnimationManager.isRecentsAnimationRunning()) {
             mActiveCallbacks = mTaskAnimationManager.continueRecentsAnimation(mGestureState);
             mActiveCallbacks.removeListener(mCleanupHandler);
@@ -422,6 +444,11 @@
      */
     private void finishTouchTracking(MotionEvent ev) {
         TraceHelper.INSTANCE.beginSection(UP_EVT);
+        if (DEBUG) {
+            Log.d(TAG, "finishTouchTracking: mPassedWindowMoveSlop=" + mPassedWindowMoveSlop);
+            Log.d(TAG, "finishTouchTracking: mInteractionHandler=" + mInteractionHandler);
+            Log.d(TAG, "finishTouchTracking: ev=" + ev);
+        }
 
         boolean isCanceled = ev.getActionMasked() == ACTION_CANCEL;
         if (mPassedWindowMoveSlop && mInteractionHandler != null) {
@@ -444,7 +471,14 @@
             // Since we start touch tracking on DOWN, we may reach this state without actually
             // starting the gesture. In that case, we need to clean-up an unfinished or un-started
             // animation.
+            if (DEBUG) {
+                Log.d(TAG, "finishTouchTracking: mActiveCallbacks=" + mActiveCallbacks);
+            }
             if (mActiveCallbacks != null && mInteractionHandler != null) {
+                if (DEBUG) {
+                    Log.d(TAG, "finishTouchTracking: isRecentsAnimationRunning="
+                            + mTaskAnimationManager.isRecentsAnimationRunning());
+                }
                 if (mTaskAnimationManager.isRecentsAnimationRunning()) {
                     // The animation started, but with no movement, in this case, there will be no
                     // animateToProgress so we have to manually finish here. In the case of
@@ -535,7 +569,13 @@
 
         public void onRecentsAnimationStart(RecentsAnimationController controller,
                 RecentsAnimationTargets targets) {
+            if (DEBUG) {
+                Log.d(TAG, "FinishImmediatelyHandler: queuing callback");
+            }
             Utilities.postAsyncCallback(MAIN_EXECUTOR.getHandler(), () -> {
+                if (DEBUG) {
+                    Log.d(TAG, "FinishImmediatelyHandler: running callback");
+                }
                 controller.finish(false /* toRecents */, null);
             });
         }
diff --git a/quickstep/src/com/android/quickstep/recents/data/RecentTasksRepository.kt b/quickstep/src/com/android/quickstep/recents/data/RecentTasksRepository.kt
index c1eef0b..9c4248c 100644
--- a/quickstep/src/com/android/quickstep/recents/data/RecentTasksRepository.kt
+++ b/quickstep/src/com/android/quickstep/recents/data/RecentTasksRepository.kt
@@ -17,6 +17,7 @@
 package com.android.quickstep.recents.data
 
 import com.android.systemui.shared.recents.model.Task
+import com.android.systemui.shared.recents.model.ThumbnailData
 import kotlinx.coroutines.flow.Flow
 
 interface RecentTasksRepository {
@@ -25,11 +26,17 @@
 
     /**
      * Gets the data associated with a task that has id [taskId]. Flow will settle on null if the
-     * task was not found.
+     * task was not found. [Task.thumbnail] will settle on null if task is invisible.
      */
     fun getTaskDataById(taskId: Int): Flow<Task?>
 
     /**
+     * Gets the [ThumbnailData] associated with a task that has id [taskId]. Flow will settle on
+     * null if the task was not found or is invisible.
+     */
+    fun getThumbnailById(taskId: Int): Flow<ThumbnailData?>
+
+    /**
      * Sets the tasks that are visible, indicating that properties relating to visuals need to be
      * populated e.g. icons/thumbnails etc.
      */
diff --git a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
index b21a1b4..f73db5a 100644
--- a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
+++ b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
@@ -16,7 +16,8 @@
 
 package com.android.quickstep.recents.data
 
-import com.android.quickstep.TaskIconCache
+import android.graphics.drawable.Drawable
+import com.android.quickstep.task.thumbnail.data.TaskIconDataSource
 import com.android.quickstep.task.thumbnail.data.TaskThumbnailDataSource
 import com.android.quickstep.util.GroupTask
 import com.android.systemui.shared.recents.model.Task
@@ -27,6 +28,7 @@
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.distinctUntilChangedBy
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flow
 import kotlinx.coroutines.flow.flowOf
@@ -37,7 +39,7 @@
 class TasksRepository(
     private val recentsModel: RecentTasksDataSource,
     private val taskThumbnailDataSource: TaskThumbnailDataSource,
-    private val taskIconCache: TaskIconCache,
+    private val taskIconDataSource: TaskIconDataSource,
 ) : RecentTasksRepository {
     private val groupedTaskData = MutableStateFlow(emptyList<GroupTask>())
     private val _taskData =
@@ -45,10 +47,19 @@
     private val visibleTaskIds = MutableStateFlow(emptySet<Int>())
 
     private val taskData: Flow<List<Task>> =
-        combine(_taskData, getThumbnailQueryResults()) { tasks, results ->
+        combine(_taskData, getThumbnailQueryResults(), getIconQueryResults()) {
+            tasks,
+            thumbnailQueryResults,
+            iconQueryResults ->
             tasks.forEach { task ->
                 // Add retrieved thumbnails + remove unnecessary thumbnails
-                task.thumbnail = results[task.key.id]
+                task.thumbnail = thumbnailQueryResults[task.key.id]
+
+                // TODO(b/352331675) don't load icons for DesktopTaskView
+                // Add retrieved icons + remove unnecessary icons
+                task.icon = iconQueryResults[task.key.id]?.icon
+                task.titleDescription = iconQueryResults[task.key.id]?.contentDescription
+                task.title = iconQueryResults[task.key.id]?.title
             }
             tasks
         }
@@ -63,18 +74,21 @@
     override fun getTaskDataById(taskId: Int): Flow<Task?> =
         taskData.map { taskList -> taskList.firstOrNull { it.key.id == taskId } }
 
+    override fun getThumbnailById(taskId: Int): Flow<ThumbnailData?> =
+        getTaskDataById(taskId).map { it?.thumbnail }.distinctUntilChangedBy { it?.snapshotId }
+
     override fun setVisibleTasks(visibleTaskIdList: List<Int>) {
         this.visibleTaskIds.value = visibleTaskIdList.toSet()
     }
 
-    /** Flow wrapper for [TaskThumbnailDataSource.updateThumbnailInBackground] api */
+    /** Flow wrapper for [TaskThumbnailDataSource.getThumbnailInBackground] api */
     private fun getThumbnailDataRequest(task: Task): ThumbnailDataRequest =
         flow {
                 emit(task.key.id to task.thumbnail)
                 val thumbnailDataResult: ThumbnailData? =
                     suspendCancellableCoroutine { continuation ->
                         val cancellableTask =
-                            taskThumbnailDataSource.updateThumbnailInBackground(task) {
+                            taskThumbnailDataSource.getThumbnailInBackground(task) {
                                 continuation.resume(it)
                             }
                         continuation.invokeOnCancellation { cancellableTask?.cancel() }
@@ -94,12 +108,7 @@
                 tasks.filter { it.key.id in visibleIds }
             }
         val visibleThumbnailDataRequests: Flow<List<ThumbnailDataRequest>> =
-            visibleTasks.map {
-                it.map { visibleTask ->
-                    val taskCopy = Task(visibleTask).apply { thumbnail = visibleTask.thumbnail }
-                    getThumbnailDataRequest(taskCopy)
-                }
-            }
+            visibleTasks.map { visibleTasksList -> visibleTasksList.map(::getThumbnailDataRequest) }
         return visibleThumbnailDataRequests.flatMapLatest {
             thumbnailRequestFlows: List<ThumbnailDataRequest> ->
             if (thumbnailRequestFlows.isEmpty()) {
@@ -109,6 +118,59 @@
             }
         }
     }
+
+    /** Flow wrapper for [TaskThumbnailDataSource.getThumbnailInBackground] api */
+    private fun getIconDataRequest(task: Task): IconDataRequest =
+        flow {
+                emit(task.key.id to task.getTaskIconQueryResponse())
+                val iconDataResponse: TaskIconQueryResponse? =
+                    suspendCancellableCoroutine { continuation ->
+                        val cancellableTask =
+                            taskIconDataSource.getIconInBackground(task) {
+                                icon,
+                                contentDescription,
+                                title ->
+                                continuation.resume(
+                                    TaskIconQueryResponse(icon, contentDescription, title)
+                                )
+                            }
+                        continuation.invokeOnCancellation { cancellableTask?.cancel() }
+                    }
+                emit(task.key.id to iconDataResponse)
+            }
+            .distinctUntilChanged()
+
+    private fun getIconQueryResults(): Flow<Map<Int, TaskIconQueryResponse?>> {
+        val visibleTasks =
+            combine(_taskData, visibleTaskIds) { tasks, visibleIds ->
+                tasks.filter { it.key.id in visibleIds }
+            }
+        val visibleIconDataRequests: Flow<List<IconDataRequest>> =
+            visibleTasks.map { visibleTasksList -> visibleTasksList.map(::getIconDataRequest) }
+        return visibleIconDataRequests.flatMapLatest { iconRequestFlows: List<IconDataRequest> ->
+            if (iconRequestFlows.isEmpty()) {
+                flowOf(emptyMap())
+            } else {
+                combine(iconRequestFlows) { it.toMap() }
+            }
+        }
+    }
 }
 
-typealias ThumbnailDataRequest = Flow<Pair<Int, ThumbnailData?>>
+private data class TaskIconQueryResponse(
+    val icon: Drawable,
+    val contentDescription: String,
+    val title: String
+)
+
+private fun Task.getTaskIconQueryResponse(): TaskIconQueryResponse? {
+    val iconVal = icon ?: return null
+    val titleDescriptionVal = titleDescription ?: return null
+    val titleVal = title ?: return null
+
+    return TaskIconQueryResponse(iconVal, titleDescriptionVal, titleVal)
+}
+
+private typealias ThumbnailDataRequest = Flow<Pair<Int, ThumbnailData?>>
+
+private typealias IconDataRequest = Flow<Pair<Int, TaskIconQueryResponse?>>
diff --git a/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewData.kt b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewData.kt
index 28212cf..fdb62df 100644
--- a/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewData.kt
+++ b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewData.kt
@@ -24,4 +24,11 @@
 
     // This is typically a View concern but it is used to invalidate rendering in other Views
     val scale = MutableStateFlow(1f)
+
+    // Whether the current RecentsView state supports task overlays.
+    // TODO(b/331753115): Derive from RecentsView state flow once migrated to MVVM.
+    val overlayEnabled = MutableStateFlow(false)
+
+    // The settled set of visible taskIds that is updated after RecentsView scroll settles.
+    val settledFullyVisibleTaskIds = MutableStateFlow(emptySet<Int>())
 }
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/LiveTileView.kt b/quickstep/src/com/android/quickstep/task/thumbnail/LiveTileView.kt
new file mode 100644
index 0000000..45b3687
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/LiveTileView.kt
@@ -0,0 +1,46 @@
+/*
+ * 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.content.Context
+import android.graphics.Canvas
+import android.graphics.Paint
+import android.graphics.PorterDuff
+import android.graphics.PorterDuffXfermode
+import android.util.AttributeSet
+import android.view.View
+
+class LiveTileView : View {
+    constructor(context: Context) : super(context)
+
+    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
+
+    constructor(
+        context: Context,
+        attrs: AttributeSet?,
+        defStyleAttr: Int,
+    ) : super(context, attrs, defStyleAttr)
+
+    override fun onDraw(canvas: Canvas) {
+        canvas.drawRect(0f, 0f, measuredWidth.toFloat(), measuredHeight.toFloat(), CLEAR_PAINT)
+    }
+
+    companion object {
+        private val CLEAR_PAINT =
+            Paint().apply { xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR) }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskOverlayUiState.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskOverlayUiState.kt
new file mode 100644
index 0000000..feee11f
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskOverlayUiState.kt
@@ -0,0 +1,31 @@
+/*
+ * 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.Bitmap
+import android.graphics.Matrix
+
+/** Ui state for [com.android.quickstep.TaskOverlayFactory.TaskOverlay] */
+sealed class TaskOverlayUiState {
+    data object Disabled : TaskOverlayUiState()
+
+    data class Enabled(
+        val isRealSnapshot: Boolean,
+        val thumbnail: Bitmap?,
+        val thumbnailMatrix: Matrix
+    ) : TaskOverlayUiState()
+}
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
index dbe2b19..d22fc94 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
@@ -18,18 +18,19 @@
 
 import android.content.Context
 import android.content.res.Configuration
-import android.graphics.Canvas
 import android.graphics.Color
 import android.graphics.Outline
-import android.graphics.Paint
-import android.graphics.PorterDuff
-import android.graphics.PorterDuffXfermode
 import android.graphics.Rect
 import android.util.AttributeSet
 import android.view.View
 import android.view.ViewOutlineProvider
+import android.widget.FrameLayout
+import android.widget.ImageView
 import androidx.annotation.ColorInt
+import androidx.core.view.isVisible
+import com.android.launcher3.R
 import com.android.launcher3.Utilities
+import com.android.launcher3.util.ViewPool
 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.BackgroundOnly
 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.LiveTile
 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot
@@ -39,10 +40,15 @@
 import com.android.quickstep.views.RecentsViewContainer
 import com.android.quickstep.views.TaskView
 import com.android.systemui.shared.system.QuickStepContract
-import kotlinx.coroutines.MainScope
-import kotlinx.coroutines.launch
+import kotlinx.coroutines.CoroutineName
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
 
-class TaskThumbnailView : View {
+class TaskThumbnailView : FrameLayout, ViewPool.Reusable {
     // TODO(b/335649589): Ideally create and obtain this from DI. This ViewModel should be scoped
     //  to [TaskView], and also shared between [TaskView] and [TaskThumbnailView]
     //  This is using a lazy for now because the dependencies cannot be obtained without DI.
@@ -51,19 +57,20 @@
             RecentsViewContainer.containerFromContext<RecentsViewContainer>(context)
                 .getOverviewPanel<RecentsView<*, *>>()
         TaskThumbnailViewModel(
-            recentsView.mRecentsViewData,
+            recentsView.mRecentsViewData!!,
             (parent as TaskView).taskViewData,
             (parent as TaskView).getTaskContainerForTaskThumbnailView(this)!!.taskContainerData,
-            recentsView.mTasksRepository,
+            recentsView.mTasksRepository!!,
         )
     }
+    private lateinit var viewAttachedScope: CoroutineScope
 
-    private var uiState: TaskThumbnailUiState = Uninitialized
+    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 thumbnail: ImageView by lazy { findViewById(R.id.task_thumbnail) }
+
     private var inheritedScale: Float = 1f
-    private var dimProgress: Float = 0f
 
-    private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG)
-    private val scrimPaint = Paint().apply { color = Color.BLACK }
     private val _measuredBounds = Rect()
     private val measuredBounds: Rect
         get() {
@@ -71,42 +78,47 @@
             return _measuredBounds
         }
 
-    private var cornerRadius: Float = TaskCornerRadius.get(context)
+    private var overviewCornerRadius: Float = TaskCornerRadius.get(context)
     private var fullscreenCornerRadius: Float = QuickStepContract.getWindowCornerRadius(context)
 
-    constructor(context: Context?) : super(context)
+    constructor(context: Context) : super(context)
 
-    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
+    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
 
     constructor(
-        context: Context?,
+        context: Context,
         attrs: AttributeSet?,
         defStyleAttr: Int,
     ) : super(context, attrs, defStyleAttr)
 
     override fun onAttachedToWindow() {
         super.onAttachedToWindow()
-        // TODO(b/335396935) replace MainScope with shorter lifecycle.
-        MainScope().launch {
-            viewModel.uiState.collect { viewModelUiState ->
-                uiState = viewModelUiState
-                invalidate()
+        viewAttachedScope =
+            CoroutineScope(SupervisorJob() + Dispatchers.Main + CoroutineName("TaskThumbnailView"))
+        viewModel.uiState
+            .onEach { viewModelUiState ->
+                resetViews()
+                when (viewModelUiState) {
+                    is Uninitialized -> {}
+                    is LiveTile -> drawLiveWindow()
+                    is Snapshot -> drawSnapshot(viewModelUiState)
+                    is BackgroundOnly -> drawBackground(viewModelUiState.backgroundColor)
+                }
             }
-        }
-        MainScope().launch {
-            viewModel.dimProgress.collect { dimProgress ->
+            .launchIn(viewAttachedScope)
+        viewModel.dimProgress
+            .onEach { dimProgress ->
                 // TODO(b/348195366) Add fade in/out for scrim
-                this@TaskThumbnailView.dimProgress = dimProgress
-                invalidate()
+                scrimView.alpha = dimProgress * MAX_SCRIM_ALPHA
             }
-        }
-        MainScope().launch { viewModel.recentsFullscreenProgress.collect { invalidateOutline() } }
-        MainScope().launch {
-            viewModel.inheritedScale.collect { viewModelInheritedScale ->
+            .launchIn(viewAttachedScope)
+        viewModel.cornerRadiusProgress.onEach { invalidateOutline() }.launchIn(viewAttachedScope)
+        viewModel.inheritedScale
+            .onEach { viewModelInheritedScale ->
                 inheritedScale = viewModelInheritedScale
                 invalidateOutline()
             }
-        }
+            .launchIn(viewAttachedScope)
 
         clipToOutline = true
         outlineProvider =
@@ -117,56 +129,52 @@
             }
     }
 
-    override fun onDraw(canvas: Canvas) {
-        when (val uiStateVal = uiState) {
-            is Uninitialized -> drawBackgroundOnly(canvas, Color.BLACK)
-            is LiveTile -> drawTransparentUiState(canvas)
-            is Snapshot -> drawSnapshotState(canvas, uiStateVal)
-            is BackgroundOnly -> drawBackgroundOnly(canvas, uiStateVal.backgroundColor)
-        }
-
-        if (dimProgress > 0) {
-            drawScrim(canvas)
-        }
+    override fun onDetachedFromWindow() {
+        super.onDetachedFromWindow()
+        viewAttachedScope.cancel("TaskThumbnailView detaching from window")
     }
 
-    private fun drawBackgroundOnly(canvas: Canvas, @ColorInt backgroundColor: Int) {
-        backgroundPaint.color = backgroundColor
-        canvas.drawRect(measuredBounds, backgroundPaint)
+    override fun onRecycle() {
+        // Do nothing
     }
 
     override fun onConfigurationChanged(newConfig: Configuration?) {
         super.onConfigurationChanged(newConfig)
 
-        cornerRadius = TaskCornerRadius.get(context)
+        overviewCornerRadius = TaskCornerRadius.get(context)
         fullscreenCornerRadius = QuickStepContract.getWindowCornerRadius(context)
         invalidateOutline()
     }
 
-    private fun drawTransparentUiState(canvas: Canvas) {
-        canvas.drawRect(measuredBounds, CLEAR_PAINT)
+    private fun resetViews() {
+        liveTileView.isVisible = false
+        thumbnail.isVisible = false
+        scrimView.alpha = 0f
+        setBackgroundColor(Color.BLACK)
     }
 
-    private fun drawSnapshotState(canvas: Canvas, snapshot: Snapshot) {
-        drawBackgroundOnly(canvas, snapshot.backgroundColor)
-        canvas.drawBitmap(snapshot.bitmap, snapshot.drawnRect, measuredBounds, null)
+    private fun drawBackground(@ColorInt background: Int) {
+        setBackgroundColor(background)
     }
 
-    private fun drawScrim(canvas: Canvas) {
-        scrimPaint.alpha = (dimProgress * MAX_SCRIM_ALPHA).toInt()
-        canvas.drawRect(measuredBounds, scrimPaint)
+    private fun drawLiveWindow() {
+        liveTileView.isVisible = true
+    }
+
+    private fun drawSnapshot(snapshot: Snapshot) {
+        drawBackground(snapshot.backgroundColor)
+        thumbnail.setImageBitmap(snapshot.bitmap)
+        thumbnail.isVisible = true
     }
 
     private fun getCurrentCornerRadius() =
         Utilities.mapRange(
-            viewModel.recentsFullscreenProgress.value,
-            cornerRadius,
+            viewModel.cornerRadiusProgress.value,
+            overviewCornerRadius,
             fullscreenCornerRadius
         ) / inheritedScale
 
-    companion object {
-        private val CLEAR_PAINT =
-            Paint().apply { xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR) }
-        private const val MAX_SCRIM_ALPHA = (0.4f * 255).toInt()
+    private companion object {
+        const val MAX_SCRIM_ALPHA = 0.4f
     }
 }
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModel.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModel.kt
index fe21174..d8729a6 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModel.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModel.kt
@@ -31,6 +31,7 @@
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flatMapLatest
@@ -47,7 +48,13 @@
     private val task = MutableStateFlow<Flow<Task?>>(flowOf(null))
     private var boundTaskIsRunning = false
 
-    val recentsFullscreenProgress = recentsViewData.fullscreenProgress
+    /**
+     * Progress for changes in corner radius. progress: 0 = overview corner radius; 1 = fullscreen
+     * corner radius.
+     */
+    val cornerRadiusProgress =
+        if (taskViewData.isOutlineFormedByThumbnailView) recentsViewData.fullscreenProgress
+        else MutableStateFlow(1f).asStateFlow()
     val inheritedScale =
         combine(recentsViewData.scale, taskViewData.scale) { recentsScale, taskScale ->
             recentsScale * taskScale
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskIconDataSource.kt b/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskIconDataSource.kt
new file mode 100644
index 0000000..ab699c6
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskIconDataSource.kt
@@ -0,0 +1,25 @@
+/*
+ * 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.data
+
+import com.android.launcher3.util.CancellableTask
+import com.android.quickstep.TaskIconCache.GetTaskIconCallback
+import com.android.systemui.shared.recents.model.Task
+
+interface TaskIconDataSource {
+    fun getIconInBackground(task: Task, callback: GetTaskIconCallback): CancellableTask<*>?
+}
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskThumbnailDataSource.kt b/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskThumbnailDataSource.kt
index 55598f0..986acbe 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskThumbnailDataSource.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskThumbnailDataSource.kt
@@ -22,7 +22,7 @@
 import java.util.function.Consumer
 
 interface TaskThumbnailDataSource {
-    fun updateThumbnailInBackground(
+    fun getThumbnailInBackground(
         task: Task,
         callback: Consumer<ThumbnailData>
     ): CancellableTask<ThumbnailData>?
diff --git a/quickstep/src/com/android/quickstep/task/util/GetThumbnailUseCase.kt b/quickstep/src/com/android/quickstep/task/util/GetThumbnailUseCase.kt
new file mode 100644
index 0000000..e8dd04c3
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/task/util/GetThumbnailUseCase.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.util
+
+import android.graphics.Bitmap
+import com.android.quickstep.recents.data.RecentTasksRepository
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.runBlocking
+
+/** 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? = runBlocking {
+        taskRepository.getThumbnailById(taskId).firstOrNull()?.thumbnail
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/task/util/TaskOverlayHelper.kt b/quickstep/src/com/android/quickstep/task/util/TaskOverlayHelper.kt
new file mode 100644
index 0000000..5e55e2e
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/task/util/TaskOverlayHelper.kt
@@ -0,0 +1,90 @@
+/*
+ * 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.util
+
+import android.util.Log
+import com.android.quickstep.TaskOverlayFactory
+import com.android.quickstep.task.thumbnail.TaskOverlayUiState
+import com.android.quickstep.task.thumbnail.TaskOverlayUiState.Disabled
+import com.android.quickstep.task.thumbnail.TaskOverlayUiState.Enabled
+import com.android.quickstep.task.viewmodel.TaskOverlayViewModel
+import com.android.quickstep.views.RecentsView
+import com.android.quickstep.views.RecentsViewContainer
+import com.android.systemui.shared.recents.model.Task
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.launch
+
+/**
+ * Helper for [TaskOverlayFactory.TaskOverlay] to interact with [TaskOverlayViewModel], this helper
+ * should merge with [TaskOverlayFactory.TaskOverlay] when it's migrated to MVVM.
+ */
+class TaskOverlayHelper(val task: Task, val overlay: TaskOverlayFactory.TaskOverlay<*>) {
+    private lateinit var job: Job
+    private var uiState: TaskOverlayUiState = Disabled
+
+    // TODO(b/335649589): Ideally create and obtain this from DI. This ViewModel should be scoped
+    //  to [TaskView], and also shared between [TaskView] and [TaskThumbnailView]
+    //  This is using a lazy for now because the dependencies cannot be obtained without DI.
+    private val taskOverlayViewModel by lazy {
+        val recentsView =
+            RecentsViewContainer.containerFromContext<RecentsViewContainer>(
+                    overlay.taskView.context
+                )
+                .getOverviewPanel<RecentsView<*, *>>()
+        TaskOverlayViewModel(task, recentsView.mRecentsViewData!!, recentsView.mTasksRepository!!)
+    }
+
+    // TODO(b/331753115): TaskOverlay should listen for state changes and react.
+    val enabledState: Enabled
+        get() = uiState as Enabled
+
+    fun init() {
+        // TODO(b/335396935): This should be changed to TaskView's scope.
+        job =
+            MainScope().launch {
+                taskOverlayViewModel.overlayState.collect {
+                    uiState = it
+                    if (it is Enabled) {
+                        Log.d(
+                            TAG,
+                            "initOverlay - taskId: ${task.key.id}, thumbnail: ${it.thumbnail}"
+                        )
+                        overlay.initOverlay(
+                            task,
+                            it.thumbnail,
+                            it.thumbnailMatrix,
+                            /* rotated= */ false
+                        )
+                    } else {
+                        Log.d(TAG, "reset - taskId: ${task.key.id}")
+                        overlay.reset()
+                    }
+                }
+            }
+    }
+
+    fun destroy() {
+        job.cancel()
+        uiState = Disabled
+        overlay.reset()
+    }
+
+    companion object {
+        private const val TAG = "TaskOverlayHelper"
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModel.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModel.kt
new file mode 100644
index 0000000..47f32fb
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModel.kt
@@ -0,0 +1,55 @@
+/*
+ * 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
+import com.android.quickstep.recents.data.RecentTasksRepository
+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.systemui.shared.recents.model.Task
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+
+/** View model for TaskOverlay */
+class TaskOverlayViewModel(
+    task: Task,
+    recentsViewData: RecentsViewData,
+    tasksRepository: RecentTasksRepository,
+) {
+    val overlayState =
+        combine(
+                recentsViewData.overlayEnabled,
+                recentsViewData.settledFullyVisibleTaskIds.map { it.contains(task.key.id) },
+                tasksRepository.getThumbnailById(task.key.id)
+            ) { isOverlayEnabled, isFullyVisible, thumbnailData ->
+                if (isOverlayEnabled && isFullyVisible) {
+                    Enabled(
+                        isRealSnapshot = (thumbnailData?.isRealSnapshot ?: false) && !task.isLocked,
+                        thumbnailData?.thumbnail,
+                        // TODO(b/343101424): Use PreviewPositionHelper, listen from a common source
+                        // with
+                        //  TaskThumbnailView.
+                        Matrix.IDENTITY_MATRIX
+                    )
+                } else {
+                    Disabled
+                }
+            }
+            .distinctUntilChanged()
+}
diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskViewData.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskViewData.kt
index a8b5112..7a9ecf2 100644
--- a/quickstep/src/com/android/quickstep/task/viewmodel/TaskViewData.kt
+++ b/quickstep/src/com/android/quickstep/task/viewmodel/TaskViewData.kt
@@ -16,9 +16,14 @@
 
 package com.android.quickstep.task.viewmodel
 
+import com.android.quickstep.views.TaskViewType
 import kotlinx.coroutines.flow.MutableStateFlow
 
-class TaskViewData {
+class TaskViewData(taskViewType: TaskViewType) {
     // This is typically a View concern but it is used to invalidate rendering in other Views
     val scale = MutableStateFlow(1f)
+
+    // TODO(b/331753115): This property should not be in TaskViewData once TaskView is MVVM.
+    /** Whether outline of TaskView is formed by outline thumbnail view(s). */
+    val isOutlineFormedByThumbnailView: Boolean = taskViewType != TaskViewType.DESKTOP
 }
diff --git a/quickstep/src/com/android/quickstep/util/ActiveGestureErrorDetector.java b/quickstep/src/com/android/quickstep/util/ActiveGestureErrorDetector.java
index 3140fff..2398e66 100644
--- a/quickstep/src/com/android/quickstep/util/ActiveGestureErrorDetector.java
+++ b/quickstep/src/com/android/quickstep/util/ActiveGestureErrorDetector.java
@@ -40,7 +40,7 @@
         SCROLLER_ANIMATION_ABORTED, TASK_APPEARED, EXPECTING_TASK_APPEARED,
         FLAG_USING_OTHER_ACTIVITY_INPUT_CONSUMER, LAUNCHER_DESTROYED, RECENT_TASKS_MISSING,
         INVALID_VELOCITY_ON_SWIPE_UP, RECENTS_ANIMATION_START_PENDING,
-        QUICK_SWITCH_FROM_HOME_FALLBACK, QUICK_SWITCH_FROM_HOME_FAILED,
+        QUICK_SWITCH_FROM_HOME_FALLBACK, QUICK_SWITCH_FROM_HOME_FAILED, NAVIGATION_MODE_SWITCHED,
 
         /**
          * These GestureEvents are specifically associated to state flags that get set in
@@ -299,6 +299,13 @@
                                     + "the current page index and index 0 were missing.",
                             writer);
                     break;
+                case NAVIGATION_MODE_SWITCHED:
+                    errorDetected |= printErrorIfTrue(
+                            true,
+                            prefix,
+                            /* errorMessage= */ "Navigation mode switched mid-gesture.",
+                            writer);
+                    break;
                 case EXPECTING_TASK_APPEARED:
                 case MOTION_DOWN:
                 case SET_END_TARGET:
diff --git a/quickstep/src/com/android/quickstep/util/AppPairsController.java b/quickstep/src/com/android/quickstep/util/AppPairsController.java
index 6f9cbfd..c3d74bb 100644
--- a/quickstep/src/com/android/quickstep/util/AppPairsController.java
+++ b/quickstep/src/com/android/quickstep/util/AppPairsController.java
@@ -22,7 +22,6 @@
 import static com.android.internal.jank.Cuj.CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_TASKBAR;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_PAIR_LAUNCH;
 import static com.android.launcher3.model.data.AppInfo.PACKAGE_KEY_COMPARATOR;
-import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_SUPPORTS_MULTI_INSTANCE;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
@@ -45,8 +44,6 @@
 
 import com.android.internal.jank.Cuj;
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherSettings;
-import com.android.launcher3.R;
 import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
 import com.android.launcher3.allapps.AllAppsStore;
 import com.android.launcher3.apppairs.AppPairIcon;
@@ -69,6 +66,7 @@
 import com.android.quickstep.TaskUtils;
 import com.android.quickstep.TopTaskTracker;
 import com.android.quickstep.views.GroupedTaskView;
+import com.android.quickstep.views.TaskContainer;
 import com.android.quickstep.views.TaskView;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
@@ -135,7 +133,7 @@
         }
 
         GroupedTaskView gtv = (GroupedTaskView) taskView;
-        List<TaskView.TaskContainer> containers = gtv.getTaskContainers();
+        List<TaskContainer> containers = gtv.getTaskContainers();
         ComponentKey taskKey1 = TaskUtils.getLaunchComponentKeyForTask(
                 containers.get(0).getTask().key);
         ComponentKey taskKey2 = TaskUtils.getLaunchComponentKeyForTask(
@@ -172,7 +170,7 @@
      */
     public void saveAppPair(GroupedTaskView gtv) {
         InteractionJankMonitorWrapper.begin(gtv, Cuj.CUJ_LAUNCHER_SAVE_APP_PAIR);
-        List<TaskView.TaskContainer> containers = gtv.getTaskContainers();
+        List<TaskContainer> containers = gtv.getTaskContainers();
         WorkspaceItemInfo recentsInfo1 = containers.get(0).getItemInfo();
         WorkspaceItemInfo recentsInfo2 = containers.get(1).getItemInfo();
         WorkspaceItemInfo app1 = resolveAppPairWorkspaceInfo(recentsInfo1);
diff --git a/quickstep/src/com/android/quickstep/util/AsyncClockEventDelegate.java b/quickstep/src/com/android/quickstep/util/AsyncClockEventDelegate.java
index c26fc0c5..38ae303 100644
--- a/quickstep/src/com/android/quickstep/util/AsyncClockEventDelegate.java
+++ b/quickstep/src/com/android/quickstep/util/AsyncClockEventDelegate.java
@@ -18,6 +18,8 @@
 import static android.content.Intent.ACTION_TIMEZONE_CHANGED;
 import static android.content.Intent.ACTION_TIME_CHANGED;
 
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
@@ -50,7 +52,7 @@
 
     private final Context mContext;
     private final SimpleBroadcastReceiver mReceiver =
-            new SimpleBroadcastReceiver(this::onClockEventReceived);
+            new SimpleBroadcastReceiver(UI_HELPER_EXECUTOR, this::onClockEventReceived);
 
     private final ArrayMap<BroadcastReceiver, Handler> mTimeEventReceivers = new ArrayMap<>();
     private final List<ContentObserver> mFormatObservers = new ArrayList<>();
@@ -62,7 +64,7 @@
     private AsyncClockEventDelegate(Context context) {
         super(context);
         mContext = context;
-        mReceiver.registerAsync(mContext, ACTION_TIME_CHANGED, ACTION_TIMEZONE_CHANGED);
+        mReceiver.register(mContext, ACTION_TIME_CHANGED, ACTION_TIMEZONE_CHANGED);
     }
 
     @Override
@@ -123,6 +125,6 @@
     public void close() {
         mDestroyed = true;
         SettingsCache.INSTANCE.get(mContext).unregister(mFormatUri, this);
-        mReceiver.unregisterReceiverSafelyAsync(mContext);
+        mReceiver.unregisterReceiverSafely(mContext);
     }
 }
diff --git a/quickstep/src/com/android/quickstep/util/BorderAnimator.kt b/quickstep/src/com/android/quickstep/util/BorderAnimator.kt
index 85238ed..7e51fcf 100644
--- a/quickstep/src/com/android/quickstep/util/BorderAnimator.kt
+++ b/quickstep/src/com/android/quickstep/util/BorderAnimator.kt
@@ -50,7 +50,7 @@
     private val disappearanceDurationMs: Long,
     private val interpolator: Interpolator,
 ) {
-    private val borderAnimationProgress = AnimatedFloat { updateOutline() }
+    private val borderAnimationProgress = AnimatedFloat { _ -> updateOutline() }
     private val borderPaint =
         Paint(Paint.ANTI_ALIAS_FLAG).apply {
             color = borderColor
@@ -224,6 +224,7 @@
 
         val borderWidth: Float
             get() = borderWidthPx * animationProgress
+
         val alignmentAdjustment: Float
             // Outset the border by half the width to create an outwards-growth animation
             get() = -borderWidth / 2f + alignmentAdjustmentInset
diff --git a/quickstep/src/com/android/quickstep/util/DesktopTask.java b/quickstep/src/com/android/quickstep/util/DesktopTask.java
index 8d99069..a727aa2 100644
--- a/quickstep/src/com/android/quickstep/util/DesktopTask.java
+++ b/quickstep/src/com/android/quickstep/util/DesktopTask.java
@@ -18,10 +18,11 @@
 
 import androidx.annotation.NonNull;
 
-import com.android.quickstep.views.TaskView;
+import com.android.quickstep.views.TaskViewType;
 import com.android.systemui.shared.recents.model.Task;
 
 import java.util.List;
+import java.util.Objects;
 
 /**
  * A {@link Task} container that can contain N number of tasks that are part of the desktop in
@@ -33,7 +34,7 @@
     public final List<Task> tasks;
 
     public DesktopTask(@NonNull List<Task> tasks) {
-        super(tasks.get(0), null, null, TaskView.Type.DESKTOP);
+        super(tasks.get(0), null, null, TaskViewType.DESKTOP);
         this.tasks = tasks;
     }
 
@@ -68,4 +69,16 @@
         return "type=" + taskViewType + " tasks=" + tasks;
     }
 
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof DesktopTask that)) return false;
+        if (!super.equals(o)) return false;
+        return Objects.equals(tasks, that.tasks);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(super.hashCode(), tasks);
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/util/GroupTask.java b/quickstep/src/com/android/quickstep/util/GroupTask.java
index 945ffe3..fba08a9 100644
--- a/quickstep/src/com/android/quickstep/util/GroupTask.java
+++ b/quickstep/src/com/android/quickstep/util/GroupTask.java
@@ -20,12 +20,13 @@
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds;
-import com.android.quickstep.views.TaskView;
+import com.android.quickstep.views.TaskViewType;
 import com.android.systemui.shared.recents.model.Task;
 
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.Objects;
 
 /**
  * A {@link Task} container that can contain one or two tasks, depending on if the two tasks
@@ -38,19 +39,18 @@
     public final Task task2;
     @Nullable
     public final SplitBounds mSplitBounds;
-    @TaskView.Type
-    public final int taskViewType;
+    public final TaskViewType taskViewType;
 
     public GroupTask(@NonNull Task task) {
         this(task, null, null);
     }
 
     public GroupTask(@NonNull Task t1, @Nullable Task t2, @Nullable SplitBounds splitBounds) {
-        this(t1, t2, splitBounds, t2 != null ? TaskView.Type.GROUPED : TaskView.Type.SINGLE);
+        this(t1, t2, splitBounds, t2 != null ? TaskViewType.GROUPED : TaskViewType.SINGLE);
     }
 
     protected GroupTask(@NonNull Task t1, @Nullable Task t2, @Nullable SplitBounds splitBounds,
-            @TaskView.Type int taskViewType) {
+            TaskViewType taskViewType) {
         task1 = t1;
         task2 = t2;
         mSplitBounds = splitBounds;
@@ -91,4 +91,17 @@
         return "type=" + taskViewType + " task1=" + task1 + " task2=" + task2;
     }
 
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof GroupTask that)) return false;
+        return taskViewType == that.taskViewType && Objects.equals(task1,
+                that.task1) && Objects.equals(task2, that.task2)
+                && Objects.equals(mSplitBounds, that.mSplitBounds);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(task1, task2, mSplitBounds, taskViewType);
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/util/InputConsumerProxy.java b/quickstep/src/com/android/quickstep/util/InputConsumerProxy.java
index cb44a1a..fcf9ab1 100644
--- a/quickstep/src/com/android/quickstep/util/InputConsumerProxy.java
+++ b/quickstep/src/com/android/quickstep/util/InputConsumerProxy.java
@@ -108,19 +108,28 @@
             return false;
         }
 
+        final SimpleOrientationTouchTransformer touchTransformer =
+                SimpleOrientationTouchTransformer.INSTANCE.get(mContext);
+        final int viewRotation = mRotationSupplier.get();
+        final boolean needTransform = viewRotation != ev.getSurfaceRotation();
         if (action == ACTION_DOWN) {
             mTouchInProgress = true;
+            if (needTransform) {
+                touchTransformer.updateTouchingOrientation(viewRotation);
+            }
             initInputConsumerIfNeeded(/* isFromTouchDown= */ true);
         } else if (action == ACTION_CANCEL || action == ACTION_UP) {
             // Finish any pending actions
             mTouchInProgress = false;
+            touchTransformer.clearTouchingOrientation();
             if (mDestroyPending) {
                 destroy();
             }
         }
         if (mInputConsumer != null) {
-            SimpleOrientationTouchTransformer.INSTANCE.get(mContext).transform(ev,
-                    mRotationSupplier.get());
+            if (needTransform) {
+                touchTransformer.transform(ev, viewRotation);
+            }
             mInputConsumer.onMotionEvent(ev);
         }
 
diff --git a/quickstep/src/com/android/quickstep/util/SlideInRemoteTransition.kt b/quickstep/src/com/android/quickstep/util/SlideInRemoteTransition.kt
index dbeedd3..ece9583 100644
--- a/quickstep/src/com/android/quickstep/util/SlideInRemoteTransition.kt
+++ b/quickstep/src/com/android/quickstep/util/SlideInRemoteTransition.kt
@@ -36,6 +36,8 @@
     private val pageSpacing: Int,
     private val cornerRadius: Float,
     private val interpolator: TimeInterpolator,
+    private val onStartCallback: Runnable,
+    private val onFinishCallback: Runnable,
 ) : RemoteTransitionStub() {
     private val animationDurationMs = 500L
 
@@ -68,6 +70,7 @@
                 startT.setCrop(leash, chg.endAbsBounds).setCornerRadius(leash, cornerRadius)
             }
         }
+        onStartCallback.run()
         startT.apply()
 
         anim.addUpdateListener {
@@ -97,6 +100,7 @@
                     val t = Transaction()
                     try {
                         finishCB.onTransitionFinished(null, t)
+                        onFinishCallback.run()
                     } catch (e: RemoteException) {
                         // Ignore
                     }
diff --git a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
index c1b7575..e31a828 100644
--- a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
+++ b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
@@ -27,8 +27,10 @@
 import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW
 import android.content.Context
 import android.graphics.Bitmap
+import android.graphics.Color
 import android.graphics.Rect
 import android.graphics.RectF
+import android.graphics.drawable.ColorDrawable
 import android.graphics.drawable.Drawable
 import android.view.RemoteAnimationTarget
 import android.view.SurfaceControl
@@ -68,9 +70,9 @@
 import com.android.quickstep.views.RecentsView
 import com.android.quickstep.views.RecentsViewContainer
 import com.android.quickstep.views.SplitInstructionsView
+import com.android.quickstep.views.TaskContainer
 import com.android.quickstep.views.TaskThumbnailViewDeprecated
 import com.android.quickstep.views.TaskView
-import com.android.quickstep.views.TaskView.TaskContainer
 import com.android.quickstep.views.TaskViewIcon
 import com.android.wm.shell.shared.TransitionUtil
 import java.util.Optional
@@ -120,8 +122,8 @@
                     val drawable = getDrawable(container.iconView, splitSelectSource)
                     return SplitAnimInitProps(
                         container.snapshotView,
-                        container.thumbnail,
-                        drawable!!,
+                        container.splitAnimationThumbnail,
+                        drawable,
                         fadeWithThumbnail = true,
                         isStagedTask = true,
                         iconView = container.iconView.asView()
@@ -139,8 +141,8 @@
                 val drawable = getDrawable(it.iconView, splitSelectSource)
                 return SplitAnimInitProps(
                     it.snapshotView,
-                    it.thumbnail,
-                    drawable!!,
+                    it.splitAnimationThumbnail,
+                    drawable,
                     fadeWithThumbnail = true,
                     isStagedTask = true,
                     iconView = it.iconView.asView()
@@ -152,15 +154,15 @@
     /**
      * Returns the drawable that's provided in iconView, however if that is null it falls back to
      * the drawable that's in splitSelectSource. TaskView's icon drawable can be null if the
-     * TaskView is scrolled far enough off screen
+     * TaskView is scrolled far enough off screen.
      *
-     * @return [Drawable]
+     * @return the [Drawable] icon, or a translucent drawable if none was found
      */
-    fun getDrawable(iconView: TaskViewIcon, splitSelectSource: SplitSelectSource?): Drawable? {
-        if (iconView.drawable == null && splitSelectSource != null) {
-            return splitSelectSource.drawable
-        }
-        return iconView.drawable
+    fun getDrawable(iconView: TaskViewIcon, splitSelectSource: SplitSelectSource?): Drawable {
+        val drawable =
+            if (iconView.drawable == null && splitSelectSource != null) splitSelectSource.drawable
+            else iconView.drawable
+        return drawable ?: ColorDrawable(Color.TRANSPARENT)
     }
 
     /**
@@ -534,7 +536,13 @@
             val appPairLaunchingAppIndex = hasChangesForBothAppPairs(launchingIconView, info)
             if (appPairLaunchingAppIndex == -1) {
                 // Launch split app pair animation
-                composeIconSplitLaunchAnimator(launchingIconView, info, t, finishCallback)
+                composeIconSplitLaunchAnimator(
+                    launchingIconView,
+                    info,
+                    t,
+                    finishCallback,
+                    cornerRadius
+                )
             } else {
                 composeFullscreenIconSplitLaunchAnimator(
                     launchingIconView,
@@ -551,8 +559,14 @@
                     "unexpected null"
             }
 
-            composeFadeInSplitLaunchAnimator(initialTaskId, secondTaskId, info, t, finishCallback,
-                    cornerRadius)
+            composeFadeInSplitLaunchAnimator(
+                initialTaskId,
+                secondTaskId,
+                info,
+                t,
+                finishCallback,
+                cornerRadius
+            )
         }
     }
 
@@ -652,15 +666,15 @@
      * To find the root shell leash that we want to fade in, we do the following: The Changes we
      * receive in transitionInfo are structured like this
      *
-     *     Root (grandparent)
+     *     (0) Root (grandparent)
      *     |
-     *     |--> Split Root 1 (left/top side parent) (WINDOWING_MODE_MULTI_WINDOW)
+     *     |--> (1) Split Root 1 (left/top side parent) (WINDOWING_MODE_MULTI_WINDOW)
      *     |   |
-     *     |    --> App 1 (left/top side child) (WINDOWING_MODE_MULTI_WINDOW)
+     *     |    --> (1a) App 1 (left/top side child) (WINDOWING_MODE_MULTI_WINDOW)
      *     |--> Divider
-     *     |--> Split Root 2 (right/bottom side parent) (WINDOWING_MODE_MULTI_WINDOW)
+     *     |--> (2) Split Root 2 (right/bottom side parent) (WINDOWING_MODE_MULTI_WINDOW)
      *         |
-     *          --> App 2 (right/bottom side child) (WINDOWING_MODE_MULTI_WINDOW)
+     *          --> (2a) App 2 (right/bottom side child) (WINDOWING_MODE_MULTI_WINDOW)
      *
      * We want to animate the Root (grandparent) so that it affects both apps and the divider. To do
      * this, we find one of the nodes with WINDOWING_MODE_MULTI_WINDOW (one of the left-side ones,
@@ -675,7 +689,8 @@
         launchingIconView: AppPairIcon,
         transitionInfo: TransitionInfo,
         t: Transaction,
-        finishCallback: Runnable
+        finishCallback: Runnable,
+        windowRadius: Float
     ) {
         // If launching an app pair from Taskbar inside of an app context (no access to Launcher),
         // use the scale-up animation
@@ -695,48 +710,26 @@
 
         // Create an AnimatorSet that will run both shell and launcher transitions together
         val launchAnimation = AnimatorSet()
-        var rootCandidate: Change? = null
 
-        for (change in transitionInfo.changes) {
-            val taskInfo: RunningTaskInfo = change.taskInfo ?: continue
+        val splitRoots: Pair<Change, List<Change>>? =
+            SplitScreenUtils.extractTopParentAndChildren(transitionInfo)
+        check(splitRoots != null) { "Could not find split roots" }
 
-            // TODO (b/316490565): Replace this logic when SplitBounds is available to
-            //  startAnimation() and we can know the precise taskIds of launching tasks.
-            // Find a change that has WINDOWING_MODE_MULTI_WINDOW.
-            if (
-                taskInfo.windowingMode == WINDOWING_MODE_MULTI_WINDOW &&
-                    (change.mode == TRANSIT_OPEN || change.mode == TRANSIT_TO_FRONT)
-            ) {
-                // Check if it is a left/top app.
-                val isLeftTopApp =
-                    (dp.isLeftRightSplit && change.endAbsBounds.left == 0) ||
-                        (!dp.isLeftRightSplit && change.endAbsBounds.top == 0)
-                if (isLeftTopApp) {
-                    // Found one!
-                    rootCandidate = change
-                    break
-                }
-            }
-        }
-
-        // If we could not find a proper root candidate, something went wrong.
-        check(rootCandidate != null) { "Could not find a split root candidate" }
+        // Will point to change (0) in diagram above
+        val mainRootCandidate = splitRoots.first
+        // Will contain changes (1) and (2) in diagram above
+        val leafRoots: List<Change> = splitRoots.second
 
         // Find the place where our left/top app window meets the divider (used for the
         // launcher side animation)
+        val leftTopApp =
+            leafRoots.single { change ->
+                (dp.isLeftRightSplit && change.endAbsBounds.left == 0) ||
+                    (!dp.isLeftRightSplit && change.endAbsBounds.top == 0)
+            }
         val dividerPos =
-            if (dp.isLeftRightSplit) rootCandidate.endAbsBounds.right
-            else rootCandidate.endAbsBounds.bottom
-
-        // Recurse up the tree until parent is null, then we've found our root.
-        var parentToken: WindowContainerToken? = rootCandidate.parent
-        while (parentToken != null) {
-            rootCandidate = transitionInfo.getChange(parentToken) ?: break
-            parentToken = rootCandidate.parent
-        }
-
-        // Make sure nothing weird happened, like getChange() returning null.
-        check(rootCandidate != null) { "Failed to find a root leash" }
+            if (dp.isLeftRightSplit) leftTopApp.endAbsBounds.right
+            else leftTopApp.endAbsBounds.bottom
 
         // Create a new floating view in Launcher, positioned above the launching icon
         val drawableArea = launchingIconView.iconDrawableArea
@@ -755,9 +748,26 @@
             )
         floatingView.bringToFront()
 
-        launchAnimation.play(
-            getIconLaunchValueAnimator(t, dp, finishCallback, launcher, floatingView, rootCandidate)
+        val iconLaunchValueAnimator =
+            getIconLaunchValueAnimator(
+                t,
+                dp,
+                finishCallback,
+                launcher,
+                floatingView,
+                mainRootCandidate
+            )
+        iconLaunchValueAnimator.addListener(
+            object : AnimatorListenerAdapter() {
+                override fun onAnimationStart(animation: Animator, isReverse: Boolean) {
+                    for (c in leafRoots) {
+                        t.setCornerRadius(c.leash, windowRadius)
+                        t.apply()
+                    }
+                }
+            }
         )
+        launchAnimation.play(iconLaunchValueAnimator)
         launchAnimation.start()
     }
 
@@ -1026,12 +1036,12 @@
      */
     @VisibleForTesting
     fun composeFadeInSplitLaunchAnimator(
-            initialTaskId: Int,
-            secondTaskId: Int,
-            transitionInfo: TransitionInfo,
-            t: Transaction,
-            finishCallback: Runnable,
-            cornerRadius: Float
+        initialTaskId: Int,
+        secondTaskId: Int,
+        transitionInfo: TransitionInfo,
+        t: Transaction,
+        finishCallback: Runnable,
+        cornerRadius: Float
     ) {
         var splitRoot1: Change? = null
         var splitRoot2: Change? = null
@@ -1109,7 +1119,7 @@
                 override fun onAnimationStart(animation: Animator) {
                     for (leash in openingTargets) {
                         animTransaction.show(leash).setAlpha(leash, 0.0f)
-                        animTransaction.setCornerRadius(leash, cornerRadius);
+                        animTransaction.setCornerRadius(leash, cornerRadius)
                     }
                     animTransaction.apply()
                 }
diff --git a/quickstep/src/com/android/quickstep/util/SplitScreenUtils.kt b/quickstep/src/com/android/quickstep/util/SplitScreenUtils.kt
index 4820c35..d58cb91 100644
--- a/quickstep/src/com/android/quickstep/util/SplitScreenUtils.kt
+++ b/quickstep/src/com/android/quickstep/util/SplitScreenUtils.kt
@@ -16,11 +16,21 @@
 
 package com.android.quickstep.util
 
+import android.util.Log
+import android.view.WindowManager
+import android.view.WindowManager.TRANSIT_OPEN
+import android.view.WindowManager.TRANSIT_TO_FRONT
+import android.window.TransitionInfo
+import android.window.TransitionInfo.Change
+import android.window.TransitionInfo.FLAG_FIRST_CUSTOM
 import com.android.launcher3.util.SplitConfigurationOptions
 import com.android.wm.shell.util.SplitBounds
+import java.lang.IllegalStateException
 
 class SplitScreenUtils {
     companion object {
+        private const val TAG = "SplitScreenUtils"
+
         // TODO(b/254378592): Remove these methods when the two classes are reunited
         /** Converts the shell version of SplitBounds to the launcher version */
         @JvmStatic
@@ -39,5 +49,51 @@
                 )
             }
         }
+
+        /**
+         * Given a TransitionInfo, generates the tree structure for those changes and extracts out
+         * the top most root and it's two immediate children.
+         * Changes can be provided in any order.
+         *
+         * @return a [Pair] where first -> top most split root,
+         *         second -> [List] of 2, leftTop/bottomRight stage roots
+         */
+        fun extractTopParentAndChildren(transitionInfo: TransitionInfo):
+                Pair<Change, List<Change>>? {
+            val parentToChildren = mutableMapOf<Change, MutableList<Change>>()
+            val hasParent = mutableSetOf<Change>()
+            // filter out anything that isn't opening and the divider
+            val taskChanges: List<Change> = transitionInfo.changes
+                    .filter { change -> (change.mode == TRANSIT_OPEN ||
+                            change.mode == TRANSIT_TO_FRONT) && change.flags < FLAG_FIRST_CUSTOM}
+                    .toList()
+
+            // 1. Build Parent-Child Relationships
+            for (change in taskChanges) {
+                // TODO (b/316490565): Replace this logic when SplitBounds is available to
+                //  startAnimation() and we can know the precise taskIds of launching tasks.
+                change.parent?.let { parent ->
+                    parentToChildren
+                            .getOrPut(transitionInfo.getChange(parent)!!) { mutableListOf() }
+                            .add(change)
+                    hasParent.add(change)
+                }
+            }
+
+            // 2. Find Top Parent
+            val topParent = taskChanges.firstOrNull { it !in hasParent }
+
+            // 3. Extract Immediate Children
+            return if (topParent != null) {
+                val immediateChildren = parentToChildren.getOrDefault(topParent, emptyList())
+                if (immediateChildren.size != 2) {
+                    throw IllegalStateException("incorrect split stage root size")
+                }
+                Pair(topParent, immediateChildren)
+            } else {
+                Log.w(TAG, "No top parent found")
+                null
+            }
+        }
     }
 }
diff --git a/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java b/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java
index 5e42b90..27fb31d 100644
--- a/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java
@@ -138,12 +138,13 @@
                     mLauncher, mLauncher.getDragLayer(),
                     controller.screenshotTask(runningTaskInfo.taskId).getThumbnail(),
                     null /* icon */, startingTaskRect);
+            Task task = Task.from(new Task.TaskKey(runningTaskInfo), runningTaskInfo,
+                    false /* isLocked */);
             RecentsModel.INSTANCE.get(mLauncher.getApplicationContext())
                     .getIconCache()
-                    .updateIconInBackground(
-                            Task.from(new Task.TaskKey(runningTaskInfo), runningTaskInfo,
-                                    false /* isLocked */),
-                            (task) -> floatingTaskView.setIcon(task.icon));
+                    .getIconInBackground(
+                            task,
+                            (icon, contentDescription, title) -> floatingTaskView.setIcon(icon));
             floatingTaskView.setAlpha(1);
             floatingTaskView.addStagingAnimation(anim, startingTaskRect, mTempRect,
                     false /* fadeWithThumbnail */, true /* isStagedTask */);
diff --git a/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java b/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
index 88c3a08..56e91ed 100644
--- a/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
+++ b/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
@@ -139,6 +139,10 @@
 
         final float aspectRatio = destinationBounds.width() / (float) destinationBounds.height();
         String reasonForCreateOverlay = null; // For debugging purpose.
+
+        // Slightly larger app bounds to allow for off by 1 pixel source-rect-hint errors.
+        Rect overflowAppBounds = new Rect(appBounds.left - 1, appBounds.top - 1,
+                        appBounds.right + 1, appBounds.bottom + 1);
         if (sourceRectHint.isEmpty()) {
             reasonForCreateOverlay = "Source rect hint is empty";
         } else if (sourceRectHint.width() < destinationBounds.width()
@@ -149,7 +153,7 @@
             // animation in this case.
             reasonForCreateOverlay = "Source rect hint is too small " + sourceRectHint;
             sourceRectHint.setEmpty();
-        } else if (!appBounds.contains(sourceRectHint)) {
+        } else if (!overflowAppBounds.contains(sourceRectHint)) {
             // This is a situation in which the source hint rect is outside the app bounds, so it is
             // not a valid rectangle to use for cropping app surface
             reasonForCreateOverlay = "Source rect hint exceeds display bounds " + sourceRectHint;
@@ -453,8 +457,9 @@
             }
             // adjust the mSourceRectHint / mAppBounds by display cutout if applicable.
             if (mSourceRectHint != null && mDisplayCutoutInsets != null) {
-                if (mFromRotation == Surface.ROTATION_0 && mDisplayCutoutInsets.top >= 0) {
-                    // TODO: this is to special case the issues on Pixel Foldable device(s).
+                if (mFromRotation == Surface.ROTATION_0) {
+                    // TODO: this is to special case the issues on Foldable device
+                    // with display cutout.
                     mSourceRectHint.offset(mDisplayCutoutInsets.left, mDisplayCutoutInsets.top);
                 } else if (mFromRotation == Surface.ROTATION_90) {
                     mSourceRectHint.offset(mDisplayCutoutInsets.left, mDisplayCutoutInsets.top);
diff --git a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
index 46ed2ee..9ce2277 100644
--- a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
@@ -24,9 +24,11 @@
 import android.graphics.drawable.shapes.RoundRectShape
 import android.util.AttributeSet
 import android.util.Log
+import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
 import androidx.core.view.updateLayoutParams
+import com.android.launcher3.Flags.enableRefactorTaskThumbnail
 import com.android.launcher3.R
 import com.android.launcher3.util.RunnableList
 import com.android.launcher3.util.SplitConfigurationOptions
@@ -40,7 +42,7 @@
 
 /** TaskView that contains all tasks that are part of the desktop. */
 class DesktopTaskView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
-    TaskView(context, attrs) {
+    TaskView(context, attrs, type = TaskViewType.DESKTOP) {
 
     private val snapshotDrawParams =
         object : FullscreenDrawParams(context) {
@@ -48,11 +50,11 @@
             override fun computeTaskCornerRadius(context: Context) =
                 computeWindowCornerRadius(context)
         }
-    private val taskThumbnailViewPool =
+    private val taskThumbnailViewDeprecatedPool =
         ViewPool<TaskThumbnailViewDeprecated>(
             context,
             this,
-            R.layout.task_thumbnail,
+            R.layout.task_thumbnail_deprecated,
             VIEW_POOL_MAX_SIZE,
             VIEW_POOL_INITIAL_SIZE
         )
@@ -89,6 +91,65 @@
         childCountAtInflation = childCount
     }
 
+    /** Updates this desktop task to the gives task list defined in `tasks` */
+    fun bind(
+        tasks: List<Task>,
+        orientedState: RecentsOrientedState,
+        taskOverlayFactory: TaskOverlayFactory
+    ) {
+        if (DEBUG) {
+            val sb = StringBuilder()
+            sb.append("bind tasks=").append(tasks.size).append("\n")
+            tasks.forEach { sb.append(" key=${it.key}\n") }
+            Log.d(TAG, sb.toString())
+        }
+        cancelPendingLoadTasks()
+        taskContainers =
+            tasks.map { task ->
+                val snapshotView =
+                    if (enableRefactorTaskThumbnail()) {
+                        LayoutInflater.from(context).inflate(R.layout.task_thumbnail, this, false)
+                    } else {
+                        taskThumbnailViewDeprecatedPool.view
+                    }
+
+                addView(
+                    snapshotView,
+                    // Add snapshotView to the front after initial views e.g. icon and
+                    // background.
+                    childCountAtInflation,
+                    LayoutParams(
+                        ViewGroup.LayoutParams.WRAP_CONTENT,
+                        ViewGroup.LayoutParams.WRAP_CONTENT
+                    )
+                )
+                TaskContainer(
+                    this,
+                    task,
+                    snapshotView,
+                    iconView,
+                    TransformingTouchDelegate(iconView.asView()),
+                    SplitConfigurationOptions.STAGE_POSITION_UNDEFINED,
+                    digitalWellBeingToast = null,
+                    showWindowsView = null,
+                    taskOverlayFactory
+                )
+            }
+        taskContainers.forEach { it.bind() }
+        setOrientationState(orientedState)
+    }
+
+    override fun onRecycle() {
+        super.onRecycle()
+        visibility = VISIBLE
+        taskContainers.forEach {
+            if (!enableRefactorTaskThumbnail()) {
+                removeView(it.thumbnailViewDeprecated)
+                taskThumbnailViewDeprecatedPool.recycle(it.thumbnailViewDeprecated)
+            }
+        }
+    }
+
     override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
         super.onMeasure(widthMeasureSpec, heightMeasureSpec)
         val containerWidth = MeasureSpec.getSize(widthMeasureSpec)
@@ -151,76 +212,6 @@
         }
     }
 
-    override fun onRecycle() {
-        super.onRecycle()
-        visibility = VISIBLE
-    }
-
-    /** Updates this desktop task to the gives task list defined in `tasks` */
-    fun bind(
-        tasks: List<Task>,
-        orientedState: RecentsOrientedState,
-        taskOverlayFactory: TaskOverlayFactory
-    ) {
-        if (DEBUG) {
-            val sb = StringBuilder()
-            sb.append("bind tasks=").append(tasks.size).append("\n")
-            tasks.forEach { sb.append(" key=${it.key}\n") }
-            Log.d(TAG, sb.toString())
-        }
-        cancelPendingLoadTasks()
-
-        if (!isTaskContainersInitialized()) {
-            taskContainers = arrayListOf()
-        }
-        val taskContainers = taskContainers as ArrayList
-        taskContainers.ensureCapacity(tasks.size)
-        tasks.forEachIndexed { index, task ->
-            val thumbnailViewDeprecated: TaskThumbnailViewDeprecated
-            if (index >= taskContainers.size) {
-                thumbnailViewDeprecated = taskThumbnailViewPool.view
-                // Add thumbnailView from to position after the initial child views.
-                addView(
-                    thumbnailViewDeprecated,
-                    childCountAtInflation,
-                    LayoutParams(
-                        ViewGroup.LayoutParams.WRAP_CONTENT,
-                        ViewGroup.LayoutParams.WRAP_CONTENT
-                    )
-                )
-            } else {
-                thumbnailViewDeprecated = taskContainers[index].thumbnailViewDeprecated
-            }
-            val taskContainer =
-                TaskContainer(
-                    task,
-                    // TODO(b/338360089): Support new TTV for DesktopTaskView
-                    thumbnailView = null,
-                    thumbnailViewDeprecated,
-                    iconView,
-                    TransformingTouchDelegate(iconView.asView()),
-                    SplitConfigurationOptions.STAGE_POSITION_UNDEFINED,
-                    digitalWellBeingToast = null,
-                    showWindowsView = null,
-                    taskOverlayFactory
-                )
-            if (index >= taskContainers.size) {
-                taskContainers.add(taskContainer)
-            } else {
-                taskContainers[index] = taskContainer
-            }
-            taskContainer.bind()
-        }
-        repeat(taskContainers.size - tasks.size) {
-            with(taskContainers.removeLast()) {
-                removeView(thumbnailViewDeprecated)
-                taskThumbnailViewPool.recycle(thumbnailViewDeprecated)
-            }
-        }
-
-        setOrientationState(orientedState)
-    }
-
     override fun needsUpdate(dataChange: Int, flag: Int) =
         if (flag == FLAG_UPDATE_THUMBNAIL) super.needsUpdate(dataChange, flag) else false
 
diff --git a/quickstep/src/com/android/quickstep/views/FloatingAppPairBackground.kt b/quickstep/src/com/android/quickstep/views/FloatingAppPairBackground.kt
index e024995..6bbd6b2 100644
--- a/quickstep/src/com/android/quickstep/views/FloatingAppPairBackground.kt
+++ b/quickstep/src/com/android/quickstep/views/FloatingAppPairBackground.kt
@@ -57,6 +57,7 @@
 
     private val container: RecentsViewContainer
     private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG)
+    private val dividerPaint = Paint(Paint.ANTI_ALIAS_FLAG)
 
     // Animation interpolators
     protected val expandXInterpolator: Interpolator
@@ -105,13 +106,15 @@
             )
 
         // Find device-specific measurements
-        deviceCornerRadius = QuickStepContract.getWindowCornerRadius(container.asContext())
+        val resources = context.resources
+        deviceCornerRadius = QuickStepContract.getWindowCornerRadius(context)
         deviceHalfDividerSize =
-                container.asContext().resources.getDimensionPixelSize(R.dimen.multi_window_task_divider_size) / 2f
+                resources.getDimensionPixelSize(R.dimen.multi_window_task_divider_size) / 2f
         val dividerCenterPos = dividerPos + deviceHalfDividerSize
         desiredSplitRatio =
             if (dp.isLeftRightSplit) dividerCenterPos / dp.widthPx
             else dividerCenterPos / dp.heightPx
+        dividerPaint.color = resources.getColor(R.color.taskbar_background_dark, null /*theme*/)
     }
 
     override fun draw(canvas: Canvas) {
@@ -153,8 +156,12 @@
         val leftSide = RectF(0f, 0f, dividerCenterPos - changingDividerSize, height)
         // The right half of the background image
         val rightSide = RectF(dividerCenterPos + changingDividerSize, 0f, width, height)
+        // Middle part is for divider background
+        val middleRect = RectF(leftSide.right - deviceHalfDividerSize, 0f,
+                rightSide.left + deviceHalfDividerSize, height)
 
         // Draw background
+        canvas.drawRect(middleRect, dividerPaint)
         drawCustomRoundedRect(
             canvas,
             leftSide,
@@ -251,8 +258,12 @@
         val topSide = RectF(0f, 0f, width, dividerCenterPos - changingDividerSize)
         // The bottom half of the background image
         val bottomSide = RectF(0f, dividerCenterPos + changingDividerSize, width, height)
+        // Middle part is for divider background
+        val middleRect = RectF(0f, topSide.bottom - deviceHalfDividerSize,
+                width, bottomSide.top + deviceHalfDividerSize)
 
         // Draw background
+        canvas.drawRect(middleRect, dividerPaint)
         drawCustomRoundedRect(
             canvas,
             topSide,
diff --git a/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt b/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
index b070244..6523ba7 100644
--- a/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
@@ -49,7 +49,7 @@
  * (Icon loading sold separately, fees may apply. Shipping & Handling for Overlays not included).
  */
 class GroupedTaskView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
-    TaskView(context, attrs) {
+    TaskView(context, attrs, type = TaskViewType.GROUPED) {
     // TODO(b/336612373): Support new TTV for GroupedTaskView
     var splitBoundsConfig: SplitConfigurationOptions.SplitBounds? = null
         private set
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index c1e112a..3273809 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -135,7 +135,6 @@
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.BaseActivity.MultiWindowModeChangedListener;
 import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.Flags;
 import com.android.launcher3.Insettable;
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.PagedView;
@@ -212,7 +211,6 @@
 import com.android.quickstep.util.TaskVisualsChangeListener;
 import com.android.quickstep.util.TransformParams;
 import com.android.quickstep.util.VibrationConstants;
-import com.android.quickstep.views.TaskView.TaskContainer;
 import com.android.systemui.plugins.ResourceProvider;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.ThumbnailData;
@@ -223,17 +221,19 @@
 import com.android.systemui.shared.system.TaskStackChangeListeners;
 import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource;
 import com.android.wm.shell.common.pip.IPipAnimationListener;
-import com.android.wm.shell.shared.DesktopModeStatus;
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
 
 import kotlin.Unit;
 
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
+import java.util.Set;
 import java.util.function.Consumer;
 import java.util.stream.Collectors;
 
@@ -461,7 +461,9 @@
 
     private static final float FOREGROUND_SCRIM_TINT = 0.32f;
 
+    @Nullable
     public final RecentsViewData mRecentsViewData = new RecentsViewData();
+    @Nullable
     public final TasksRepository mTasksRepository;
 
     protected final RecentsOrientedState mOrientationState;
@@ -793,6 +795,13 @@
 
     private int mOffsetMidpointIndexOverride = INVALID_PAGE;
 
+    /**
+     * Whether or not any task has been dismissed i.e. swiped away by the user, in the lifetime of
+     * RecentsView being open and displayed to the user. It is reset in the {@link #reset()} method
+     * i.e. when RecentsView closes.
+     */
+    private boolean mAnyTaskHasBeenDismissed;
+
     public RecentsView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
             BaseContainerInterface sizeStrategy) {
         super(context, attrs, defStyleAttr);
@@ -1016,14 +1025,17 @@
     @Nullable
     public Task onTaskThumbnailChanged(int taskId, ThumbnailData thumbnailData) {
         if (mHandleTaskStackChanges) {
-            TaskView taskView = getTaskViewByTaskId(taskId);
-            if (taskView != null) {
-                for (TaskContainer container : taskView.getTaskContainers()) {
-                    if (container == null || taskId != container.getTask().key.id) {
-                        continue;
+            // TODO(b/342560598): Handle onTaskThumbnailChanged for new TTV.
+            if (!enableRefactorTaskThumbnail()) {
+                TaskView taskView = getTaskViewByTaskId(taskId);
+                if (taskView != null) {
+                    for (TaskContainer container : taskView.getTaskContainers()) {
+                        if (container == null || taskId != container.getTask().key.id) {
+                            continue;
+                        }
+                        container.getThumbnailViewDeprecated().setThumbnail(container.getTask(),
+                                thumbnailData);
                     }
-                    container.getThumbnailViewDeprecated().setThumbnail(container.getTask(),
-                            thumbnailData);
                 }
             }
         }
@@ -1060,6 +1072,10 @@
     @Nullable
     public TaskView updateThumbnail(
             HashMap<Integer, ThumbnailData> thumbnailData, boolean refreshNow) {
+        if (enableRefactorTaskThumbnail()) {
+            // TODO(b/342560598): Handle updateThumbnail for new TTV.
+            return null;
+        }
         TaskView updatedTaskView = null;
         for (Map.Entry<Integer, ThumbnailData> entry : thumbnailData.entrySet()) {
             Integer id = entry.getKey();
@@ -1817,7 +1833,7 @@
             // If we need to remove half of a pair of tasks, force a TaskView with Type.SINGLE
             // to be a temporary container for the remaining task.
             TaskView taskView = getTaskViewFromPool(
-                    isRemovalNeeded ? TaskView.Type.SINGLE : groupTask.taskViewType);
+                    isRemovalNeeded ? TaskViewType.SINGLE : groupTask.taskViewType);
             if (taskView instanceof GroupedTaskView) {
                 boolean firstTaskIsLeftTopTask =
                         groupTask.mSplitBounds.leftTopTaskId == groupTask.task1.key.id;
@@ -2172,15 +2188,6 @@
      * Updates TaskView scaling and translation required to support variable width.
      */
     private void updateTaskSize() {
-        updateTaskSize(false);
-    }
-
-    /**
-     * Updates TaskView scaling and translation required to support variable width.
-     *
-     * @param isTaskDismissal indicates if update was called due to task dismissal
-     */
-    private void updateTaskSize(boolean isTaskDismissal) {
         final int taskCount = getTaskViewCount();
         if (taskCount == 0) {
             return;
@@ -2207,7 +2214,7 @@
 
         mClearAllButton.setFullscreenTranslationPrimary(accumulatedTranslationX);
 
-        updateGridProperties(isTaskDismissal);
+        updateGridProperties();
     }
 
     public void getTaskSize(Rect outRect) {
@@ -2391,12 +2398,11 @@
             if (containers.isEmpty()) {
                 continue;
             }
-            int index = indexOfChild(taskView);
             boolean visible;
             if (showAsGrid()) {
                 visible = isTaskViewWithinBounds(taskView, visibleStart, visibleEnd);
             } else {
-                visible = lower <= index && index <= upper;
+                visible = lower <= i && i <= upper;
             }
             if (visible) {
                 // Default update all non-null tasks, then remove running ones
@@ -2427,7 +2433,7 @@
                         }
                         taskView.onTaskListVisibilityChanged(true /* visible */, changes);
                     }
-                    mHasVisibleTaskData.put(task.key.id, visible);
+                    mHasVisibleTaskData.put(task.key.id, true);
                 }
             } else {
                 for (TaskContainer container : containers) {
@@ -2508,6 +2514,7 @@
         mIgnoreResetTaskId = -1;
         mTaskListChangeId = -1;
         mFocusedTaskViewId = -1;
+        mAnyTaskHasBeenDismissed = false;
 
         Log.d(TAG, "reset - mEnableDrawingLiveTile: " + mEnableDrawingLiveTile
                 + ", mRecentsAnimationController: " + mRecentsAnimationController);
@@ -2594,16 +2601,16 @@
      * Handle the edge case where Recents could increment task count very high over long
      * period of device usage. Probably will never happen, but meh.
      */
-    private TaskView getTaskViewFromPool(@TaskView.Type int type) {
+    private TaskView getTaskViewFromPool(TaskViewType type) {
         TaskView taskView;
         switch (type) {
-            case TaskView.Type.GROUPED:
+            case GROUPED:
                 taskView = mGroupedTaskViewPool.getView();
                 break;
-            case TaskView.Type.DESKTOP:
+            case DESKTOP:
                 taskView = mDesktopTaskViewPool.getView();
                 break;
-            case TaskView.Type.SINGLE:
+            case SINGLE:
             default:
                 taskView = mTaskViewPool.getView();
         }
@@ -2834,12 +2841,12 @@
             // Add an empty view for now until the task plan is loaded and applied
             final TaskView taskView;
             if (needDesktopTask) {
-                taskView = getTaskViewFromPool(TaskView.Type.DESKTOP);
+                taskView = getTaskViewFromPool(TaskViewType.DESKTOP);
                 mTmpRunningTasks = Arrays.copyOf(runningTasks, runningTasks.length);
                 ((DesktopTaskView) taskView).bind(Arrays.asList(mTmpRunningTasks),
                         mOrientationState, mTaskOverlayFactory);
             } else if (needGroupTaskView) {
-                taskView = getTaskViewFromPool(TaskView.Type.GROUPED);
+                taskView = getTaskViewFromPool(TaskViewType.GROUPED);
                 mTmpRunningTasks = new Task[]{runningTasks[0], runningTasks[1]};
                 // When we create a placeholder task view mSplitBoundsConfig will be null, but with
                 // the actual app running we won't need to show the thumbnail until all the tasks
@@ -2847,7 +2854,7 @@
                 ((GroupedTaskView)taskView).bind(mTmpRunningTasks[0], mTmpRunningTasks[1],
                         mOrientationState, mTaskOverlayFactory, mSplitBoundsConfig);
             } else {
-                taskView = getTaskViewFromPool(TaskView.Type.SINGLE);
+                taskView = getTaskViewFromPool(TaskViewType.SINGLE);
                 // The temporary running task is only used for the duration between the start of the
                 // gesture and the task list is loaded and applied
                 mTmpRunningTasks = new Task[]{runningTasks[0]};
@@ -2915,7 +2922,7 @@
         int prevRunningTaskViewId = mRunningTaskViewId;
         mRunningTaskViewId = runningTaskViewId;
 
-        if (Flags.enableRefactorTaskThumbnail()) {
+        if (enableRefactorTaskThumbnail()) {
             TaskView previousRunningTaskView = getTaskViewFromTaskViewId(prevRunningTaskViewId);
             if (previousRunningTaskView != null) {
                 previousRunningTaskView.notifyIsRunningTaskUpdated();
@@ -2983,22 +2990,11 @@
     /**
      * Updates TaskView and ClearAllButtion scaling and translation required to turn into grid
      * layout.
-     * This method is used when no task dismissal has occurred.
+     *
+     * Skips rebalance.
      */
     private void updateGridProperties() {
-        updateGridProperties(false, Integer.MAX_VALUE);
-    }
-
-    /**
-     * Updates TaskView and ClearAllButtion scaling and translation required to turn into grid
-     * layout.
-     *
-     * This method is used when task dismissal has occurred, but rebalance is not needed.
-     *
-     * @param isTaskDismissal indicates if update was called due to task dismissal
-     */
-    private void updateGridProperties(boolean isTaskDismissal) {
-        updateGridProperties(isTaskDismissal, Integer.MAX_VALUE);
+        updateGridProperties(Integer.MAX_VALUE);
     }
 
     /**
@@ -3008,11 +3004,10 @@
      * This method only calculates the potential position and depends on {@link #setGridProgress} to
      * apply the actual scaling and translation.
      *
-     * @param isTaskDismissal    indicates if update was called due to task dismissal
      * @param startRebalanceAfter which view index to start rebalancing from. Use Integer.MAX_VALUE
-     *                           to skip rebalance
+     *                            to skip rebalance
      */
-    private void updateGridProperties(boolean isTaskDismissal, int startRebalanceAfter) {
+    private void updateGridProperties(int startRebalanceAfter) {
         int taskCount = getTaskViewCount();
         if (taskCount == 0) {
             return;
@@ -3043,7 +3038,8 @@
         TaskView homeTaskView = getHomeTaskView();
         TaskView nextFocusedTaskView = null;
 
-        if (!isTaskDismissal) {
+        // Don't clear the top row, if the user has dismissed a task, to maintain the task order.
+        if (!mAnyTaskHasBeenDismissed) {
             mTopRowIdSet.clear();
         }
         for (int i = 0; i < taskCount; i++) {
@@ -3083,7 +3079,7 @@
 
                 // Rebalance the grid starting after a certain index
                 boolean isTopRow;
-                if (isTaskDismissal) {
+                if (mAnyTaskHasBeenDismissed) {
                     if (i > startRebalanceAfter) {
                         mTopRowIdSet.remove(taskViewId);
                         isTopRow = topRowWidth <= bottomRowWidth;
@@ -3808,7 +3804,7 @@
                     anim.setFloat(taskView, taskView.getSecondaryDismissTranslationProperty(),
                             secondaryTranslation, clampToProgress(LINEAR, animationStartProgress,
                                     dismissTranslationInterpolationEnd));
-                    anim.setFloat(taskView, TaskView.SCALE_AND_DIM_OUT, 0f,
+                    anim.add(taskView.getFocusTransitionScaleAndDimOutAnimator(),
                             clampToProgress(LINEAR, 0f, ANIMATION_DISMISS_PROGRESS_MIDPOINT));
                 } else {
                     float primaryTranslation =
@@ -3864,6 +3860,7 @@
                 resetTaskVisuals();
 
                 if (success) {
+                    mAnyTaskHasBeenDismissed = true;
                     if (shouldRemoveTask) {
                         if (dismissedTaskView.isRunningTask()) {
                             finishRecentsAnimation(true /* toRecents */, false /* shouldPip */,
@@ -3972,7 +3969,7 @@
                             mTopRowIdSet.remove(mFocusedTaskViewId);
                             finalNextFocusedTaskView.animateIconScaleAndDimIntoView();
                         }
-                        updateTaskSize(/*isTaskDismissal=*/ true);
+                        updateTaskSize();
                         updateChildTaskOrientations();
                         // Update scroll and snap to page.
                         updateScrollSynchronously();
@@ -4008,8 +4005,7 @@
                                 }
 
                                 if (shouldRebalance) {
-                                    updateGridProperties(/*isTaskDismissal=*/ true,
-                                            highestVisibleTaskIndex);
+                                    updateGridProperties(highestVisibleTaskIndex);
                                     updateScrollSynchronously();
                                 }
                             }
@@ -4861,14 +4857,16 @@
                     == mSplitSelectStateController.getInitialTaskId();
             TaskContainer taskContainer = mSplitHiddenTaskView
                     .getTaskContainers().get(primaryTaskSelected ? 1 : 0);
-            TaskThumbnailViewDeprecated thumbnail = taskContainer.getThumbnailViewDeprecated();
             mSplitSelectStateController.getSplitAnimationController()
                     .addInitialSplitFromPair(taskContainer, builder,
                             mContainer.getDeviceProfile(),
                             mSplitHiddenTaskView.getWidth(), mSplitHiddenTaskView.getHeight(),
                             primaryTaskSelected);
             builder.addOnFrameCallback(() ->{
-                thumbnail.refreshSplashView();
+                // TODO(b/334826842): Handle splash icon for new TTV.
+                if (!enableRefactorTaskThumbnail()) {
+                    taskContainer.getThumbnailViewDeprecated().refreshSplashView();
+                }
                 mSplitHiddenTaskView.updateSnapshotRadius();
             });
         } else if (isInitiatingSplitFromTaskView) {
@@ -5260,7 +5258,7 @@
         updateGridProperties();
         updateScrollSynchronously();
 
-        int targetSysUiFlags = tv.getFirstThumbnailViewDeprecated().getSysUiStatusNavFlags();
+        int targetSysUiFlags = tv.getTaskContainers().getFirst().getSysUiStatusNavFlags();
         final boolean[] passedOverviewThreshold = new boolean[] {false};
         ValueAnimator progressAnim = ValueAnimator.ofFloat(0, 1);
         progressAnim.addUpdateListener(animator -> {
@@ -5347,6 +5345,43 @@
         updateCurrentTaskActionsVisibility();
         loadVisibleTaskData(TaskView.FLAG_UPDATE_ALL);
         updateEnabledOverlays();
+
+        if (enableRefactorTaskThumbnail()) {
+            int screenStart = 0;
+            int screenEnd = 0;
+            int centerPageIndex = 0;
+            if (showAsGrid()) {
+                screenStart = getPagedOrientationHandler().getPrimaryScroll(this);
+                int pageOrientedSize = getPagedOrientationHandler().getMeasuredSize(this);
+                screenEnd = screenStart + pageOrientedSize;
+            } else {
+                centerPageIndex = getPageNearestToCenterOfScreen();
+            }
+
+            Set<Integer> fullyVisibleTaskIds = new HashSet<>();
+
+            // Update the task data for the in/visible children
+            for (int i = 0; i < getTaskViewCount(); i++) {
+                TaskView taskView = requireTaskViewAt(i);
+                List<TaskContainer> containers = taskView.getTaskContainers();
+                if (containers.isEmpty()) {
+                    continue;
+                }
+                boolean isFullyVisible;
+                if (showAsGrid()) {
+                    isFullyVisible = isTaskViewFullyWithinBounds(taskView, screenStart,
+                            screenEnd);
+                } else {
+                    isFullyVisible = i == centerPageIndex;
+                }
+                if (isFullyVisible) {
+                    List<Integer> taskIds = containers.stream().map(
+                            taskContainer -> taskContainer.getTask().key.id).toList();
+                    fullyVisibleTaskIds.addAll(taskIds);
+                }
+            }
+            mRecentsViewData.getSettledFullyVisibleTaskIds().setValue(fullyVisibleTaskIds);
+        }
     }
 
     @Override
@@ -5897,6 +5932,7 @@
         if (mOverlayEnabled != overlayEnabled) {
             mOverlayEnabled = overlayEnabled;
             updateEnabledOverlays();
+            mRecentsViewData.getOverlayEnabled().setValue(overlayEnabled);
         }
     }
 
@@ -5948,6 +5984,12 @@
     }
 
     private void switchToScreenshotInternal(Runnable onFinishRunnable) {
+        // TODO(b/342560598): Handle switchToScreenshot for new TTV.
+        if (enableRefactorTaskThumbnail()) {
+            onFinishRunnable.run();
+            return;
+        }
+
         TaskView taskView = getRunningTaskView();
         if (taskView == null) {
             onFinishRunnable.run();
@@ -6035,7 +6077,7 @@
      * tasks to be dimmed while other elements in the recents view are left alone.
      */
     public void showForegroundScrim(boolean show) {
-        // TODO(b/335606129) Add scrim response into new TTV - this is called from overlay
+        // TODO(b/349601769) Add scrim response into new TTV - this is called from overlay
         if (!show && mColorTint == 0) {
             if (mTintingAnimator != null) {
                 mTintingAnimator.cancel();
diff --git a/quickstep/src/com/android/quickstep/views/TaskContainer.kt b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
new file mode 100644
index 0000000..74d120f
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.views
+
+import android.content.Intent
+import android.graphics.Bitmap
+import android.view.View
+import com.android.launcher3.Flags.enableRefactorTaskThumbnail
+import com.android.launcher3.Flags.privateSpaceRestrictAccessibilityDrag
+import com.android.launcher3.LauncherSettings
+import com.android.launcher3.model.data.ItemInfoWithIcon
+import com.android.launcher3.model.data.WorkspaceItemInfo
+import com.android.launcher3.pm.UserCache
+import com.android.launcher3.util.SplitConfigurationOptions
+import com.android.launcher3.util.TransformingTouchDelegate
+import com.android.quickstep.TaskOverlayFactory
+import com.android.quickstep.TaskUtils
+import com.android.quickstep.task.thumbnail.TaskThumbnail
+import com.android.quickstep.task.thumbnail.TaskThumbnailView
+import com.android.quickstep.task.util.GetThumbnailUseCase
+import com.android.quickstep.task.viewmodel.TaskContainerData
+import com.android.systemui.shared.recents.model.Task
+
+/** Holder for all Task dependent information. */
+class TaskContainer(
+    val taskView: TaskView,
+    val task: Task,
+    val snapshotView: View,
+    val iconView: TaskViewIcon,
+    /**
+     * This technically can be a vanilla [android.view.TouchDelegate] class, however that class
+     * requires setting the touch bounds at construction, so we'd repeatedly be created many
+     * instances unnecessarily as scrolling occurs, whereas [TransformingTouchDelegate] allows touch
+     * delegated bounds only to be updated.
+     */
+    val iconTouchDelegate: TransformingTouchDelegate,
+    /** Defaults to STAGE_POSITION_UNDEFINED if in not a split screen task view */
+    @SplitConfigurationOptions.StagePosition val stagePosition: Int,
+    val digitalWellBeingToast: DigitalWellBeingToast?,
+    val showWindowsView: View?,
+    taskOverlayFactory: TaskOverlayFactory
+) {
+    val overlay: TaskOverlayFactory.TaskOverlay<*> = taskOverlayFactory.createOverlay(this)
+    val taskContainerData = TaskContainerData()
+
+    private val getThumbnailUseCase by lazy {
+        // TODO(b/335649589): Ideally create and obtain this from DI.
+        val recentsView =
+            RecentsViewContainer.containerFromContext<RecentsViewContainer>(
+                    overlay.taskView.context
+                )
+                .getOverviewPanel<RecentsView<*, *>>()
+        GetThumbnailUseCase(recentsView.mTasksRepository!!)
+    }
+
+    init {
+        if (enableRefactorTaskThumbnail()) {
+            require(snapshotView is TaskThumbnailView)
+        } else {
+            require(snapshotView is TaskThumbnailViewDeprecated)
+        }
+    }
+
+    val splitAnimationThumbnail: Bitmap?
+        get() =
+            if (enableRefactorTaskThumbnail()) {
+                getThumbnailUseCase.run(task.key.id)
+            } else {
+                thumbnailViewDeprecated.thumbnail
+            }
+
+    val thumbnailView: TaskThumbnailView
+        get() {
+            require(enableRefactorTaskThumbnail())
+            return snapshotView as TaskThumbnailView
+        }
+
+    val thumbnailViewDeprecated: TaskThumbnailViewDeprecated
+        get() {
+            require(!enableRefactorTaskThumbnail())
+            return snapshotView as TaskThumbnailViewDeprecated
+        }
+
+    // TODO(b/334826842): Support shouldShowSplashView for new TTV.
+    val shouldShowSplashView: Boolean
+        get() =
+            if (enableRefactorTaskThumbnail()) false
+            else thumbnailViewDeprecated.shouldShowSplashView()
+
+    // TODO(b/350743460) Support sysUiStatusNavFlags for new TTV.
+    val sysUiStatusNavFlags: Int
+        get() =
+            if (enableRefactorTaskThumbnail()) 0 else thumbnailViewDeprecated.sysUiStatusNavFlags
+
+    /** Builds proto for logging */
+    val itemInfo: WorkspaceItemInfo
+        get() =
+            WorkspaceItemInfo().apply {
+                itemType = LauncherSettings.Favorites.ITEM_TYPE_TASK
+                container = LauncherSettings.Favorites.CONTAINER_TASKSWITCHER
+                val componentKey = TaskUtils.getLaunchComponentKeyForTask(task.key)
+                user = componentKey.user
+                intent = Intent().setComponent(componentKey.componentName)
+                title = task.title
+                taskView.recentsView?.let { screenId = it.indexOfChild(taskView) }
+                if (privateSpaceRestrictAccessibilityDrag()) {
+                    if (
+                        UserCache.getInstance(taskView.context)
+                            .getUserInfo(componentKey.user)
+                            .isPrivate
+                    ) {
+                        runtimeStatusFlags =
+                            runtimeStatusFlags or ItemInfoWithIcon.FLAG_NOT_PINNABLE
+                    }
+                }
+            }
+
+    fun bind() {
+        if (enableRefactorTaskThumbnail()) {
+            bindThumbnailView()
+        } else {
+            thumbnailViewDeprecated.bind(task, overlay)
+        }
+        overlay.init()
+    }
+
+    fun destroy() {
+        digitalWellBeingToast?.destroy()
+        if (enableRefactorTaskThumbnail()) {
+            taskView.removeView(thumbnailView)
+        }
+        overlay.destroy()
+    }
+
+    // TODO(b/335649589): TaskView's VM will already have access to TaskThumbnailView's VM
+    //  so there will be no need to access TaskThumbnailView's VM through the TaskThumbnailView
+    fun bindThumbnailView() {
+        // TODO(b/343364498): Existing view has shouldShowScreenshot as an override as well but
+        //  this should be decided inside TaskThumbnailViewModel.
+        thumbnailView.viewModel.bind(TaskThumbnail(task.key.id, taskView.isRunningTask))
+    }
+
+    fun setOverlayEnabled(enabled: Boolean) {
+        if (!enableRefactorTaskThumbnail()) {
+            thumbnailViewDeprecated.setOverlayEnabled(enabled)
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/views/TaskMenuView.java b/quickstep/src/com/android/quickstep/views/TaskMenuView.java
index 8d5ba77..63bc509 100644
--- a/quickstep/src/com/android/quickstep/views/TaskMenuView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskMenuView.java
@@ -55,7 +55,6 @@
 import com.android.quickstep.TaskUtils;
 import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
 import com.android.quickstep.util.TaskCornerRadius;
-import com.android.quickstep.views.TaskView.TaskContainer;
 
 /**
  * Contains options for a recent task when long-pressing its icon.
diff --git a/quickstep/src/com/android/quickstep/views/TaskMenuViewWithArrow.kt b/quickstep/src/com/android/quickstep/views/TaskMenuViewWithArrow.kt
index 659cc0c..e10d38c 100644
--- a/quickstep/src/com/android/quickstep/views/TaskMenuViewWithArrow.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskMenuViewWithArrow.kt
@@ -37,7 +37,6 @@
 import com.android.launcher3.popup.SystemShortcut
 import com.android.launcher3.util.Themes
 import com.android.quickstep.TaskOverlayFactory
-import com.android.quickstep.views.TaskView.TaskContainer
 
 class TaskMenuViewWithArrow<T> : ArrowPopup<T> where T : RecentsViewContainer, T : Context {
     companion object {
@@ -58,7 +57,9 @@
     }
 
     constructor(context: Context) : super(context)
+
     constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
+
     constructor(
         context: Context,
         attrs: AttributeSet,
@@ -80,6 +81,7 @@
     private var alignedOptionIndex: Int = 0
     private val extraSpaceForRowAlignment: Int
         get() = optionMeasuredHeight * alignedOptionIndex
+
     private val menuPaddingEnd = context.resources.getDimensionPixelSize(R.dimen.task_card_margin)
 
     private lateinit var taskView: TaskView
diff --git a/quickstep/src/com/android/quickstep/views/TaskThumbnailViewDeprecated.java b/quickstep/src/com/android/quickstep/views/TaskThumbnailViewDeprecated.java
index 2afb6a6..56ca043 100644
--- a/quickstep/src/com/android/quickstep/views/TaskThumbnailViewDeprecated.java
+++ b/quickstep/src/com/android/quickstep/views/TaskThumbnailViewDeprecated.java
@@ -28,16 +28,13 @@
 import android.graphics.Canvas;
 import android.graphics.Color;
 import android.graphics.ColorFilter;
-import android.graphics.Insets;
 import android.graphics.Matrix;
 import android.graphics.Paint;
 import android.graphics.PorterDuff;
 import android.graphics.PorterDuffXfermode;
 import android.graphics.Rect;
-import android.graphics.RectF;
 import android.graphics.Shader;
 import android.graphics.drawable.Drawable;
-import android.os.Build;
 import android.util.AttributeSet;
 import android.util.FloatProperty;
 import android.util.Property;
@@ -45,7 +42,6 @@
 import android.widget.ImageView;
 
 import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
 import androidx.core.graphics.ColorUtils;
 
 import com.android.launcher3.DeviceProfile;
@@ -169,17 +165,6 @@
     }
 
     /**
-     * Sets TaskOverlay without binding a task.
-     *
-     * @deprecated Should only be used when using new
-     * {@link com.android.quickstep.task.thumbnail.TaskThumbnailView}.
-     */
-    @Deprecated
-    public void setTaskOverlay(TaskOverlay<?> overlay) {
-        mOverlay = overlay;
-    }
-
-    /**
      * Updates the thumbnail.
      *
      * @param refreshNow whether the {@code thumbnailData} will be used to redraw immediately.
@@ -266,40 +251,6 @@
         return mDimAlpha;
     }
 
-    /**
-     * Get the scaled insets that are being used to draw the task view. This is a subsection of
-     * the full snapshot.
-     *
-     * @return the insets in snapshot bitmap coordinates.
-     */
-    @RequiresApi(api = Build.VERSION_CODES.Q)
-    public Insets getScaledInsets() {
-        if (mThumbnailData == null) {
-            return Insets.NONE;
-        }
-
-        RectF bitmapRect = new RectF(
-                0,
-                0,
-                mThumbnailData.getThumbnail().getWidth(),
-                mThumbnailData.getThumbnail().getHeight());
-        RectF viewRect = new RectF(0, 0, getMeasuredWidth(), getMeasuredHeight());
-
-        // The position helper matrix tells us how to transform the bitmap to fit the view, the
-        // inverse tells us where the view would be in the bitmaps coordinates. The insets are the
-        // difference between the bitmap bounds and the projected view bounds.
-        Matrix boundsToBitmapSpace = new Matrix();
-        mPreviewPositionHelper.getMatrix().invert(boundsToBitmapSpace);
-        RectF boundsInBitmapSpace = new RectF();
-        boundsToBitmapSpace.mapRect(boundsInBitmapSpace, viewRect);
-
-        DeviceProfile dp = mContainer.getDeviceProfile();
-        int bottomInset = dp.isTablet
-                ? Math.round(bitmapRect.bottom - boundsInBitmapSpace.bottom) : 0;
-        return Insets.of(0, 0, 0, bottomInset);
-    }
-
-
     @SystemUiControllerFlags
     public int getSysUiStatusNavFlags() {
         if (mThumbnailData != null) {
@@ -487,7 +438,9 @@
      */
     private void refreshOverlay() {
         if (mOverlayEnabled) {
-            mOverlay.initOverlay(mTask, mThumbnailData, mPreviewPositionHelper.getMatrix(),
+            mOverlay.initOverlay(mTask,
+                    mThumbnailData != null ? mThumbnailData.getThumbnail() : null,
+                    mPreviewPositionHelper.getMatrix(),
                     mPreviewPositionHelper.isOrientationChanged());
         } else {
             mOverlay.reset();
@@ -560,6 +513,10 @@
         return mThumbnailData.isRealSnapshot && !mTask.isLocked;
     }
 
+    public Matrix getThumbnailMatrix() {
+        return mPreviewPositionHelper.getMatrix();
+    }
+
     @Override
     public void onRecycle() {
         // Do nothing
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index d4b0040..004003c 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -22,10 +22,7 @@
 import android.annotation.IdRes
 import android.app.ActivityOptions
 import android.content.Context
-import android.content.Intent
-import android.graphics.Bitmap
 import android.graphics.Canvas
-import android.graphics.Insets
 import android.graphics.PointF
 import android.graphics.Rect
 import android.graphics.drawable.Drawable
@@ -34,6 +31,7 @@
 import android.util.FloatProperty
 import android.util.Log
 import android.view.Display
+import android.view.LayoutInflater
 import android.view.MotionEvent
 import android.view.View
 import android.view.View.OnClickListener
@@ -52,16 +50,12 @@
 import com.android.launcher3.Flags.enableGridOnlyOverview
 import com.android.launcher3.Flags.enableOverviewIconMenu
 import com.android.launcher3.Flags.enableRefactorTaskThumbnail
-import com.android.launcher3.Flags.privateSpaceRestrictAccessibilityDrag
-import com.android.launcher3.LauncherSettings
 import com.android.launcher3.R
 import com.android.launcher3.Utilities
+import com.android.launcher3.anim.AnimatedFloat
 import com.android.launcher3.config.FeatureFlags.ENABLE_KEYBOARD_QUICK_SWITCH
 import com.android.launcher3.logging.StatsLogManager.LauncherEvent
 import com.android.launcher3.model.data.ItemInfo
-import com.android.launcher3.model.data.ItemInfoWithIcon
-import com.android.launcher3.model.data.WorkspaceItemInfo
-import com.android.launcher3.pm.UserCache
 import com.android.launcher3.testing.TestLogging
 import com.android.launcher3.testing.shared.TestProtocol
 import com.android.launcher3.util.CancellableTask
@@ -84,13 +78,9 @@
 import com.android.quickstep.RemoteAnimationTargets
 import com.android.quickstep.TaskAnimationManager
 import com.android.quickstep.TaskOverlayFactory
-import com.android.quickstep.TaskOverlayFactory.TaskOverlay
-import com.android.quickstep.TaskUtils
 import com.android.quickstep.TaskViewUtils
 import com.android.quickstep.orientation.RecentsPagedOrientationHandler
-import com.android.quickstep.task.thumbnail.TaskThumbnail
 import com.android.quickstep.task.thumbnail.TaskThumbnailView
-import com.android.quickstep.task.viewmodel.TaskContainerData
 import com.android.quickstep.task.viewmodel.TaskViewData
 import com.android.quickstep.util.ActiveGestureErrorDetector
 import com.android.quickstep.util.ActiveGestureLog
@@ -114,7 +104,8 @@
     defStyleAttr: Int = 0,
     defStyleRes: Int = 0,
     focusBorderAnimator: BorderAnimator? = null,
-    hoverBorderAnimator: BorderAnimator? = null
+    hoverBorderAnimator: BorderAnimator? = null,
+    type: TaskViewType = TaskViewType.SINGLE
 ) : FrameLayout(context, attrs), ViewPool.Reusable {
     /**
      * Used in conjunction with [onTaskListVisibilityChanged], providing more granularity on which
@@ -124,18 +115,7 @@
     @IntDef(FLAG_UPDATE_ALL, FLAG_UPDATE_ICON, FLAG_UPDATE_THUMBNAIL, FLAG_UPDATE_CORNER_RADIUS)
     annotation class TaskDataChanges
 
-    /** Type of task view */
-    @Retention(AnnotationRetention.SOURCE)
-    @IntDef(Type.SINGLE, Type.GROUPED, Type.DESKTOP)
-    annotation class Type {
-        companion object {
-            const val SINGLE = 1
-            const val GROUPED = 2
-            const val DESKTOP = 3
-        }
-    }
-
-    val taskViewData = TaskViewData()
+    val taskViewData = TaskViewData(type)
     val taskIds: IntArray
         /** Returns a copy of integer array containing taskIds of all tasks in the TaskView. */
         get() = taskContainers.map { it.task.key.id }.toIntArray()
@@ -168,11 +148,6 @@
         get() = taskContainers[0].task
 
     @get:Deprecated("Use [taskContainers] instead.")
-    val firstThumbnailViewDeprecated: TaskThumbnailViewDeprecated
-        /** Returns the first thumbnailView of the TaskView. */
-        get() = taskContainers[0].thumbnailViewDeprecated
-
-    @get:Deprecated("Use [taskContainers] instead.")
     val firstSnapshotView: View
         /** Returns the first snapshotView of the TaskView. */
         get() = taskContainers[0].snapshotView
@@ -446,17 +421,17 @@
         focusTransitionPropertyFactory.get(FOCUS_TRANSITION_INDEX_FULLSCREEN)
     private val focusTransitionScaleAndDim =
         focusTransitionPropertyFactory.get(FOCUS_TRANSITION_INDEX_SCALE_AND_DIM)
+
     /**
-     * Variant of [focusTransitionScaleAndDim] that has a built-in interpolator, to be used with
-     * [com.android.launcher3.anim.PendingAnimation] via [SCALE_AND_DIM_OUT] only. PendingAnimation
-     * doesn't support interpolator per animation, so we'll have to interpolate inside the property.
+     * Returns an animator of [focusTransitionScaleAndDim] that transition out with a built-in
+     * interpolator.
      */
-    private var focusTransitionScaleAndDimOut = focusTransitionScaleAndDim.value
-        set(value) {
-            field = value
-            focusTransitionScaleAndDim.value =
-                FOCUS_TRANSITION_FAST_OUT_INTERPOLATOR.getInterpolation(field)
-        }
+    fun getFocusTransitionScaleAndDimOutAnimator(): ObjectAnimator =
+        AnimatedFloat { v ->
+                focusTransitionScaleAndDim.value =
+                    FOCUS_TRANSITION_FAST_OUT_INTERPOLATOR.getInterpolation(v)
+            }
+            .animateToValue(1f, 0f)
 
     private var iconAndDimAnimator: ObjectAnimator? = null
     // The current background requests to load the task thumbnail and icon
@@ -688,23 +663,21 @@
         taskOverlayFactory: TaskOverlayFactory
     ): TaskContainer {
         val thumbnailViewDeprecated: TaskThumbnailViewDeprecated = findViewById(thumbnailViewId)!!
-        val thumbnailView: TaskThumbnailView?
-        if (enableRefactorTaskThumbnail()) {
-            val indexOfSnapshotView = indexOfChild(thumbnailViewDeprecated)
-            thumbnailView =
-                TaskThumbnailView(context).apply {
-                    layoutParams = thumbnailViewDeprecated.layoutParams
-                    addView(this, indexOfSnapshotView)
+        val snapshotView =
+            if (enableRefactorTaskThumbnail()) {
+                thumbnailViewDeprecated.visibility = GONE
+                val indexOfSnapshotView = indexOfChild(thumbnailViewDeprecated)
+                LayoutInflater.from(context).inflate(R.layout.task_thumbnail, this, false).also {
+                    addView(it, indexOfSnapshotView, thumbnailViewDeprecated.layoutParams)
                 }
-            thumbnailViewDeprecated.visibility = GONE
-        } else {
-            thumbnailView = null
-        }
+            } else {
+                thumbnailViewDeprecated
+            }
         val iconView = getOrInflateIconView(iconViewId)
         return TaskContainer(
+            this,
             task,
-            thumbnailView,
-            thumbnailViewDeprecated,
+            snapshotView,
             iconView,
             TransformingTouchDelegate(iconView.asView()),
             stagePosition,
@@ -726,8 +699,6 @@
                 .inflate() as TaskViewIcon
     }
 
-    protected fun isTaskContainersInitialized() = this::taskContainers.isInitialized
-
     fun containsMultipleTasks() = taskContainers.size > 1
 
     /**
@@ -862,7 +833,8 @@
             taskContainers.forEach {
                 if (visible) {
                     recentsModel.thumbnailCache
-                        .updateThumbnailInBackground(it.task) { thumbnailData ->
+                        .getThumbnailInBackground(it.task) { thumbnailData ->
+                            it.task.thumbnail = thumbnailData
                             it.thumbnailViewDeprecated.setThumbnail(it.task, thumbnailData)
                         }
                         ?.also { request -> pendingThumbnailLoadRequests.add(request) }
@@ -878,12 +850,15 @@
             taskContainers.forEach {
                 if (visible) {
                     recentsModel.iconCache
-                        .updateIconInBackground(it.task) { task ->
-                            setIcon(it.iconView, task.icon)
+                        .getIconInBackground(it.task) { icon, contentDescription, title ->
+                            it.task.icon = icon
+                            it.task.titleDescription = contentDescription
+                            it.task.title = title
+                            setIcon(it.iconView, icon)
                             if (enableOverviewIconMenu()) {
-                                setText(it.iconView, task.title)
+                                setText(it.iconView, title)
                             }
-                            it.digitalWellBeingToast?.initialize(task)
+                            it.digitalWellBeingToast?.initialize(it.task)
                         }
                         ?.also { request -> pendingIconLoadRequests.add(request) }
                 } else {
@@ -1205,7 +1180,7 @@
             container.task,
             container.iconView.drawable,
             container.snapshotView,
-            container.thumbnail,
+            container.splitAnimationThumbnail,
             /* intent */ null,
             /* user */ null,
             container.itemInfo
@@ -1405,10 +1380,8 @@
     }
 
     open fun setOverlayEnabled(overlayEnabled: Boolean) {
-        // TODO(b/335606129) Investigate the usage of [TaskOverlay] in the new TaskThumbnailView.
-        //  and if it's still necessary we should support that in the new TTV class.
         if (!enableRefactorTaskThumbnail()) {
-            taskContainers.forEach { it.thumbnailViewDeprecated.setOverlayEnabled(overlayEnabled) }
+            taskContainers.forEach { it.setOverlayEnabled(overlayEnabled) }
         }
     }
 
@@ -1483,7 +1456,9 @@
     protected open fun updateSnapshotRadius() {
         updateCurrentFullscreenParams()
         taskContainers.forEach {
-            it.thumbnailViewDeprecated.setFullscreenParams(getThumbnailFullscreenParams())
+            if (!enableRefactorTaskThumbnail()) {
+                it.thumbnailViewDeprecated.setFullscreenParams(getThumbnailFullscreenParams())
+            }
             it.overlay.setFullscreenParams(getThumbnailFullscreenParams())
         }
     }
@@ -1606,90 +1581,6 @@
         override fun close() {}
     }
 
-    /** Holder for all Task dependent information. */
-    inner class TaskContainer(
-        val task: Task,
-        val thumbnailView: TaskThumbnailView?,
-        val thumbnailViewDeprecated: TaskThumbnailViewDeprecated,
-        val iconView: TaskViewIcon,
-        /**
-         * This technically can be a vanilla [android.view.TouchDelegate] class, however that class
-         * requires setting the touch bounds at construction, so we'd repeatedly be created many
-         * instances unnecessarily as scrolling occurs, whereas [TransformingTouchDelegate] allows
-         * touch delegated bounds only to be updated.
-         */
-        val iconTouchDelegate: TransformingTouchDelegate,
-        /** Defaults to STAGE_POSITION_UNDEFINED if in not a split screen task view */
-        @StagePosition val stagePosition: Int,
-        val digitalWellBeingToast: DigitalWellBeingToast?,
-        val showWindowsView: View?,
-        taskOverlayFactory: TaskOverlayFactory
-    ) {
-        val overlay: TaskOverlay<*> = taskOverlayFactory.createOverlay(this)
-        val taskContainerData = TaskContainerData()
-
-        val snapshotView: View
-            get() = thumbnailView ?: thumbnailViewDeprecated
-
-        // TODO(b/349120849): Extract ThumbnailData from TaskContainerData/TaskThumbnailViewModel
-        val thumbnail: Bitmap?
-            get() = thumbnailViewDeprecated.thumbnail
-
-        // TODO(b/349120849): Extract ThumbnailData from TaskContainerData/TaskThumbnailViewModel
-        val isRealSnapshot: Boolean
-            get() = thumbnailViewDeprecated.isRealSnapshot()
-
-        // TODO(b/349120849): Extract ThumbnailData from TaskContainerData/TaskThumbnailViewModel
-        val scaledInsets: Insets
-            get() = thumbnailViewDeprecated.scaledInsets
-
-        /** Builds proto for logging */
-        val itemInfo: WorkspaceItemInfo
-            get() =
-                WorkspaceItemInfo().apply {
-                    itemType = LauncherSettings.Favorites.ITEM_TYPE_TASK
-                    container = LauncherSettings.Favorites.CONTAINER_TASKSWITCHER
-                    val componentKey = TaskUtils.getLaunchComponentKeyForTask(task.key)
-                    user = componentKey.user
-                    intent = Intent().setComponent(componentKey.componentName)
-                    title = task.title
-                    recentsView?.let { screenId = it.indexOfChild(this@TaskView) }
-                    if (privateSpaceRestrictAccessibilityDrag()) {
-                        if (
-                            UserCache.getInstance(context).getUserInfo(componentKey.user).isPrivate
-                        ) {
-                            runtimeStatusFlags =
-                                runtimeStatusFlags or ItemInfoWithIcon.FLAG_NOT_PINNABLE
-                        }
-                    }
-                }
-
-        val taskView: TaskView
-            get() = this@TaskView
-
-        fun destroy() {
-            digitalWellBeingToast?.destroy()
-            thumbnailView?.let { taskView.removeView(it) }
-        }
-
-        fun bind() {
-            if (enableRefactorTaskThumbnail() && thumbnailView != null) {
-                thumbnailViewDeprecated.setTaskOverlay(overlay)
-                bindThumbnailView()
-            } else {
-                thumbnailViewDeprecated.bind(task, overlay)
-            }
-        }
-
-        // TODO(b/335649589): TaskView's VM will already have access to TaskThumbnailView's VM
-        //  so there will be no need to access TaskThumbnailView's VM through the TaskThumbnailView
-        fun bindThumbnailView() {
-            // TODO(b/343364498): Existing view has shouldShowScreenshot as an override as well but
-            //  this should be decided inside TaskThumbnailViewModel.
-            thumbnailView?.viewModel?.bind(TaskThumbnail(task.key.id, isRunningTask))
-        }
-    }
-
     companion object {
         private const val TAG = "TaskView"
         const val FLAG_UPDATE_ICON = 1
@@ -1725,16 +1616,6 @@
                 override fun get(taskView: TaskView) = taskView.focusTransitionProgress
             }
 
-        @JvmField
-        val SCALE_AND_DIM_OUT: FloatProperty<TaskView> =
-            object : FloatProperty<TaskView>("scaleAndDimFastOut") {
-                override fun setValue(taskView: TaskView, v: Float) {
-                    taskView.focusTransitionScaleAndDimOut = v
-                }
-
-                override fun get(taskView: TaskView) = taskView.focusTransitionScaleAndDimOut
-            }
-
         private val SPLIT_SELECT_TRANSLATION_X: FloatProperty<TaskView> =
             object : FloatProperty<TaskView>("splitSelectTranslationX") {
                 override fun setValue(taskView: TaskView, v: Float) {
diff --git a/quickstep/src/com/android/quickstep/views/TaskViewType.kt b/quickstep/src/com/android/quickstep/views/TaskViewType.kt
new file mode 100644
index 0000000..b2a32a9
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/TaskViewType.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.views
+
+/** Type of the [TaskView] */
+enum class TaskViewType {
+    SINGLE,
+    GROUPED,
+    DESKTOP
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/model/WidgetsPredictionsRequesterTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/model/WidgetsPredictionsRequesterTest.kt
index 039dce4..4ea74df 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/model/WidgetsPredictionsRequesterTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/model/WidgetsPredictionsRequesterTest.kt
@@ -30,6 +30,7 @@
 import com.android.launcher3.InvariantDeviceProfile
 import com.android.launcher3.LauncherAppState
 import com.android.launcher3.icons.IconCache
+import com.android.launcher3.model.WidgetPredictionsRequester.LAUNCH_LOCATION
 import com.android.launcher3.model.WidgetPredictionsRequester.buildBundleForPredictionSession
 import com.android.launcher3.model.WidgetPredictionsRequester.filterPredictions
 import com.android.launcher3.model.WidgetPredictionsRequester.notOnUiSurfaceFilter
@@ -103,7 +104,7 @@
     fun buildBundleForPredictionSession_includesAddedAppWidgets() {
         val existingWidgets = arrayListOf(widget1aInfo, widget1bInfo, widget2Info)
 
-        val bundle = buildBundleForPredictionSession(existingWidgets, TEST_UI_SURFACE)
+        val bundle = buildBundleForPredictionSession(existingWidgets)
         val addedWidgetsBundleExtra =
             bundle.getParcelableArrayList(BUNDLE_KEY_ADDED_APP_WIDGETS, AppTarget::class.java)
 
@@ -213,7 +214,7 @@
                     .setClassName(providerClassName)
                     .build()
             return AppTargetEvent.Builder(appTarget, AppTargetEvent.ACTION_PIN)
-                .setLaunchLocation(TEST_UI_SURFACE)
+                .setLaunchLocation(LAUNCH_LOCATION)
                 .build()
         }
     }
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java
index 0f06d98..399aea6 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java
@@ -4,6 +4,8 @@
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_BACK_BUTTON_TAP;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_HOME_BUTTON_LONGPRESS;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_HOME_BUTTON_TAP;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_IME_SWITCHER_BUTTON_LONGPRESS;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_IME_SWITCHER_BUTTON_TAP;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_OVERVIEW_BUTTON_LONGPRESS;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_OVERVIEW_BUTTON_TAP;
 import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_A11Y;
@@ -26,6 +28,7 @@
 
 import android.os.Handler;
 import android.view.View;
+import android.view.inputmethod.Flags;
 
 import androidx.test.platform.app.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
@@ -109,8 +112,27 @@
 
     @Test
     public void testPressImeSwitcher() {
+        mNavButtonController.init(mockTaskbarControllers);
         mNavButtonController.onButtonClick(BUTTON_IME_SWITCH, mockView);
+        verify(mockStatsLogger, times(1)).log(LAUNCHER_TASKBAR_IME_SWITCHER_BUTTON_TAP);
+        verify(mockStatsLogger, never()).log(LAUNCHER_TASKBAR_IME_SWITCHER_BUTTON_LONGPRESS);
         verify(mockSystemUiProxy, times(1)).onImeSwitcherPressed();
+        verify(mockSystemUiProxy, never()).onImeSwitcherLongPress();
+    }
+
+    @Test
+    public void testLongPressImeSwitcher() {
+        mNavButtonController.init(mockTaskbarControllers);
+        mNavButtonController.onButtonLongClick(BUTTON_IME_SWITCH, mockView);
+        verify(mockStatsLogger, never()).log(LAUNCHER_TASKBAR_IME_SWITCHER_BUTTON_TAP);
+        verify(mockSystemUiProxy, never()).onImeSwitcherPressed();
+        if (Flags.imeSwitcherRevamp()) {
+            verify(mockStatsLogger, times(1)).log(LAUNCHER_TASKBAR_IME_SWITCHER_BUTTON_LONGPRESS);
+            verify(mockSystemUiProxy, times(1)).onImeSwitcherLongPress();
+        } else {
+            verify(mockStatsLogger, never()).log(LAUNCHER_TASKBAR_IME_SWITCHER_BUTTON_LONGPRESS);
+            verify(mockSystemUiProxy, never()).onImeSwitcherLongPress();
+        }
     }
 
     @Test
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarUnitTestRule.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarUnitTestRule.kt
deleted file mode 100644
index bbf738e..0000000
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarUnitTestRule.kt
+++ /dev/null
@@ -1,165 +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.taskbar
-
-import android.app.Instrumentation
-import android.app.PendingIntent
-import android.content.Context
-import android.content.IIntentSender
-import android.content.Intent
-import androidx.test.platform.app.InstrumentationRegistry
-import androidx.test.rule.ServiceTestRule
-import com.android.launcher3.LauncherAppState
-import com.android.launcher3.taskbar.TaskbarNavButtonController.TaskbarNavButtonCallbacks
-import com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR
-import com.android.launcher3.util.LauncherMultivalentJUnit.Companion.isRunningInRobolectric
-import com.android.quickstep.AllAppsActionManager
-import com.android.quickstep.TouchInteractionService
-import com.android.quickstep.TouchInteractionService.TISBinder
-import org.junit.Assume.assumeTrue
-import org.junit.rules.TestRule
-import org.junit.runner.Description
-import org.junit.runners.model.Statement
-
-/**
- * Manages the Taskbar lifecycle for unit tests.
- *
- * Tests should pass in themselves as [testInstance]. They also need to provide their target
- * [context] through the constructor.
- *
- * See [InjectController] for grabbing controller(s) under test with minimal boilerplate.
- *
- * The rule interacts with [TaskbarManager] on the main thread. A good rule of thumb for tests is
- * that code that is executed on the main thread in production should also happen on that thread
- * when tested.
- *
- * `@UiThreadTest` is a simple way to run an entire test body on the main thread. But if a test
- * executes code that appends message(s) to the main thread's `MessageQueue`, the annotation will
- * prevent those messages from being processed until after the test body finishes.
- *
- * To test pending messages, instead use something like [Instrumentation.runOnMainSync] to perform
- * only sections of the test body on the main thread synchronously:
- * ```
- * @Test
- * fun example() {
- *     instrumentation.runOnMainSync { doWorkThatPostsMessage() }
- *     // Second lambda will not execute until message is processed.
- *     instrumentation.runOnMainSync { verifyMessageResults() }
- * }
- * ```
- */
-class TaskbarUnitTestRule(private val testInstance: Any, private val context: Context) : TestRule {
-    private val instrumentation = InstrumentationRegistry.getInstrumentation()
-    private val serviceTestRule = ServiceTestRule()
-
-    private lateinit var taskbarManager: TaskbarManager
-
-    val activityContext: TaskbarActivityContext
-        get() {
-            return taskbarManager.currentActivityContext
-                ?: throw RuntimeException("Failed to obtain TaskbarActivityContext.")
-        }
-
-    override fun apply(base: Statement, description: Description): Statement {
-        return object : Statement() {
-            override fun evaluate() {
-
-                instrumentation.runOnMainSync {
-                    assumeTrue(
-                        LauncherAppState.getIDP(context).getDeviceProfile(context).isTaskbarPresent
-                    )
-                }
-
-                // Check for existing Taskbar instance from Launcher process.
-                val launcherTaskbarManager: TaskbarManager? =
-                    if (!isRunningInRobolectric) {
-                        try {
-                            val tisBinder =
-                                serviceTestRule.bindService(
-                                    Intent(context, TouchInteractionService::class.java)
-                                ) as? TISBinder
-                            tisBinder?.taskbarManager
-                        } catch (_: Exception) {
-                            null
-                        }
-                    } else {
-                        null
-                    }
-
-                instrumentation.runOnMainSync {
-                    taskbarManager =
-                        TaskbarManager(
-                            context,
-                            AllAppsActionManager(context, UI_HELPER_EXECUTOR) {
-                                PendingIntent(IIntentSender.Default())
-                            },
-                            object : TaskbarNavButtonCallbacks {},
-                        )
-                }
-
-                try {
-                    // Replace Launcher Taskbar window with test instance.
-                    instrumentation.runOnMainSync {
-                        launcherTaskbarManager?.removeTaskbarRootViewFromWindow()
-                        taskbarManager.onUserUnlocked() // Required to complete initialization.
-                    }
-
-                    injectControllers()
-                    base.evaluate()
-                } finally {
-                    // Revert Taskbar window.
-                    instrumentation.runOnMainSync {
-                        taskbarManager.destroy()
-                        launcherTaskbarManager?.addTaskbarRootViewToWindow()
-                    }
-                }
-            }
-        }
-    }
-
-    /** Simulates Taskbar recreation lifecycle. */
-    fun recreateTaskbar() {
-        taskbarManager.recreateTaskbar()
-        injectControllers()
-    }
-
-    private fun injectControllers() {
-        val controllers = activityContext.controllers
-        val controllerFieldsByType = controllers.javaClass.fields.associateBy { it.type }
-        testInstance.javaClass.fields
-            .filter { it.isAnnotationPresent(InjectController::class.java) }
-            .forEach {
-                it.set(
-                    testInstance,
-                    controllerFieldsByType[it.type]?.get(controllers)
-                        ?: throw NoSuchElementException("Failed to find controller for ${it.type}"),
-                )
-            }
-    }
-
-    /**
-     * Annotates test controller fields to inject the corresponding controllers from the current
-     * [TaskbarControllers] instance.
-     *
-     * Controllers are injected during test setup and upon calling [recreateTaskbar].
-     *
-     * Multiple controllers can be injected if needed.
-     */
-    @Retention(AnnotationRetention.RUNTIME)
-    @Target(AnnotationTarget.FIELD)
-    annotation class InjectController
-}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsControllerTest.kt
index bfad697..2f0b446 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsControllerTest.kt
@@ -20,19 +20,20 @@
 import android.content.ComponentName
 import android.content.Intent
 import android.os.Process
-import androidx.test.annotation.UiThreadTest
 import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
 import com.android.launcher3.BubbleTextView
 import com.android.launcher3.appprediction.PredictionRowView
 import com.android.launcher3.model.data.AppInfo
 import com.android.launcher3.model.data.WorkspaceItemInfo
 import com.android.launcher3.notification.NotificationKeyData
-import com.android.launcher3.taskbar.TaskbarUnitTestRule
-import com.android.launcher3.taskbar.TaskbarUnitTestRule.InjectController
 import com.android.launcher3.taskbar.overlay.TaskbarOverlayController
+import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule
+import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController
+import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext
 import com.android.launcher3.util.LauncherMultivalentJUnit
 import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices
 import com.android.launcher3.util.PackageUserKey
+import com.android.launcher3.util.TestUtil
 import com.google.common.truth.Truth.assertThat
 import org.junit.Rule
 import org.junit.Test
@@ -43,84 +44,95 @@
 class TaskbarAllAppsControllerTest {
 
     @get:Rule
-    val taskbarUnitTestRule = TaskbarUnitTestRule(this, getInstrumentation().targetContext)
+    val taskbarUnitTestRule =
+        TaskbarUnitTestRule(
+            this,
+            TaskbarWindowSandboxContext.create(getInstrumentation().targetContext),
+        )
     @get:Rule val animatorTestRule = AnimatorTestRule(this)
 
     @InjectController lateinit var allAppsController: TaskbarAllAppsController
     @InjectController lateinit var overlayController: TaskbarOverlayController
 
     @Test
-    @UiThreadTest
     fun testToggle_once_showsAllApps() {
-        allAppsController.toggle()
+        getInstrumentation().runOnMainSync { allAppsController.toggle() }
         assertThat(allAppsController.isOpen).isTrue()
     }
 
     @Test
-    @UiThreadTest
     fun testToggle_twice_closesAllApps() {
-        allAppsController.toggle()
-        allAppsController.toggle()
+        getInstrumentation().runOnMainSync {
+            allAppsController.toggle()
+            allAppsController.toggle()
+        }
         assertThat(allAppsController.isOpen).isFalse()
     }
 
     @Test
-    @UiThreadTest
     fun testToggle_taskbarRecreated_allAppsReopened() {
-        allAppsController.toggle()
+        getInstrumentation().runOnMainSync { allAppsController.toggle() }
         taskbarUnitTestRule.recreateTaskbar()
         assertThat(allAppsController.isOpen).isTrue()
     }
 
     @Test
-    @UiThreadTest
     fun testSetApps_beforeOpened_cachesInfo() {
-        allAppsController.setApps(TEST_APPS, 0, emptyMap())
-        allAppsController.toggle()
+        val overlayContext =
+            TestUtil.getOnUiThread {
+                allAppsController.setApps(TEST_APPS, 0, emptyMap())
+                allAppsController.toggle()
+                overlayController.requestWindow()
+            }
 
-        val overlayContext = overlayController.requestWindow()
         assertThat(overlayContext.appsView.appsStore.apps).isEqualTo(TEST_APPS)
     }
 
     @Test
-    @UiThreadTest
     fun testSetApps_afterOpened_updatesStore() {
-        allAppsController.toggle()
-        allAppsController.setApps(TEST_APPS, 0, emptyMap())
+        val overlayContext =
+            TestUtil.getOnUiThread {
+                allAppsController.toggle()
+                allAppsController.setApps(TEST_APPS, 0, emptyMap())
+                overlayController.requestWindow()
+            }
 
-        val overlayContext = overlayController.requestWindow()
         assertThat(overlayContext.appsView.appsStore.apps).isEqualTo(TEST_APPS)
     }
 
     @Test
-    @UiThreadTest
     fun testSetPredictedApps_beforeOpened_cachesInfo() {
-        allAppsController.setPredictedApps(TEST_PREDICTED_APPS)
-        allAppsController.toggle()
-
         val predictedApps =
-            overlayController
-                .requestWindow()
-                .appsView
-                .floatingHeaderView
-                .findFixedRowByType(PredictionRowView::class.java)
-                .predictedApps
+            TestUtil.getOnUiThread {
+                allAppsController.setPredictedApps(TEST_PREDICTED_APPS)
+                allAppsController.toggle()
+
+                overlayController
+                    .requestWindow()
+                    .appsView
+                    .floatingHeaderView
+                    .findFixedRowByType(PredictionRowView::class.java)
+                    .predictedApps
+            }
+
         assertThat(predictedApps).isEqualTo(TEST_PREDICTED_APPS)
     }
 
     @Test
-    @UiThreadTest
     fun testSetPredictedApps_afterOpened_cachesInfo() {
-        allAppsController.toggle()
-        allAppsController.setPredictedApps(TEST_PREDICTED_APPS)
-
         val predictedApps =
-            overlayController
-                .requestWindow()
-                .appsView
-                .floatingHeaderView
-                .findFixedRowByType(PredictionRowView::class.java)
-                .predictedApps
+            TestUtil.getOnUiThread {
+                allAppsController.toggle()
+                allAppsController.setPredictedApps(TEST_PREDICTED_APPS)
+
+                overlayController
+                    .requestWindow()
+                    .appsView
+                    .floatingHeaderView
+                    .findFixedRowByType(PredictionRowView::class.java)
+                    .predictedApps
+            }
+
         assertThat(predictedApps).isEqualTo(TEST_PREDICTED_APPS)
     }
 
@@ -136,36 +148,38 @@
         }
 
         // Ensure the recycler view fully inflates before trying to grab an icon.
-        getInstrumentation().runOnMainSync {
-            val btv =
+        val btv =
+            TestUtil.getOnUiThread {
                 overlayController
                     .requestWindow()
                     .appsView
                     .activeRecyclerView
                     .findViewHolderForAdapterPosition(0)
                     ?.itemView as? BubbleTextView
-            assertThat(btv?.hasDot()).isTrue()
-        }
+            }
+        assertThat(btv?.hasDot()).isTrue()
     }
 
     @Test
-    @UiThreadTest
     fun testUpdateNotificationDots_predictedApp_hasDot() {
-        allAppsController.setPredictedApps(TEST_PREDICTED_APPS)
-        allAppsController.toggle()
+        getInstrumentation().runOnMainSync {
+            allAppsController.setPredictedApps(TEST_PREDICTED_APPS)
+            allAppsController.toggle()
+            taskbarUnitTestRule.activityContext.popupDataProvider.onNotificationPosted(
+                PackageUserKey.fromItemInfo(TEST_PREDICTED_APPS[0]),
+                NotificationKeyData("key"),
+            )
+        }
 
-        taskbarUnitTestRule.activityContext.popupDataProvider.onNotificationPosted(
-            PackageUserKey.fromItemInfo(TEST_PREDICTED_APPS[0]),
-            NotificationKeyData("key"),
-        )
-
-        val predictionRowView =
-            overlayController
-                .requestWindow()
-                .appsView
-                .floatingHeaderView
-                .findFixedRowByType(PredictionRowView::class.java)
-        val btv = predictionRowView.getChildAt(0) as BubbleTextView
+        val btv =
+            TestUtil.getOnUiThread {
+                overlayController
+                    .requestWindow()
+                    .appsView
+                    .floatingHeaderView
+                    .findFixedRowByType(PredictionRowView::class.java)
+                    .getChildAt(0) as BubbleTextView
+            }
         assertThat(btv.hasDot()).isTrue()
     }
 
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayControllerTest.kt
index 72bdc16..f946d4d 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayControllerTest.kt
@@ -18,7 +18,6 @@
 
 import android.app.ActivityManager.RunningTaskInfo
 import android.view.MotionEvent
-import androidx.test.annotation.UiThreadTest
 import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
 import com.android.launcher3.AbstractFloatingView
 import com.android.launcher3.AbstractFloatingView.TYPE_OPTIONS_POPUP
@@ -26,11 +25,12 @@
 import com.android.launcher3.AbstractFloatingView.TYPE_TASKBAR_OVERLAY_PROXY
 import com.android.launcher3.AbstractFloatingView.hasOpenView
 import com.android.launcher3.taskbar.TaskbarActivityContext
-import com.android.launcher3.taskbar.TaskbarUnitTestRule
-import com.android.launcher3.taskbar.TaskbarUnitTestRule.InjectController
+import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule
+import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController
+import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext
 import com.android.launcher3.util.LauncherMultivalentJUnit
 import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices
-import com.android.launcher3.views.BaseDragLayer
+import com.android.launcher3.util.TestUtil.getOnUiThread
 import com.android.systemui.shared.system.TaskStackChangeListeners
 import com.google.common.truth.Truth.assertThat
 import org.junit.Rule
@@ -42,81 +42,80 @@
 class TaskbarOverlayControllerTest {
 
     @get:Rule
-    val taskbarUnitTestRule = TaskbarUnitTestRule(this, getInstrumentation().targetContext)
+    val taskbarUnitTestRule =
+        TaskbarUnitTestRule(
+            this,
+            TaskbarWindowSandboxContext.create(getInstrumentation().targetContext),
+        )
     @InjectController lateinit var overlayController: TaskbarOverlayController
 
     private val taskbarContext: TaskbarActivityContext
         get() = taskbarUnitTestRule.activityContext
 
     @Test
-    @UiThreadTest
     fun testRequestWindow_twice_reusesWindow() {
-        val context1 = overlayController.requestWindow()
-        val context2 = overlayController.requestWindow()
+        val (context1, context2) =
+            getOnUiThread {
+                Pair(overlayController.requestWindow(), overlayController.requestWindow())
+            }
         assertThat(context1).isSameInstanceAs(context2)
     }
 
     @Test
-    @UiThreadTest
     fun testRequestWindow_afterHidingExistingWindow_createsNewWindow() {
-        val context1 = overlayController.requestWindow()
-        overlayController.hideWindow()
+        val context1 = getOnUiThread { overlayController.requestWindow() }
+        getInstrumentation().runOnMainSync { overlayController.hideWindow() }
 
-        val context2 = overlayController.requestWindow()
+        val context2 = getOnUiThread { overlayController.requestWindow() }
         assertThat(context1).isNotSameInstanceAs(context2)
     }
 
     @Test
-    @UiThreadTest
     fun testRequestWindow_afterHidingOverlay_createsNewWindow() {
-        val context1 = overlayController.requestWindow()
-        TestOverlayView.show(context1)
-        overlayController.hideWindow()
+        val context1 = getOnUiThread { overlayController.requestWindow() }
+        getInstrumentation().runOnMainSync {
+            TestOverlayView.show(context1)
+            overlayController.hideWindow()
+        }
 
-        val context2 = overlayController.requestWindow()
+        val context2 = getOnUiThread { overlayController.requestWindow() }
         assertThat(context1).isNotSameInstanceAs(context2)
     }
 
     @Test
-    @UiThreadTest
     fun testRequestWindow_addsProxyView() {
-        TestOverlayView.show(overlayController.requestWindow())
+        getInstrumentation().runOnMainSync {
+            TestOverlayView.show(overlayController.requestWindow())
+        }
         assertThat(hasOpenView(taskbarContext, TYPE_TASKBAR_OVERLAY_PROXY)).isTrue()
     }
 
     @Test
-    @UiThreadTest
     fun testRequestWindow_closeProxyView_closesOverlay() {
-        val overlay = TestOverlayView.show(overlayController.requestWindow())
-        AbstractFloatingView.closeOpenContainer(taskbarContext, TYPE_TASKBAR_OVERLAY_PROXY)
+        val overlay = getOnUiThread { TestOverlayView.show(overlayController.requestWindow()) }
+        getInstrumentation().runOnMainSync {
+            AbstractFloatingView.closeOpenContainer(taskbarContext, TYPE_TASKBAR_OVERLAY_PROXY)
+        }
         assertThat(overlay.isOpen).isFalse()
     }
 
     @Test
     fun testRequestWindow_attachesDragLayer() {
-        lateinit var dragLayer: BaseDragLayer<*>
-        getInstrumentation().runOnMainSync {
-            dragLayer = overlayController.requestWindow().dragLayer
-        }
-
+        val dragLayer = getOnUiThread { overlayController.requestWindow().dragLayer }
         // Allow drag layer to attach before checking.
         getInstrumentation().runOnMainSync { assertThat(dragLayer.isAttachedToWindow).isTrue() }
     }
 
     @Test
-    @UiThreadTest
     fun testHideWindow_closesOverlay() {
-        val overlay = TestOverlayView.show(overlayController.requestWindow())
-        overlayController.hideWindow()
+        val overlay = getOnUiThread { TestOverlayView.show(overlayController.requestWindow()) }
+        getInstrumentation().runOnMainSync { overlayController.hideWindow() }
         assertThat(overlay.isOpen).isFalse()
     }
 
     @Test
     fun testHideWindow_detachesDragLayer() {
-        lateinit var dragLayer: BaseDragLayer<*>
-        getInstrumentation().runOnMainSync {
-            dragLayer = overlayController.requestWindow().dragLayer
-        }
+        val dragLayer = getOnUiThread { overlayController.requestWindow().dragLayer }
 
         // Wait for drag layer to be attached to window before hiding.
         getInstrumentation().runOnMainSync {
@@ -126,44 +125,45 @@
     }
 
     @Test
-    @UiThreadTest
     fun testTwoOverlays_closeOne_windowStaysOpen() {
-        val context = overlayController.requestWindow()
-        val overlay1 = TestOverlayView.show(context)
-        val overlay2 = TestOverlayView.show(context)
+        val (overlay1, overlay2) =
+            getOnUiThread {
+                val context = overlayController.requestWindow()
+                Pair(TestOverlayView.show(context), TestOverlayView.show(context))
+            }
 
-        overlay1.close(false)
+        getInstrumentation().runOnMainSync { overlay1.close(false) }
         assertThat(overlay2.isOpen).isTrue()
         assertThat(hasOpenView(taskbarContext, TYPE_TASKBAR_OVERLAY_PROXY)).isTrue()
     }
 
     @Test
-    @UiThreadTest
     fun testTwoOverlays_closeAll_closesWindow() {
-        val context = overlayController.requestWindow()
-        val overlay1 = TestOverlayView.show(context)
-        val overlay2 = TestOverlayView.show(context)
+        val (overlay1, overlay2) =
+            getOnUiThread {
+                val context = overlayController.requestWindow()
+                Pair(TestOverlayView.show(context), TestOverlayView.show(context))
+            }
 
-        overlay1.close(false)
-        overlay2.close(false)
+        getInstrumentation().runOnMainSync {
+            overlay1.close(false)
+            overlay2.close(false)
+        }
         assertThat(hasOpenView(taskbarContext, TYPE_TASKBAR_OVERLAY_PROXY)).isFalse()
     }
 
     @Test
-    @UiThreadTest
     fun testRecreateTaskbar_closesWindow() {
-        TestOverlayView.show(overlayController.requestWindow())
+        getInstrumentation().runOnMainSync {
+            TestOverlayView.show(overlayController.requestWindow())
+        }
         taskbarUnitTestRule.recreateTaskbar()
         assertThat(hasOpenView(taskbarContext, TYPE_TASKBAR_OVERLAY_PROXY)).isFalse()
     }
 
     @Test
     fun testTaskMovedToFront_closesOverlay() {
-        lateinit var overlay: TestOverlayView
-        getInstrumentation().runOnMainSync {
-            overlay = TestOverlayView.show(overlayController.requestWindow())
-        }
-
+        val overlay = getOnUiThread { TestOverlayView.show(overlayController.requestWindow()) }
         TaskStackChangeListeners.getInstance().listenerImpl.onTaskMovedToFront(RunningTaskInfo())
         // Make sure TaskStackChangeListeners' Handler posts the callback before checking state.
         getInstrumentation().runOnMainSync { assertThat(overlay.isOpen).isFalse() }
@@ -171,9 +171,8 @@
 
     @Test
     fun testTaskStackChanged_allAppsClosed_overlayStaysOpen() {
-        lateinit var overlay: TestOverlayView
+        val overlay = getOnUiThread { TestOverlayView.show(overlayController.requestWindow()) }
         getInstrumentation().runOnMainSync {
-            overlay = TestOverlayView.show(overlayController.requestWindow())
             taskbarContext.controllers.sharedState?.allAppsVisible = false
         }
 
@@ -183,9 +182,8 @@
 
     @Test
     fun testTaskStackChanged_allAppsOpen_closesOverlay() {
-        lateinit var overlay: TestOverlayView
+        val overlay = getOnUiThread { TestOverlayView.show(overlayController.requestWindow()) }
         getInstrumentation().runOnMainSync {
-            overlay = TestOverlayView.show(overlayController.requestWindow())
             taskbarContext.controllers.sharedState?.allAppsVisible = true
         }
 
@@ -194,33 +192,39 @@
     }
 
     @Test
-    @UiThreadTest
     fun testUpdateLauncherDeviceProfile_overlayNotRebindSafe_closesOverlay() {
-        val overlayContext = overlayController.requestWindow()
-        val overlay = TestOverlayView.show(overlayContext).apply { type = TYPE_OPTIONS_POPUP }
+        val context = getOnUiThread { overlayController.requestWindow() }
+        val overlay = getOnUiThread {
+            TestOverlayView.show(context).apply { type = TYPE_OPTIONS_POPUP }
+        }
 
-        overlayController.updateLauncherDeviceProfile(
-            overlayController.launcherDeviceProfile
-                .toBuilder(overlayContext)
-                .setGestureMode(false)
-                .build()
-        )
+        getInstrumentation().runOnMainSync {
+            overlayController.updateLauncherDeviceProfile(
+                overlayController.launcherDeviceProfile
+                    .toBuilder(context)
+                    .setGestureMode(false)
+                    .build()
+            )
+        }
 
         assertThat(overlay.isOpen).isFalse()
     }
 
     @Test
-    @UiThreadTest
     fun testUpdateLauncherDeviceProfile_overlayRebindSafe_overlayStaysOpen() {
-        val overlayContext = overlayController.requestWindow()
-        val overlay = TestOverlayView.show(overlayContext).apply { type = TYPE_TASKBAR_ALL_APPS }
+        val context = getOnUiThread { overlayController.requestWindow() }
+        val overlay = getOnUiThread {
+            TestOverlayView.show(context).apply { type = TYPE_TASKBAR_ALL_APPS }
+        }
 
-        overlayController.updateLauncherDeviceProfile(
-            overlayController.launcherDeviceProfile
-                .toBuilder(overlayContext)
-                .setGestureMode(false)
-                .build()
-        )
+        getInstrumentation().runOnMainSync {
+            overlayController.updateLauncherDeviceProfile(
+                overlayController.launcherDeviceProfile
+                    .toBuilder(context)
+                    .setGestureMode(false)
+                    .build()
+            )
+        }
 
         assertThat(overlay.isOpen).isTrue()
     }
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarModeRule.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRule.kt
similarity index 60%
rename from quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarModeRule.kt
rename to quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRule.kt
index 3b53cdc..c48947e 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarModeRule.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRule.kt
@@ -14,13 +14,13 @@
  * limitations under the License.
  */
 
-package com.android.launcher3.taskbar
+package com.android.launcher3.taskbar.rules
 
-import com.android.launcher3.taskbar.TaskbarModeRule.Mode
-import com.android.launcher3.taskbar.TaskbarModeRule.TaskbarMode
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode
+import com.android.launcher3.taskbar.rules.TaskbarModeRule.TaskbarMode
 import com.android.launcher3.util.DisplayController
 import com.android.launcher3.util.MainThreadInitializedObject
-import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext
 import com.android.launcher3.util.NavigationMode
 import org.junit.rules.TestRule
 import org.junit.runner.Description
@@ -40,7 +40,7 @@
  * Make sure this rule precedes any rules that depend on [DisplayController], or else the instance
  * might be inconsistent across the test lifecycle.
  */
-class TaskbarModeRule(private val context: SandboxContext) : TestRule {
+class TaskbarModeRule(private val context: TaskbarWindowSandboxContext) : TestRule {
     /** The selected Taskbar mode. */
     enum class Mode {
         TRANSIENT,
@@ -60,23 +60,25 @@
             override fun evaluate() {
                 val mode = taskbarMode.mode
 
-                context.putObject(
-                    DisplayController.INSTANCE,
-                    object : DisplayController(context) {
-                        override fun getInfo(): Info {
-                            return spy(super.getInfo()) {
-                                on { isTransientTaskbar } doReturn (mode == Mode.TRANSIENT)
-                                on { isPinnedTaskbar } doReturn (mode == Mode.PINNED)
-                                on { navigationMode } doReturn
-                                    when (mode) {
-                                        Mode.TRANSIENT,
-                                        Mode.PINNED -> NavigationMode.NO_BUTTON
-                                        Mode.THREE_BUTTONS -> NavigationMode.THREE_BUTTONS
-                                    }
+                getInstrumentation().runOnMainSync {
+                    context.applicationContext.putObject(
+                        DisplayController.INSTANCE,
+                        object : DisplayController(context) {
+                            override fun getInfo(): Info {
+                                return spy(super.getInfo()) {
+                                    on { isTransientTaskbar } doReturn (mode == Mode.TRANSIENT)
+                                    on { isPinnedTaskbar } doReturn (mode == Mode.PINNED)
+                                    on { navigationMode } doReturn
+                                        when (mode) {
+                                            Mode.TRANSIENT,
+                                            Mode.PINNED -> NavigationMode.NO_BUTTON
+                                            Mode.THREE_BUTTONS -> NavigationMode.THREE_BUTTONS
+                                        }
+                                }
                             }
-                        }
-                    },
-                )
+                        },
+                    )
+                }
 
                 base.evaluate()
             }
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarModeRuleTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRuleTest.kt
similarity index 87%
rename from quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarModeRuleTest.kt
rename to quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRuleTest.kt
index 7dfbb9a..f75e542 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarModeRuleTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRuleTest.kt
@@ -14,17 +14,16 @@
  * limitations under the License.
  */
 
-package com.android.launcher3.taskbar
+package com.android.launcher3.taskbar.rules
 
 import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
 import com.android.launcher3.InvariantDeviceProfile
-import com.android.launcher3.taskbar.TaskbarModeRule.Mode.PINNED
-import com.android.launcher3.taskbar.TaskbarModeRule.Mode.THREE_BUTTONS
-import com.android.launcher3.taskbar.TaskbarModeRule.Mode.TRANSIENT
-import com.android.launcher3.taskbar.TaskbarModeRule.TaskbarMode
+import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.PINNED
+import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.THREE_BUTTONS
+import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.TRANSIENT
+import com.android.launcher3.taskbar.rules.TaskbarModeRule.TaskbarMode
 import com.android.launcher3.util.DisplayController
 import com.android.launcher3.util.LauncherMultivalentJUnit
-import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext
 import com.android.launcher3.util.NavigationMode
 import com.google.common.truth.Truth.assertThat
 import org.junit.Rule
@@ -34,7 +33,7 @@
 @RunWith(LauncherMultivalentJUnit::class)
 class TaskbarModeRuleTest {
 
-    private val context = SandboxContext(getInstrumentation().targetContext)
+    private val context = TaskbarWindowSandboxContext.create(getInstrumentation().targetContext)
 
     @get:Rule val taskbarModeRule = TaskbarModeRule(context)
 
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPinningPreferenceRule.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPinningPreferenceRule.kt
new file mode 100644
index 0000000..d417790
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPinningPreferenceRule.kt
@@ -0,0 +1,65 @@
+/*
+ * 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.taskbar.rules
+
+import android.platform.test.flag.junit.FlagsParameterization
+import android.platform.test.flag.junit.SetFlagsRule
+import com.android.launcher3.Flags.FLAG_ENABLE_TASKBAR_PINNING
+import com.android.launcher3.LauncherPrefs.Companion.TASKBAR_PINNING
+import com.android.launcher3.LauncherPrefs.Companion.TASKBAR_PINNING_IN_DESKTOP_MODE
+import com.android.launcher3.util.DisplayController
+import org.junit.rules.RuleChain
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+/**
+ * Rule that allows modifying the Taskbar pinned preferences.
+ *
+ * The original preference values are restored on teardown.
+ *
+ * If this rule is being used with [TaskbarUnitTestRule], make sure this rule is applied first.
+ *
+ * This rule is overkill if a test does not need to change the mode during Taskbar's lifecycle. If
+ * the mode is static, use [TaskbarModeRule] instead, which forces the mode. A test can class can
+ * declare both this rule and [TaskbarModeRule] but using both for a test method is unsupported.
+ */
+class TaskbarPinningPreferenceRule(context: TaskbarWindowSandboxContext) : TestRule {
+
+    private val setFlagsRule =
+        SetFlagsRule(FlagsParameterization(mapOf(FLAG_ENABLE_TASKBAR_PINNING to true)))
+    private val pinningRule = TaskbarPreferenceRule(context, TASKBAR_PINNING)
+    private val desktopPinningRule = TaskbarPreferenceRule(context, TASKBAR_PINNING_IN_DESKTOP_MODE)
+    private val ruleChain =
+        RuleChain.outerRule(setFlagsRule).around(pinningRule).around(desktopPinningRule)
+
+    var isPinned by pinningRule::value
+    var isPinnedInDesktopMode by desktopPinningRule::value
+
+    override fun apply(base: Statement, description: Description): Statement {
+        return object : Statement() {
+            override fun evaluate() {
+                DisplayController.enableTaskbarModePreferenceForTests(true)
+                try {
+                    ruleChain.apply(base, description).evaluate()
+                } finally {
+                    DisplayController.enableTaskbarModePreferenceForTests(false)
+                }
+            }
+        }
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPinningPreferenceRuleTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPinningPreferenceRuleTest.kt
new file mode 100644
index 0000000..a709133
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPinningPreferenceRuleTest.kt
@@ -0,0 +1,112 @@
+/*
+ * 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.taskbar.rules
+
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.android.launcher3.util.DisplayController
+import com.android.launcher3.util.LauncherMultivalentJUnit
+import com.android.launcher3.util.window.WindowManagerProxy
+import com.google.android.apps.nexuslauncher.deviceemulator.TestWindowManagerProxy
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.Description
+import org.junit.runner.RunWith
+import org.junit.runners.model.Statement
+
+@RunWith(LauncherMultivalentJUnit::class)
+class TaskbarPinningPreferenceRuleTest {
+    private val context = TaskbarWindowSandboxContext.create(getInstrumentation().targetContext)
+
+    private val preferenceRule = TaskbarPinningPreferenceRule(context)
+
+    @Test
+    fun testEnablePinning_verifyDisplayController() {
+        onSetup {
+            preferenceRule.isPinned = true
+            preferenceRule.isPinnedInDesktopMode = false
+            assertThat(DisplayController.isPinnedTaskbar(context)).isTrue()
+        }
+    }
+
+    @Test
+    fun testDisablePinning_verifyDisplayController() {
+        onSetup {
+            preferenceRule.isPinned = false
+            preferenceRule.isPinnedInDesktopMode = false
+            assertThat(DisplayController.isPinnedTaskbar(context)).isFalse()
+        }
+    }
+
+    @Test
+    fun testEnableDesktopPinning_verifyDisplayController() {
+        context.applicationContext.putObject(
+            WindowManagerProxy.INSTANCE,
+            TestWindowManagerProxy(context).apply { isInDesktopMode = true },
+        )
+
+        onSetup {
+            preferenceRule.isPinned = false
+            preferenceRule.isPinnedInDesktopMode = true
+            assertThat(DisplayController.isPinnedTaskbar(context)).isTrue()
+        }
+    }
+
+    @Test
+    fun testDisableDesktopPinning_verifyDisplayController() {
+        context.applicationContext.putObject(
+            WindowManagerProxy.INSTANCE,
+            TestWindowManagerProxy(context).apply { isInDesktopMode = true },
+        )
+
+        onSetup {
+            preferenceRule.isPinned = false
+            preferenceRule.isPinnedInDesktopMode = false
+            assertThat(DisplayController.isPinnedTaskbar(context)).isFalse()
+        }
+    }
+
+    @Test
+    fun testTearDown_afterTogglingPinnedPreference_preferenceReset() {
+        val wasPinned = preferenceRule.isPinned
+        onSetup { preferenceRule.isPinned = !preferenceRule.isPinned }
+        assertThat(preferenceRule.isPinned).isEqualTo(wasPinned)
+    }
+
+    @Test
+    fun testTearDown_afterTogglingDesktopPreference_preferenceReset() {
+        val wasPinnedInDesktopMode = preferenceRule.isPinnedInDesktopMode
+        onSetup { preferenceRule.isPinnedInDesktopMode = !preferenceRule.isPinnedInDesktopMode }
+        assertThat(preferenceRule.isPinnedInDesktopMode).isEqualTo(wasPinnedInDesktopMode)
+    }
+
+    /** Executes [runTest] after the [preferenceRule] setup phase completes. */
+    private fun onSetup(runTest: () -> Unit) {
+        preferenceRule
+            .apply(
+                object : Statement() {
+                    override fun evaluate() = runTest()
+                },
+                DESCRIPTION,
+            )
+            .evaluate()
+    }
+
+    private companion object {
+        private val DESCRIPTION =
+            Description.createSuiteDescription(TaskbarPinningPreferenceRule::class.java)
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPreferenceRule.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPreferenceRule.kt
new file mode 100644
index 0000000..a76a77d
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPreferenceRule.kt
@@ -0,0 +1,54 @@
+/*
+ * 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.taskbar.rules
+
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.android.launcher3.ConstantItem
+import com.android.launcher3.LauncherPrefs
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+/**
+ * Rule for modifying a Taskbar preference.
+ *
+ * The original preference value is restored on teardown.
+ */
+class TaskbarPreferenceRule<T : Any>(
+    context: TaskbarWindowSandboxContext,
+    private val constantItem: ConstantItem<T>
+) : TestRule {
+
+    private val prefs = LauncherPrefs.get(context)
+
+    var value: T
+        get() = prefs.get(constantItem)
+        set(value) = getInstrumentation().runOnMainSync { prefs.put(constantItem, value) }
+
+    override fun apply(base: Statement, description: Description): Statement {
+        return object : Statement() {
+            override fun evaluate() {
+                val originalValue = value
+                try {
+                    base.evaluate()
+                } finally {
+                    value = originalValue
+                }
+            }
+        }
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPreferenceRuleTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPreferenceRuleTest.kt
new file mode 100644
index 0000000..22d2079
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPreferenceRuleTest.kt
@@ -0,0 +1,65 @@
+/*
+ * 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.taskbar.rules
+
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.android.launcher3.LauncherPrefs.Companion.TASKBAR_PINNING
+import com.android.launcher3.util.LauncherMultivalentJUnit
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.Description
+import org.junit.runner.RunWith
+import org.junit.runners.model.Statement
+
+@RunWith(LauncherMultivalentJUnit::class)
+class TaskbarPreferenceRuleTest {
+
+    private val context = TaskbarWindowSandboxContext.create(getInstrumentation().targetContext)
+    private val preferenceRule = TaskbarPreferenceRule(context, TASKBAR_PINNING)
+
+    @Test
+    fun testSetup_toggleBoolean_updatesPreferences() {
+        val originalValue = preferenceRule.value
+        onSetup {
+            preferenceRule.value = !preferenceRule.value
+            assertThat(preferenceRule.value).isNotEqualTo(originalValue)
+        }
+    }
+
+    @Test
+    fun testTeardown_afterTogglingBoolean_preferenceReset() {
+        val originalValue = preferenceRule.value
+        onSetup { preferenceRule.value = !preferenceRule.value }
+        assertThat(preferenceRule.value).isEqualTo(originalValue)
+    }
+
+    private fun onSetup(runTest: () -> Unit) {
+        preferenceRule
+            .apply(
+                object : Statement() {
+                    override fun evaluate() = runTest()
+                },
+                DESCRIPTION,
+            )
+            .evaluate()
+    }
+
+    private companion object {
+        private val DESCRIPTION =
+            Description.createSuiteDescription(TaskbarPreferenceRule::class.java)
+    }
+}
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
new file mode 100644
index 0000000..a966d2a
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt
@@ -0,0 +1,234 @@
+/*
+ * 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.taskbar.rules
+
+import android.app.Instrumentation
+import android.app.PendingIntent
+import android.content.IIntentSender
+import android.content.Intent
+import android.provider.Settings
+import android.provider.Settings.Secure.NAV_BAR_KIDS_MODE
+import android.provider.Settings.Secure.USER_SETUP_COMPLETE
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.rule.ServiceTestRule
+import com.android.launcher3.LauncherAppState
+import com.android.launcher3.taskbar.TaskbarActivityContext
+import com.android.launcher3.taskbar.TaskbarManager
+import com.android.launcher3.taskbar.TaskbarNavButtonController.TaskbarNavButtonCallbacks
+import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController
+import com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR
+import com.android.launcher3.util.LauncherMultivalentJUnit.Companion.isRunningInRobolectric
+import com.android.launcher3.util.ModelTestExtensions.loadModelSync
+import com.android.launcher3.util.TestUtil
+import com.android.quickstep.AllAppsActionManager
+import com.android.quickstep.TouchInteractionService
+import com.android.quickstep.TouchInteractionService.TISBinder
+import org.junit.Assume.assumeTrue
+import org.junit.rules.RuleChain
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+/**
+ * Manages the Taskbar lifecycle for unit tests.
+ *
+ * Tests should pass in themselves as [testInstance]. They also need to provide their target
+ * [context] through the constructor.
+ *
+ * See [InjectController] for grabbing controller(s) under test with minimal boilerplate.
+ *
+ * The rule interacts with [TaskbarManager] on the main thread. A good rule of thumb for tests is
+ * that code that is executed on the main thread in production should also happen on that thread
+ * when tested.
+ *
+ * `@UiThreadTest` is incompatible with this rule. The annotation causes this rule to run on the
+ * main thread, but it needs to be run on the test thread for it to work properly. Instead, only run
+ * code that requires the main thread using something like [Instrumentation.runOnMainSync] or
+ * [TestUtil.getOnUiThread].
+ *
+ * ```
+ * @Test
+ * fun example() {
+ *     instrumentation.runOnMainSync { doWorkThatPostsMessage() }
+ *     // Second lambda will not execute until message is processed.
+ *     instrumentation.runOnMainSync { verifyMessageResults() }
+ * }
+ * ```
+ */
+class TaskbarUnitTestRule(
+    private val testInstance: Any,
+    private val context: TaskbarWindowSandboxContext,
+) : TestRule {
+
+    private val instrumentation = InstrumentationRegistry.getInstrumentation()
+    private val serviceTestRule = ServiceTestRule()
+
+    private val userSetupCompleteRule = TaskbarSecureSettingRule(USER_SETUP_COMPLETE)
+    private val kidsModeRule = TaskbarSecureSettingRule(NAV_BAR_KIDS_MODE)
+    private val settingRules = RuleChain.outerRule(userSetupCompleteRule).around(kidsModeRule)
+
+    private lateinit var taskbarManager: TaskbarManager
+
+    val activityContext: TaskbarActivityContext
+        get() {
+            return taskbarManager.currentActivityContext
+                ?: throw RuntimeException("Failed to obtain TaskbarActivityContext.")
+        }
+
+    override fun apply(base: Statement, description: Description): Statement {
+        return settingRules.apply(createStatement(base, description), description)
+    }
+
+    private fun createStatement(base: Statement, description: Description): Statement {
+        return object : Statement() {
+            override fun evaluate() {
+
+                // Only run test when Taskbar is enabled.
+                instrumentation.runOnMainSync {
+                    assumeTrue(
+                        LauncherAppState.getIDP(context).getDeviceProfile(context).isTaskbarPresent
+                    )
+                }
+
+                // Process secure setting annotations.
+                instrumentation.runOnMainSync {
+                    userSetupCompleteRule.putInt(
+                        if (description.getAnnotation(UserSetupMode::class.java) != null) {
+                            0
+                        } else {
+                            1
+                        }
+                    )
+                    kidsModeRule.putInt(
+                        if (description.getAnnotation(NavBarKidsMode::class.java) != null) 1 else 0
+                    )
+                }
+
+                // Check for existing Taskbar instance from Launcher process.
+                val launcherTaskbarManager: TaskbarManager? =
+                    if (!isRunningInRobolectric) {
+                        try {
+                            val tisBinder =
+                                serviceTestRule.bindService(
+                                    Intent(context, TouchInteractionService::class.java)
+                                ) as? TISBinder
+                            tisBinder?.taskbarManager
+                        } catch (_: Exception) {
+                            null
+                        }
+                    } else {
+                        null
+                    }
+
+                taskbarManager =
+                    TestUtil.getOnUiThread {
+                        object :
+                            TaskbarManager(
+                                context,
+                                AllAppsActionManager(context, UI_HELPER_EXECUTOR) {
+                                    PendingIntent(IIntentSender.Default())
+                                },
+                                object : TaskbarNavButtonCallbacks {},
+                            ) {
+                            override fun recreateTaskbar() {
+                                super.recreateTaskbar()
+                                if (currentActivityContext != null) injectControllers()
+                            }
+                        }
+                    }
+
+                try {
+                    LauncherAppState.getInstance(context).model.loadModelSync()
+
+                    // Replace Launcher Taskbar window with test instance.
+                    instrumentation.runOnMainSync {
+                        launcherTaskbarManager?.setSuspended(true)
+                        taskbarManager.onUserUnlocked() // Required to complete initialization.
+                    }
+
+                    base.evaluate()
+                } finally {
+                    // Revert Taskbar window.
+                    instrumentation.runOnMainSync {
+                        taskbarManager.destroy()
+                        launcherTaskbarManager?.setSuspended(false)
+                    }
+                }
+            }
+        }
+    }
+
+    /** Simulates Taskbar recreation lifecycle. */
+    fun recreateTaskbar() = instrumentation.runOnMainSync { taskbarManager.recreateTaskbar() }
+
+    private fun injectControllers() {
+        val controllers = activityContext.controllers
+        val controllerFieldsByType = controllers.javaClass.fields.associateBy { it.type }
+        testInstance.javaClass.fields
+            .filter { it.isAnnotationPresent(InjectController::class.java) }
+            .forEach {
+                it.set(
+                    testInstance,
+                    controllerFieldsByType[it.type]?.get(controllers)
+                        ?: throw NoSuchElementException("Failed to find controller for ${it.type}"),
+                )
+            }
+    }
+
+    /**
+     * Annotates test controller fields to inject the corresponding controllers from the current
+     * [TaskbarControllers] instance.
+     *
+     * Controllers are injected during test setup and upon calling [recreateTaskbar].
+     *
+     * Multiple controllers can be injected if needed.
+     */
+    @Retention(AnnotationRetention.RUNTIME)
+    @Target(AnnotationTarget.FIELD)
+    annotation class InjectController
+
+    /** Overrides [USER_SETUP_COMPLETE] to be `false` for tests. */
+    @Retention(AnnotationRetention.RUNTIME)
+    @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
+    annotation class UserSetupMode
+
+    /** Overrides [NAV_BAR_KIDS_MODE] to be `true` for tests. */
+    @Retention(AnnotationRetention.RUNTIME)
+    @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
+    annotation class NavBarKidsMode
+
+    /** Rule for Taskbar integer-based secure settings. */
+    private inner class TaskbarSecureSettingRule(private val settingName: String) : TestRule {
+
+        override fun apply(base: Statement, description: Description): Statement {
+            return object : Statement() {
+                override fun evaluate() {
+                    val originalValue =
+                        Settings.Secure.getInt(context.contentResolver, settingName, /* def= */ 0)
+                    try {
+                        base.evaluate()
+                    } finally {
+                        instrumentation.runOnMainSync { putInt(originalValue) }
+                    }
+                }
+            }
+        }
+
+        /** Puts [value] into secure settings under [settingName]. */
+        fun putInt(value: Int) = Settings.Secure.putInt(context.contentResolver, settingName, value)
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRuleTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRuleTest.kt
new file mode 100644
index 0000000..234e499
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRuleTest.kt
@@ -0,0 +1,184 @@
+/*
+ * 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.taskbar.rules
+
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.android.launcher3.taskbar.TaskbarActivityContext
+import com.android.launcher3.taskbar.TaskbarKeyguardController
+import com.android.launcher3.taskbar.TaskbarManager
+import com.android.launcher3.taskbar.TaskbarStashController
+import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController
+import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.NavBarKidsMode
+import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.UserSetupMode
+import com.android.launcher3.util.LauncherMultivalentJUnit
+import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertThrows
+import org.junit.Test
+import org.junit.runner.Description
+import org.junit.runner.RunWith
+import org.junit.runners.model.Statement
+
+@RunWith(LauncherMultivalentJUnit::class)
+@EmulatedDevices(["pixelFoldable2023"])
+class TaskbarUnitTestRuleTest {
+
+    private val context = TaskbarWindowSandboxContext.create(getInstrumentation().targetContext)
+
+    @Test
+    fun testSetup_taskbarInitialized() {
+        onSetup { assertThat(activityContext).isInstanceOf(TaskbarActivityContext::class.java) }
+    }
+
+    @Test
+    fun testRecreateTaskbar_activityContextChanged() {
+        onSetup {
+            val context1 = activityContext
+            recreateTaskbar()
+            val context2 = activityContext
+            assertThat(context1).isNotSameInstanceAs(context2)
+        }
+    }
+
+    @Test
+    fun testTeardown_taskbarDestroyed() {
+        val testRule = TaskbarUnitTestRule(this, context)
+        testRule.apply(EMPTY_STATEMENT, DESCRIPTION).evaluate()
+        assertThrows(RuntimeException::class.java) { testRule.activityContext }
+    }
+
+    @Test
+    fun testInjectController_validControllerType_isInjected() {
+        val testClass =
+            object {
+                @InjectController lateinit var controller: TaskbarStashController
+                val isInjected: Boolean
+                    get() = ::controller.isInitialized
+            }
+
+        TaskbarUnitTestRule(testClass, context).apply(EMPTY_STATEMENT, DESCRIPTION).evaluate()
+
+        onSetup(TaskbarUnitTestRule(testClass, context)) {
+            assertThat(testClass.isInjected).isTrue()
+        }
+    }
+
+    @Test
+    fun testInjectController_multipleControllers_areInjected() {
+        val testClass =
+            object {
+                @InjectController lateinit var controller1: TaskbarStashController
+                @InjectController lateinit var controller2: TaskbarKeyguardController
+                val areInjected: Boolean
+                    get() = ::controller1.isInitialized && ::controller2.isInitialized
+            }
+
+        onSetup(TaskbarUnitTestRule(testClass, context)) {
+            assertThat(testClass.areInjected).isTrue()
+        }
+    }
+
+    @Test
+    fun testInjectController_invalidControllerType_exceptionThrown() {
+        val testClass =
+            object {
+                @InjectController lateinit var manager: TaskbarManager // Not a controller.
+            }
+
+        // We cannot use #assertThrows because we also catch an assumption violated exception when
+        // running #evaluate on devices that do not support Taskbar.
+        val result =
+            try {
+                TaskbarUnitTestRule(testClass, context)
+                    .apply(EMPTY_STATEMENT, DESCRIPTION)
+                    .evaluate()
+            } catch (e: NoSuchElementException) {
+                e
+            }
+        assertThat(result).isInstanceOf(NoSuchElementException::class.java)
+    }
+
+    @Test
+    fun testInjectController_recreateTaskbar_controllerChanged() {
+        val testClass =
+            object {
+                @InjectController lateinit var controller: TaskbarStashController
+            }
+
+        onSetup(TaskbarUnitTestRule(testClass, context)) {
+            val controller1 = testClass.controller
+            recreateTaskbar()
+            val controller2 = testClass.controller
+            assertThat(controller1).isNotSameInstanceAs(controller2)
+        }
+    }
+
+    @Test
+    fun testUserSetupMode_default_isComplete() {
+        onSetup { assertThat(activityContext.isUserSetupComplete).isTrue() }
+    }
+
+    @Test
+    fun testUserSetupMode_withAnnotation_isIncomplete() {
+        @UserSetupMode class Mode
+        onSetup(description = Description.createSuiteDescription(Mode::class.java)) {
+            assertThat(activityContext.isUserSetupComplete).isFalse()
+        }
+    }
+
+    @Test
+    fun testNavBarKidsMode_default_navBarNotForcedVisible() {
+        onSetup { assertThat(activityContext.isNavBarForceVisible).isFalse() }
+    }
+
+    @Test
+    fun testNavBarKidsMode_withAnnotation_navBarForcedVisible() {
+        @NavBarKidsMode class Mode
+        onSetup(description = Description.createSuiteDescription(Mode::class.java)) {
+            assertThat(activityContext.isNavBarForceVisible).isTrue()
+        }
+    }
+
+    /**
+     * Executes [runTest] after the [testRule] setup phase completes.
+     *
+     * A [description] can also be provided to mimic annotating a test or test class.
+     */
+    private fun onSetup(
+        testRule: TaskbarUnitTestRule = TaskbarUnitTestRule(this, context),
+        description: Description = DESCRIPTION,
+        runTest: TaskbarUnitTestRule.() -> Unit,
+    ) {
+        testRule
+            .apply(
+                object : Statement() {
+                    override fun evaluate() = runTest(testRule)
+                },
+                description,
+            )
+            .evaluate()
+    }
+
+    private companion object {
+        private val EMPTY_STATEMENT =
+            object : Statement() {
+                override fun evaluate() = Unit
+            }
+        private val DESCRIPTION =
+            Description.createSuiteDescription(TaskbarUnitTestRuleTest::class.java)
+    }
+}
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
new file mode 100644
index 0000000..ee21df8
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContext.kt
@@ -0,0 +1,61 @@
+/*
+ * 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.taskbar.rules
+
+import android.content.Context
+import android.content.ContextWrapper
+import android.os.Bundle
+import android.view.Display
+import com.android.launcher3.util.MainThreadInitializedObject
+import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext
+
+/**
+ * Sandbox wrapper where [createWindowContext] provides contexts that are still sandboxed within
+ * [application].
+ *
+ * Taskbar can create window contexts, which need to operate under the same sandbox application, but
+ * [Context.getApplicationContext] by default returns the actual application. For this reason,
+ * [SandboxContext] overrides [getApplicationContext] to return itself, which prevents leaving the
+ * sandbox. [SandboxContext] and the real application have different sets of
+ * [MainThreadInitializedObject] instances, so overriding the application prevents the latter set
+ * from leaking into the sandbox. Similarly, this implementation overrides [getApplicationContext]
+ * to return the original sandboxed [application], and it wraps created windowed contexts to
+ * propagate this [application].
+ */
+class TaskbarWindowSandboxContext
+private constructor(private val application: SandboxContext, base: Context) : ContextWrapper(base) {
+
+    override fun createWindowContext(type: Int, options: Bundle?): Context {
+        return TaskbarWindowSandboxContext(application, super.createWindowContext(type, options))
+    }
+
+    override fun createWindowContext(display: Display, type: Int, options: Bundle?): Context {
+        return TaskbarWindowSandboxContext(
+            application,
+            super.createWindowContext(display, type, options),
+        )
+    }
+
+    override fun getApplicationContext(): SandboxContext = application
+
+    companion object {
+        /** Creates a [TaskbarWindowSandboxContext] to sandbox [base] for Taskbar tests. */
+        fun create(base: Context): TaskbarWindowSandboxContext {
+            return SandboxContext(base).let { TaskbarWindowSandboxContext(it, it) }
+        }
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContextTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContextTest.kt
new file mode 100644
index 0000000..ad4b4de
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContextTest.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.taskbar.rules
+
+import android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.android.launcher3.util.LauncherMultivalentJUnit
+import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(LauncherMultivalentJUnit::class)
+class TaskbarWindowSandboxContextTest {
+
+    private val context = TaskbarWindowSandboxContext.create(getInstrumentation().targetContext)
+
+    @Test
+    fun testCreateWindowContext_applicationContextSandboxed() {
+        val windowContext = context.createWindowContext(TYPE_APPLICATION_OVERLAY, null)
+        assertThat(windowContext.applicationContext).isInstanceOf(SandboxContext::class.java)
+    }
+
+    @Test
+    fun testCreateWindowContext_nested_applicationContextSandboxed() {
+        val windowContext = context.createWindowContext(TYPE_APPLICATION_OVERLAY, null)
+        val nestedContext = windowContext.createWindowContext(TYPE_APPLICATION_OVERLAY, null)
+        assertThat(nestedContext.applicationContext).isInstanceOf(SandboxContext::class.java)
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskIconDataSource.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskIconDataSource.kt
new file mode 100644
index 0000000..242bc73
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskIconDataSource.kt
@@ -0,0 +1,58 @@
+/*
+ * 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.data
+
+import android.graphics.drawable.Drawable
+import com.android.launcher3.util.CancellableTask
+import com.android.quickstep.TaskIconCache
+import com.android.quickstep.task.thumbnail.data.TaskIconDataSource
+import com.android.systemui.shared.recents.model.Task
+import com.google.common.truth.Truth.assertThat
+import org.mockito.kotlin.mock
+
+class FakeTaskIconDataSource : TaskIconDataSource {
+
+    val taskIdToDrawable: Map<Int, Drawable> = (0..10).associateWith { mock() }
+    val taskIdToUpdatingTask: MutableMap<Int, () -> Unit> = mutableMapOf()
+    var shouldLoadSynchronously: Boolean = true
+
+    /** Retrieves and sets an icon on [task] from [taskIdToDrawable]. */
+    override fun getIconInBackground(
+        task: Task,
+        callback: TaskIconCache.GetTaskIconCallback
+    ): CancellableTask<*>? {
+        val wrappedCallback = {
+            callback.onTaskIconReceived(
+                taskIdToDrawable.getValue(task.key.id),
+                "content desc ${task.key.id}",
+                "title ${task.key.id}"
+            )
+        }
+        if (shouldLoadSynchronously) {
+            wrappedCallback()
+        } else {
+            taskIdToUpdatingTask[task.key.id] = wrappedCallback
+        }
+        return null
+    }
+}
+
+fun Task.assertHasIconDataFromSource(fakeTaskIconDataSource: FakeTaskIconDataSource) {
+    assertThat(icon).isEqualTo(fakeTaskIconDataSource.taskIdToDrawable[key.id])
+    assertThat(titleDescription).isEqualTo("content desc ${key.id}")
+    assertThat(title).isEqualTo("title ${key.id}")
+}
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 b66b735..30fc491 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
@@ -32,7 +32,7 @@
     var shouldLoadSynchronously: Boolean = true
 
     /** Retrieves and sets a thumbnail on [task] from [taskIdToBitmap]. */
-    override fun updateThumbnailInBackground(
+    override fun getThumbnailInBackground(
         task: Task,
         callback: Consumer<ThumbnailData>
     ): CancellableTask<ThumbnailData>? {
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTasksRepository.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTasksRepository.kt
index e160627..19990a8 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTasksRepository.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTasksRepository.kt
@@ -32,6 +32,9 @@
     override fun getTaskDataById(taskId: Int): Flow<Task?> =
         getAllTaskData().map { taskList -> taskList.firstOrNull { it.key.id == taskId } }
 
+    override fun getThumbnailById(taskId: Int): Flow<ThumbnailData?> =
+        getTaskDataById(taskId).map { it?.thumbnail }
+
     override fun setVisibleTasks(visibleTaskIdList: List<Int>) {
         visibleTasks.value = visibleTaskIdList
         tasks.value = tasks.value.map { it.apply { thumbnail = thumbnailDataMap[it.key.id] } }
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 c28a85a..88fa190 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
@@ -18,7 +18,6 @@
 
 import android.content.ComponentName
 import android.content.Intent
-import com.android.quickstep.TaskIconCache
 import com.android.quickstep.util.DesktopTask
 import com.android.quickstep.util.GroupTask
 import com.android.systemui.shared.recents.model.Task
@@ -31,7 +30,6 @@
 import kotlinx.coroutines.test.UnconfinedTestDispatcher
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
-import org.mockito.kotlin.mock
 
 @OptIn(ExperimentalCoroutinesApi::class)
 class TasksRepositoryTest {
@@ -44,10 +42,10 @@
         )
     private val recentsModel = FakeRecentTasksDataSource()
     private val taskThumbnailDataSource = FakeTaskThumbnailDataSource()
-    private val taskIconCache = mock<TaskIconCache>()
+    private val taskIconDataSource = FakeTaskIconDataSource()
 
     private val systemUnderTest =
-        TasksRepository(recentsModel, taskThumbnailDataSource, taskIconCache)
+        TasksRepository(recentsModel, taskThumbnailDataSource, taskIconDataSource)
 
     @Test
     fun getAllTaskDataReturnsFlattenedListOfTasks() = runTest {
@@ -81,6 +79,22 @@
     }
 
     @Test
+    fun setVisibleTasksPopulatesIcons() = runTest {
+        recentsModel.seedTasks(defaultTaskList)
+        systemUnderTest.getAllTaskData(forceRefresh = true)
+
+        systemUnderTest.setVisibleTasks(listOf(1, 2))
+
+        // .drop(1) to ignore initial null content before from thumbnail was loaded.
+        systemUnderTest
+            .getTaskDataById(1)
+            .drop(1)
+            .first()!!
+            .assertHasIconDataFromSource(taskIconDataSource)
+        systemUnderTest.getTaskDataById(2).first()!!.assertHasIconDataFromSource(taskIconDataSource)
+    }
+
+    @Test
     fun changingVisibleTasksContainsAlreadyPopulatedThumbnails() = runTest {
         recentsModel.seedTasks(defaultTaskList)
         val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2]
@@ -101,7 +115,28 @@
     }
 
     @Test
-    fun retrievedThumbnailsAreDiscardedWhenTaskBecomesInvisible() = runTest {
+    fun changingVisibleTasksContainsAlreadyPopulatedIcons() = runTest {
+        recentsModel.seedTasks(defaultTaskList)
+        systemUnderTest.getAllTaskData(forceRefresh = true)
+
+        systemUnderTest.setVisibleTasks(listOf(1, 2))
+
+        // .drop(1) to ignore initial null content before from icon was loaded.
+        systemUnderTest
+            .getTaskDataById(2)
+            .drop(1)
+            .first()!!
+            .assertHasIconDataFromSource(taskIconDataSource)
+
+        // Prevent new loading of Drawables
+        taskThumbnailDataSource.shouldLoadSynchronously = false
+        systemUnderTest.setVisibleTasks(listOf(2, 3))
+
+        systemUnderTest.getTaskDataById(2).first()!!.assertHasIconDataFromSource(taskIconDataSource)
+    }
+
+    @Test
+    fun retrievedImagesAreDiscardedWhenTaskBecomesInvisible() = runTest {
         recentsModel.seedTasks(defaultTaskList)
         val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2]
         systemUnderTest.getAllTaskData(forceRefresh = true)
@@ -109,14 +144,20 @@
         systemUnderTest.setVisibleTasks(listOf(1, 2))
 
         // .drop(1) to ignore initial null content before from thumbnail was loaded.
-        assertThat(systemUnderTest.getTaskDataById(2).drop(1).first()!!.thumbnail!!.thumbnail)
-            .isEqualTo(bitmap2)
+        val task2 = systemUnderTest.getTaskDataById(2).drop(1).first()!!
+        assertThat(task2.thumbnail!!.thumbnail).isEqualTo(bitmap2)
+        task2.assertHasIconDataFromSource(taskIconDataSource)
 
         // Prevent new loading of Bitmaps
         taskThumbnailDataSource.shouldLoadSynchronously = false
+        taskIconDataSource.shouldLoadSynchronously = false
         systemUnderTest.setVisibleTasks(listOf(0, 1))
 
-        assertThat(systemUnderTest.getTaskDataById(2).first()!!.thumbnail).isNull()
+        val task2AfterVisibleTasksChanged = systemUnderTest.getTaskDataById(2).first()!!
+        assertThat(task2AfterVisibleTasksChanged.thumbnail).isNull()
+        assertThat(task2AfterVisibleTasksChanged.icon).isNull()
+        assertThat(task2AfterVisibleTasksChanged.titleDescription).isNull()
+        assertThat(task2AfterVisibleTasksChanged.title).isNull()
     }
 
     @Test
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelTest.kt
index a394b65..b78f871 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelTest.kt
@@ -30,6 +30,7 @@
 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
 import com.android.quickstep.task.viewmodel.TaskContainerData
 import com.android.quickstep.task.viewmodel.TaskViewData
+import com.android.quickstep.views.TaskViewType
 import com.android.systemui.shared.recents.model.Task
 import com.android.systemui.shared.recents.model.ThumbnailData
 import com.google.common.truth.Truth.assertThat
@@ -42,12 +43,14 @@
 
 @RunWith(AndroidJUnit4::class)
 class TaskThumbnailViewModelTest {
+    private var taskViewType = TaskViewType.SINGLE
     private val recentsViewData = RecentsViewData()
-    private val taskViewData = TaskViewData()
+    private val taskViewData by lazy { TaskViewData(taskViewType) }
     private val taskContainerData = TaskContainerData()
     private val tasksRepository = FakeTasksRepository()
-    private val systemUnderTest =
+    private val systemUnderTest by lazy {
         TaskThumbnailViewModel(recentsViewData, taskViewData, taskContainerData, tasksRepository)
+    }
 
     private val tasks = (0..5).map(::createTaskWithId)
 
@@ -66,14 +69,26 @@
     }
 
     @Test
-    fun setRecentsFullscreenProgress_thenProgressIsPassedThrough() = runTest {
+    fun setRecentsFullscreenProgress_thenCornerRadiusProgressIsPassedThrough() = runTest {
         recentsViewData.fullscreenProgress.value = 0.5f
 
-        assertThat(systemUnderTest.recentsFullscreenProgress.first()).isEqualTo(0.5f)
+        assertThat(systemUnderTest.cornerRadiusProgress.first()).isEqualTo(0.5f)
 
         recentsViewData.fullscreenProgress.value = 0.6f
 
-        assertThat(systemUnderTest.recentsFullscreenProgress.first()).isEqualTo(0.6f)
+        assertThat(systemUnderTest.cornerRadiusProgress.first()).isEqualTo(0.6f)
+    }
+
+    @Test
+    fun setRecentsFullscreenProgress_thenCornerRadiusProgressIsConstantForDesktop() = runTest {
+        taskViewType = TaskViewType.DESKTOP
+        recentsViewData.fullscreenProgress.value = 0.5f
+
+        assertThat(systemUnderTest.cornerRadiusProgress.first()).isEqualTo(1f)
+
+        recentsViewData.fullscreenProgress.value = 0.6f
+
+        assertThat(systemUnderTest.cornerRadiusProgress.first()).isEqualTo(1f)
     }
 
     @Test
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/util/GetThumbnailUseCaseTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/util/GetThumbnailUseCaseTest.kt
new file mode 100644
index 0000000..414f8ca
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/util/GetThumbnailUseCaseTest.kt
@@ -0,0 +1,84 @@
+/*
+ * 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.util
+
+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(listOf(TaskOverlayViewModelTest.TASK_ID))
+
+        assertThat(systemUnderTest.run(TASK_ID)).isEqualTo(thumbnailData.thumbnail)
+    }
+
+    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/viewmodel/TaskOverlayViewModelTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModelTest.kt
new file mode 100644
index 0000000..40482c4
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModelTest.kt
@@ -0,0 +1,160 @@
+/*
+ * 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.quickstep.recents.data.FakeTasksRepository
+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.systemui.shared.recents.model.Task
+import com.android.systemui.shared.recents.model.ThumbnailData
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.first
+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] */
+@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 systemUnderTest = TaskOverlayViewModel(task, recentsViewData, tasksRepository)
+
+    @Test
+    fun initialStateIsDisabled() = runTest {
+        assertThat(systemUnderTest.overlayState.first()).isEqualTo(Disabled)
+    }
+
+    @Test
+    fun recentsViewOverlayDisabled_Disabled() = runTest {
+        recentsViewData.overlayEnabled.value = false
+        recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID)
+
+        assertThat(systemUnderTest.overlayState.first()).isEqualTo(Disabled)
+    }
+
+    @Test
+    fun taskNotFullyVisible_Disabled() = runTest {
+        recentsViewData.overlayEnabled.value = true
+        recentsViewData.settledFullyVisibleTaskIds.value = setOf()
+
+        assertThat(systemUnderTest.overlayState.first()).isEqualTo(Disabled)
+    }
+
+    @Test
+    fun noThumbnail_Enabled() = runTest {
+        recentsViewData.overlayEnabled.value = true
+        recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID)
+        task.isLocked = false
+
+        assertThat(systemUnderTest.overlayState.first())
+            .isEqualTo(
+                Enabled(
+                    isRealSnapshot = false,
+                    thumbnail = null,
+                    thumbnailMatrix = Matrix.IDENTITY_MATRIX
+                )
+            )
+    }
+
+    @Test
+    fun withThumbnail_RealSnapshot_NotLocked_Enabled() = runTest {
+        recentsViewData.overlayEnabled.value = true
+        recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID)
+        tasksRepository.seedTasks(listOf(task))
+        tasksRepository.seedThumbnailData(mapOf(TASK_ID to thumbnailData))
+        tasksRepository.setVisibleTasks(listOf(TASK_ID))
+        thumbnailData.isRealSnapshot = true
+        task.isLocked = false
+
+        assertThat(systemUnderTest.overlayState.first())
+            .isEqualTo(
+                Enabled(
+                    isRealSnapshot = true,
+                    thumbnail = thumbnailData.thumbnail,
+                    thumbnailMatrix = Matrix.IDENTITY_MATRIX
+                )
+            )
+    }
+
+    @Test
+    fun withThumbnail_RealSnapshot_Locked_Enabled() = runTest {
+        recentsViewData.overlayEnabled.value = true
+        recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID)
+        tasksRepository.seedTasks(listOf(task))
+        tasksRepository.seedThumbnailData(mapOf(TASK_ID to thumbnailData))
+        tasksRepository.setVisibleTasks(listOf(TASK_ID))
+        thumbnailData.isRealSnapshot = true
+        task.isLocked = true
+
+        assertThat(systemUnderTest.overlayState.first())
+            .isEqualTo(
+                Enabled(
+                    isRealSnapshot = false,
+                    thumbnail = thumbnailData.thumbnail,
+                    thumbnailMatrix = Matrix.IDENTITY_MATRIX
+                )
+            )
+    }
+
+    @Test
+    fun withThumbnail_FakeSnapshot_Enabled() = runTest {
+        recentsViewData.overlayEnabled.value = true
+        recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID)
+        tasksRepository.seedTasks(listOf(task))
+        tasksRepository.seedThumbnailData(mapOf(TASK_ID to thumbnailData))
+        tasksRepository.setVisibleTasks(listOf(TASK_ID))
+        thumbnailData.isRealSnapshot = false
+        task.isLocked = false
+
+        assertThat(systemUnderTest.overlayState.first())
+            .isEqualTo(
+                Enabled(
+                    isRealSnapshot = false,
+                    thumbnail = thumbnailData.thumbnail,
+                    thumbnailMatrix = Matrix.IDENTITY_MATRIX
+                )
+            )
+    }
+
+    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/util/DesktopTaskTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/DesktopTaskTest.kt
new file mode 100644
index 0000000..7aed579
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/DesktopTaskTest.kt
@@ -0,0 +1,67 @@
+/*
+ * 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.util
+
+import android.content.ComponentName
+import android.content.Intent
+import com.android.launcher3.util.LauncherMultivalentJUnit
+import com.android.systemui.shared.recents.model.Task
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(LauncherMultivalentJUnit::class)
+class DesktopTaskTest {
+
+    @Test
+    fun testDesktopTask_sameInstance_isEqual() {
+        val task = DesktopTask(createTasks(1))
+        assertThat(task).isEqualTo(task)
+    }
+
+    @Test
+    fun testDesktopTask_identicalConstructor_isEqual() {
+        val task1 = DesktopTask(createTasks(1))
+        val task2 = DesktopTask(createTasks(1))
+        assertThat(task1).isEqualTo(task2)
+    }
+
+    @Test
+    fun testDesktopTask_copy_isEqual() {
+        val task1 = DesktopTask(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))
+        assertThat(task1).isNotEqualTo(task2)
+    }
+
+    @Test
+    fun testDesktopTask_differentLength_isNotEqual() {
+        val task1 = DesktopTask(createTasks(1))
+        val task2 = DesktopTask(createTasks(1, 2))
+        assertThat(task1).isNotEqualTo(task2)
+    }
+
+    private fun createTasks(vararg ids: Int): List<Task> {
+        return ids.map { Task(Task.TaskKey(it, 0, Intent(), ComponentName("", ""), 0, 0)) }
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/GroupTaskTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/GroupTaskTest.kt
new file mode 100644
index 0000000..f11cd0b
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/GroupTaskTest.kt
@@ -0,0 +1,109 @@
+/*
+ * 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.util
+
+import android.content.ComponentName
+import android.content.Intent
+import android.graphics.Rect
+import com.android.launcher3.util.LauncherMultivalentJUnit
+import com.android.launcher3.util.SplitConfigurationOptions
+import com.android.quickstep.views.TaskViewType
+import com.android.systemui.shared.recents.model.Task
+import com.android.wm.shell.common.split.SplitScreenConstants
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(LauncherMultivalentJUnit::class)
+class GroupTaskTest {
+
+    @Test
+    fun testGroupTask_sameInstance_isEqual() {
+        val task = GroupTask(createTask(1))
+        assertThat(task).isEqualTo(task)
+    }
+
+    @Test
+    fun testGroupTask_identicalConstructor_isEqual() {
+        val task1 = GroupTask(createTask(1))
+        val task2 = GroupTask(createTask(1))
+        assertThat(task1).isEqualTo(task2)
+    }
+
+    @Test
+    fun testGroupTask_copy_isEqual() {
+        val task1 = GroupTask(createTask(1))
+        val task2 = task1.copy()
+        assertThat(task1).isEqualTo(task2)
+    }
+
+    @Test
+    fun testGroupTask_differentId_isNotEqual() {
+        val task1 = GroupTask(createTask(1))
+        val task2 = GroupTask(createTask(2))
+        assertThat(task1).isNotEqualTo(task2)
+    }
+
+    @Test
+    fun testGroupTask_equalSplitTasks_isEqual() {
+        val splitBounds =
+            SplitConfigurationOptions.SplitBounds(
+                Rect(),
+                Rect(),
+                1,
+                2,
+                SplitScreenConstants.SNAP_TO_50_50
+            )
+        val task1 = GroupTask(createTask(1), createTask(2), splitBounds, TaskViewType.GROUPED)
+        val task2 = GroupTask(createTask(1), createTask(2), splitBounds, TaskViewType.GROUPED)
+        assertThat(task1).isEqualTo(task2)
+    }
+
+    @Test
+    fun testGroupTask_differentSplitTasks_isNotEqual() {
+        val splitBounds1 =
+            SplitConfigurationOptions.SplitBounds(
+                Rect(),
+                Rect(),
+                1,
+                2,
+                SplitScreenConstants.SNAP_TO_50_50
+            )
+        val splitBounds2 =
+            SplitConfigurationOptions.SplitBounds(
+                Rect(),
+                Rect(),
+                1,
+                2,
+                SplitScreenConstants.SNAP_TO_30_70
+            )
+        val task1 = GroupTask(createTask(1), createTask(2), splitBounds1, TaskViewType.GROUPED)
+        val task2 = GroupTask(createTask(1), createTask(2), splitBounds2, TaskViewType.GROUPED)
+        assertThat(task1).isNotEqualTo(task2)
+    }
+
+    @Test
+    fun testGroupTask_differentType_isNotEqual() {
+        val task1 = GroupTask(createTask(1), null, null, TaskViewType.SINGLE)
+        val task2 = GroupTask(createTask(1), null, null, TaskViewType.DESKTOP)
+        assertThat(task1).isNotEqualTo(task2)
+    }
+
+    private fun createTask(id: Int): Task {
+        return Task(Task.TaskKey(id, 0, Intent(), ComponentName("", ""), 0, 0))
+    }
+}
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 7d12400..f3cde52 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt
@@ -33,8 +33,8 @@
 import com.android.launcher3.util.SplitConfigurationOptions
 import com.android.quickstep.views.GroupedTaskView
 import com.android.quickstep.views.IconView
+import com.android.quickstep.views.TaskContainer
 import com.android.quickstep.views.TaskView
-import com.android.quickstep.views.TaskView.TaskContainer
 import com.android.systemui.shared.recents.model.Task
 import org.junit.Assert.assertEquals
 import org.junit.Before
@@ -87,7 +87,7 @@
     @Before
     fun setup() {
         whenever(mockTaskContainer.snapshotView).thenReturn(mockSnapshotView)
-        whenever(mockTaskContainer.thumbnail).thenReturn(mockBitmap)
+        whenever(mockTaskContainer.splitAnimationThumbnail).thenReturn(mockBitmap)
         whenever(mockTaskContainer.iconView).thenReturn(mockIconView)
         whenever(mockIconView.drawable).thenReturn(mockTaskViewDrawable)
         whenever(mockTaskView.taskContainers).thenReturn(List(1) { mockTaskContainer })
@@ -276,7 +276,7 @@
         whenever(mockAppPairIcon.context).thenReturn(mockContextThemeWrapper)
         doNothing()
             .whenever(spySplitAnimationController)
-            .composeIconSplitLaunchAnimator(any(), any(), any(), any())
+            .composeIconSplitLaunchAnimator(any(), any(), any(), any(), any())
         doReturn(-1).whenever(spySplitAnimationController).hasChangesForBothAppPairs(any(), any())
 
         spySplitAnimationController.playSplitLaunchAnimation(
@@ -296,7 +296,7 @@
         )
 
         verify(spySplitAnimationController)
-            .composeIconSplitLaunchAnimator(any(), any(), any(), any())
+            .composeIconSplitLaunchAnimator(any(), any(), any(), any(), any())
     }
 
     @Test
diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarHoverToolTipControllerTest.java b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarHoverToolTipControllerTest.java
index 9ed3906..ef3a833 100644
--- a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarHoverToolTipControllerTest.java
+++ b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarHoverToolTipControllerTest.java
@@ -39,6 +39,7 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.launcher3.BubbleTextView;
+import com.android.launcher3.apppairs.AppPairIcon;
 import com.android.launcher3.folder.Folder;
 import com.android.launcher3.folder.FolderIcon;
 import com.android.launcher3.model.data.FolderInfo;
@@ -66,6 +67,7 @@
     @Mock private MotionEvent mMotionEvent;
     @Mock private BubbleTextView mHoverBubbleTextView;
     @Mock private FolderIcon mHoverFolderIcon;
+    @Mock private AppPairIcon mAppPairIcon;
     @Mock private Display mDisplay;
     @Mock private TaskbarDragLayer mTaskbarDragLayer;
     private Folder mSpyFolderView;
@@ -85,6 +87,7 @@
         when(taskbarActivityContext.getDragLayer()).thenReturn(mTaskbarDragLayer);
         when(taskbarActivityContext.getMainLooper()).thenReturn(context.getMainLooper());
         when(taskbarActivityContext.getDisplay()).thenReturn(mDisplay);
+        when(taskbarActivityContext.isIconAlignedWithHotseat()).thenReturn(false);
 
         when(mTaskbarDragLayer.getChildCount()).thenReturn(1);
         mSpyFolderView = spy(new Folder(new ActivityContextWrapper(context), null));
@@ -213,6 +216,49 @@
         assertThat(hoverHandled).isFalse();
     }
 
+    @Test
+    public void onHover_hoverEnterAppPair_revealToolTip() {
+        when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_ENTER);
+        when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_ENTER);
+
+        boolean hoverHandled =
+                mTaskbarHoverToolTipController.onHover(mAppPairIcon, mMotionEvent);
+        waitForIdleSync();
+
+        assertThat(hoverHandled).isTrue();
+        verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS,
+                true);
+    }
+
+    @Test
+    public void onHover_hoverExitAppPair_closeToolTip() {
+        when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_EXIT);
+        when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_EXIT);
+
+        boolean hoverHandled =
+                mTaskbarHoverToolTipController.onHover(mAppPairIcon, mMotionEvent);
+        waitForIdleSync();
+
+        assertThat(hoverHandled).isTrue();
+        verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS,
+                false);
+    }
+
+    @Test
+    public void onHover_hoverEnterIconAlignedWithHotseat_noReveal() {
+        when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_ENTER);
+        when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_ENTER);
+        when(taskbarActivityContext.isIconAlignedWithHotseat()).thenReturn(true);
+
+        boolean hoverHandled =
+                mTaskbarHoverToolTipController.onHover(mHoverBubbleTextView, mMotionEvent);
+        waitForIdleSync();
+
+        assertThat(hoverHandled).isTrue();
+        verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS,
+                true);
+    }
+
     private void waitForIdleSync() {
         mTestableLooper.processAllMessages();
     }
diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
index 486dc68..27e761a 100644
--- a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
+++ b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
@@ -26,6 +26,7 @@
 import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION
 import com.android.launcher3.model.data.AppInfo
 import com.android.launcher3.model.data.ItemInfo
+import com.android.launcher3.model.data.TaskItemInfo
 import com.android.launcher3.statehandlers.DesktopVisibilityController
 import com.android.quickstep.RecentsModel
 import com.android.quickstep.RecentsModel.RecentTasksChangedListener
@@ -44,6 +45,7 @@
 import org.mockito.junit.MockitoJUnit
 import org.mockito.kotlin.any
 import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.times
 import org.mockito.kotlin.verify
 import org.mockito.kotlin.whenever
 
@@ -56,7 +58,6 @@
     @Mock private lateinit var mockRecentsModel: RecentsModel
     @Mock private lateinit var mockDesktopVisibilityController: DesktopVisibilityController
 
-    private var nextTaskId: Int = 500
     private var taskListChangeId: Int = 1
 
     private lateinit var recentAppsController: TaskbarRecentAppsController
@@ -78,6 +79,13 @@
         val listenerCaptor = ArgumentCaptor.forClass(RecentTasksChangedListener::class.java)
         verify(mockRecentsModel).registerRecentTasksChangedListener(listenerCaptor.capture())
         recentTasksChangedListener = listenerCaptor.value
+
+        // Make sure updateHotseatItemInfos() is called after commitRunningAppsToUI()
+        whenever(taskbarViewController.commitRunningAppsToUI()).then {
+            recentAppsController.updateHotseatItemInfos(
+                recentAppsController.shownHotseatItems.toTypedArray()
+            )
+        }
     }
 
     @Test
@@ -88,7 +96,7 @@
         val newHotseatItems =
             prepareHotseatAndRunningAndRecentApps(
                 hotseatPackages = hotseatPackages,
-                runningTaskPackages = emptyList(),
+                runningTasks = emptyList(),
                 recentTaskPackages = emptyList()
             )
         assertThat(newHotseatItems.map { it?.targetPackage })
@@ -103,7 +111,7 @@
         val newHotseatItems =
             prepareHotseatAndRunningAndRecentApps(
                 hotseatPackages = hotseatPackages,
-                runningTaskPackages = emptyList(),
+                runningTasks = emptyList(),
                 recentTaskPackages = emptyList()
             )
         assertThat(newHotseatItems.map { it?.targetPackage })
@@ -117,7 +125,7 @@
         val newHotseatItems =
             prepareHotseatAndRunningAndRecentApps(
                 hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2, PREDICTED_PACKAGE_1),
-                runningTaskPackages = emptyList(),
+                runningTasks = emptyList(),
                 recentTaskPackages = emptyList()
             )
         val expectedPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2)
@@ -126,13 +134,58 @@
     }
 
     @Test
+    fun updateHotseatItemInfos_inDesktopMode_hotseatPackageHasRunningTask_hotseatItemLinksToTask() {
+        setInDesktopMode(true)
+
+        val newHotseatItems =
+            prepareHotseatAndRunningAndRecentApps(
+                hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2),
+                runningTasks = listOf(createTask(id = 1, HOTSEAT_PACKAGE_1)),
+                recentTaskPackages = emptyList()
+            )
+
+        assertThat(newHotseatItems).hasLength(2)
+        assertThat(newHotseatItems[0]).isInstanceOf(TaskItemInfo::class.java)
+        assertThat(newHotseatItems[1]).isNotInstanceOf(TaskItemInfo::class.java)
+        val hotseatItem1 = newHotseatItems[0] as TaskItemInfo
+        assertThat(hotseatItem1.taskId).isEqualTo(1)
+    }
+
+    @Test
+    fun updateHotseatItemInfos_inDesktopMode_twoRunningTasksSamePackage_hotseatCoversFirstTask() {
+        setInDesktopMode(true)
+
+        val newHotseatItems =
+            prepareHotseatAndRunningAndRecentApps(
+                hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2),
+                runningTasks =
+                    listOf(
+                        createTask(id = 1, HOTSEAT_PACKAGE_1),
+                        createTask(id = 2, HOTSEAT_PACKAGE_1)
+                    ),
+                recentTaskPackages = emptyList()
+            )
+
+        // First task is in Hotseat Items
+        assertThat(newHotseatItems).hasLength(2)
+        assertThat(newHotseatItems[0]).isInstanceOf(TaskItemInfo::class.java)
+        assertThat(newHotseatItems[1]).isNotInstanceOf(TaskItemInfo::class.java)
+        val hotseatItem1 = newHotseatItems[0] as TaskItemInfo
+        assertThat(hotseatItem1.taskId).isEqualTo(1)
+        // Second task is in shownTasks
+        val shownTasks = recentAppsController.shownTasks.map { it.task1 }
+        assertThat(shownTasks)
+            .containsExactlyElementsIn(listOf(createTask(id = 2, HOTSEAT_PACKAGE_1)))
+    }
+
+    @Test
     fun updateHotseatItemInfos_canShowRecent_notInDesktopMode_returnsNonPredictedHotseatItems() {
         recentAppsController.canShowRecentApps = true
         setInDesktopMode(false)
         val newHotseatItems =
             prepareHotseatAndRunningAndRecentApps(
                 hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2, PREDICTED_PACKAGE_1),
-                runningTaskPackages = emptyList(),
+                runningTasks = emptyList(),
                 recentTaskPackages = emptyList()
             )
         val expectedPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2)
@@ -146,7 +199,11 @@
         setInDesktopMode(true)
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2, PREDICTED_PACKAGE_1),
-            runningTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2),
+            runningTasks =
+                listOf(
+                    createTask(id = 1, RUNNING_APP_PACKAGE_1),
+                    createTask(id = 2, RUNNING_APP_PACKAGE_2)
+                ),
             recentTaskPackages = emptyList()
         )
         assertThat(recentAppsController.shownTasks).isEmpty()
@@ -158,7 +215,7 @@
         setInDesktopMode(false)
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2, PREDICTED_PACKAGE_1),
-            runningTaskPackages = emptyList(),
+            runningTasks = emptyList(),
             recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2)
         )
         assertThat(recentAppsController.shownTasks).isEmpty()
@@ -169,11 +226,15 @@
         setInDesktopMode(false)
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
-            runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2),
+            runningTasks =
+                listOf(
+                    createTask(id = 1, RUNNING_APP_PACKAGE_1),
+                    createTask(id = 2, RUNNING_APP_PACKAGE_2)
+                ),
             recentTaskPackages = emptyList()
         )
         assertThat(recentAppsController.shownTasks).isEmpty()
-        assertThat(recentAppsController.minimizedAppPackages).isEmpty()
+        assertThat(recentAppsController.minimizedTaskIds).isEmpty()
     }
 
     @Test
@@ -181,7 +242,7 @@
         setInDesktopMode(true)
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
-            runningTaskPackages = emptyList(),
+            runningTasks = emptyList(),
             recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2)
         )
         assertThat(recentAppsController.shownTasks).isEmpty()
@@ -190,120 +251,161 @@
     @Test
     fun onRecentTasksChanged_inDesktopMode_shownTasks_returnsRunningTasks() {
         setInDesktopMode(true)
-        val runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
+        val task1 = createTask(id = 1, RUNNING_APP_PACKAGE_1)
+        val task2 = createTask(id = 2, RUNNING_APP_PACKAGE_2)
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
-            runningTaskPackages = runningTaskPackages,
+            runningTasks = listOf(task1, task2),
             recentTaskPackages = emptyList()
         )
-        val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
-        assertThat(shownPackages).containsExactlyElementsIn(runningTaskPackages)
-    }
-
-    @Test
-    fun onRecentTasksChanged_inDesktopMode_runningAppIsHotseatItem_shownTasks_returnsDistinctItems() {
-        setInDesktopMode(true)
-        prepareHotseatAndRunningAndRecentApps(
-            hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2),
-            runningTaskPackages =
-                listOf(HOTSEAT_PACKAGE_1, RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2),
-            recentTaskPackages = emptyList()
-        )
-        val expectedPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
-        val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
-        assertThat(shownPackages).containsExactlyElementsIn(expectedPackages)
+        val shownTasks = recentAppsController.shownTasks.map { it.task1 }
+        assertThat(shownTasks).containsExactlyElementsIn(listOf(task1, task2))
     }
 
     @Test
     fun onRecentTasksChanged_notInDesktopMode_getRunningApps_returnsEmptySet() {
         setInDesktopMode(false)
+        val task1 = createTask(id = 1, RUNNING_APP_PACKAGE_1)
+        val task2 = createTask(id = 2, RUNNING_APP_PACKAGE_2)
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
-            runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2),
+            runningTasks = listOf(task1, task2),
             recentTaskPackages = emptyList()
         )
-        assertThat(recentAppsController.runningAppPackages).isEmpty()
-        assertThat(recentAppsController.minimizedAppPackages).isEmpty()
+        assertThat(recentAppsController.runningTaskIds).isEmpty()
+        assertThat(recentAppsController.minimizedTaskIds).isEmpty()
     }
 
     @Test
     fun onRecentTasksChanged_inDesktopMode_getRunningApps_returnsAllDesktopTasks() {
         setInDesktopMode(true)
-        val runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
+        val task1 = createTask(id = 1, RUNNING_APP_PACKAGE_1)
+        val task2 = createTask(id = 2, RUNNING_APP_PACKAGE_2)
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
-            runningTaskPackages = runningTaskPackages,
+            runningTasks = listOf(task1, task2),
             recentTaskPackages = emptyList()
         )
-        assertThat(recentAppsController.runningAppPackages)
-            .containsExactlyElementsIn(runningTaskPackages)
-        assertThat(recentAppsController.minimizedAppPackages).isEmpty()
+        assertThat(recentAppsController.runningTaskIds).containsExactlyElementsIn(listOf(1, 2))
+        assertThat(recentAppsController.minimizedTaskIds).isEmpty()
     }
 
     @Test
     fun onRecentTasksChanged_inDesktopMode_getRunningApps_includesHotseat() {
         setInDesktopMode(true)
-        val runningTaskPackages =
-            listOf(HOTSEAT_PACKAGE_1, RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
+        val runningTasks =
+            listOf(
+                createTask(id = 1, HOTSEAT_PACKAGE_1),
+                createTask(id = 2, RUNNING_APP_PACKAGE_1),
+                createTask(id = 3, RUNNING_APP_PACKAGE_2)
+            )
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2),
-            runningTaskPackages = runningTaskPackages,
+            runningTasks = runningTasks,
             recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2)
         )
-        assertThat(recentAppsController.runningAppPackages)
-            .containsExactlyElementsIn(runningTaskPackages)
-        assertThat(recentAppsController.minimizedAppPackages).isEmpty()
+        assertThat(recentAppsController.runningTaskIds).containsExactlyElementsIn(listOf(1, 2, 3))
+        assertThat(recentAppsController.minimizedTaskIds).isEmpty()
     }
 
     @Test
-    fun getMinimizedApps_inDesktopMode_returnsAllAppsRunningAndInvisibleAppsMinimized() {
+    fun onRecentTasksChanged_inDesktopMode_allAppsRunningAndInvisibleAppsMinimized() {
         setInDesktopMode(true)
-        val runningTaskPackages =
-            listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2, RUNNING_APP_PACKAGE_3)
-        val minimizedTaskIndices = setOf(2) // RUNNING_APP_PACKAGE_3
+        val task1 = createTask(id = 1, RUNNING_APP_PACKAGE_1)
+        val task2 = createTask(id = 2, RUNNING_APP_PACKAGE_2)
+        val task3Minimized = createTask(id = 3, RUNNING_APP_PACKAGE_3, isVisible = false)
+        val runningTasks = listOf(task1, task2, task3Minimized)
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
-            runningTaskPackages = runningTaskPackages,
-            minimizedTaskIndices = minimizedTaskIndices,
+            runningTasks = runningTasks,
             recentTaskPackages = emptyList()
         )
-        assertThat(recentAppsController.runningAppPackages)
-            .containsExactlyElementsIn(runningTaskPackages)
-        assertThat(recentAppsController.minimizedAppPackages).containsExactly(RUNNING_APP_PACKAGE_3)
+        assertThat(recentAppsController.runningTaskIds).containsExactly(1, 2, 3)
+        assertThat(recentAppsController.minimizedTaskIds).containsExactly(3)
     }
 
     @Test
-    fun getMinimizedApps_inDesktopMode_twoTasksSamePackageOneMinimizedReturnsNotMinimized() {
+    fun onRecentTasksChanged_inDesktopMode_samePackage_differentTasks_severalRunningTasks() {
         setInDesktopMode(true)
-        val runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_1)
-        val minimizedTaskIndices = setOf(1) // The second RUNNING_APP_PACKAGE_1 task.
+        val task1 = createTask(id = 1, RUNNING_APP_PACKAGE_1)
+        val task2 = createTask(id = 2, RUNNING_APP_PACKAGE_2)
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
-            runningTaskPackages = runningTaskPackages,
-            minimizedTaskIndices = minimizedTaskIndices,
+            runningTasks = listOf(task1, task2),
             recentTaskPackages = emptyList()
         )
-        assertThat(recentAppsController.runningAppPackages)
-            .containsExactlyElementsIn(runningTaskPackages.toSet())
-        assertThat(recentAppsController.minimizedAppPackages).isEmpty()
+        assertThat(recentAppsController.runningTaskIds).containsExactlyElementsIn(listOf(1, 2))
     }
 
     @Test
     fun onRecentTasksChanged_inDesktopMode_shownTasks_maintainsOrder() {
         setInDesktopMode(true)
-        val originalOrder = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
+        val task1 = createTask(id = 1, RUNNING_APP_PACKAGE_1)
+        val task2 = createTask(id = 2, RUNNING_APP_PACKAGE_2)
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
-            runningTaskPackages = originalOrder,
+            runningTasks = listOf(task1, task2),
             recentTaskPackages = emptyList()
         )
+
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
-            runningTaskPackages = listOf(RUNNING_APP_PACKAGE_2, RUNNING_APP_PACKAGE_1),
+            runningTasks = listOf(task2, task1),
             recentTaskPackages = emptyList()
         )
-        val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
-        assertThat(shownPackages).isEqualTo(originalOrder)
+
+        val shownTasks = recentAppsController.shownTasks.map { it.task1 }
+        assertThat(shownTasks).isEqualTo(listOf(task1, task2))
+    }
+
+    @Test
+    fun onRecentTasksChanged_inDesktopMode_multiInstance_shownTasks_maintainsOrder() {
+        setInDesktopMode(true)
+        val task1 = createTask(id = 1, RUNNING_APP_PACKAGE_1)
+        val task2 = createTask(id = 2, RUNNING_APP_PACKAGE_1)
+        prepareHotseatAndRunningAndRecentApps(
+            hotseatPackages = emptyList(),
+            runningTasks = listOf(task1, task2),
+            recentTaskPackages = emptyList()
+        )
+
+        prepareHotseatAndRunningAndRecentApps(
+            hotseatPackages = emptyList(),
+            runningTasks = listOf(task2, task1),
+            recentTaskPackages = emptyList()
+        )
+
+        val shownTasks = recentAppsController.shownTasks.map { it.task1 }
+        assertThat(shownTasks).isEqualTo(listOf(task1, task2))
+    }
+
+    @Test
+    fun updateHotseatItems_inDesktopMode_multiInstanceHotseatPackage_shownItems_maintainsOrder() {
+        setInDesktopMode(true)
+        val task1 = createTask(id = 1, RUNNING_APP_PACKAGE_1)
+        val task2 = createTask(id = 2, RUNNING_APP_PACKAGE_1)
+        prepareHotseatAndRunningAndRecentApps(
+            hotseatPackages = listOf(RUNNING_APP_PACKAGE_1),
+            runningTasks = listOf(task1, task2),
+            recentTaskPackages = emptyList()
+        )
+        updateRecentTasks( // Trigger a recent-tasks change before calling updateHotseatItems()
+            runningTasks = listOf(task2, task1),
+            recentTaskPackages = emptyList()
+        )
+
+        prepareHotseatAndRunningAndRecentApps(
+            hotseatPackages = listOf(RUNNING_APP_PACKAGE_1),
+            runningTasks = listOf(task2, task1),
+            recentTaskPackages = emptyList()
+        )
+
+        val newHotseatItems = recentAppsController.shownHotseatItems
+        assertThat(newHotseatItems).hasSize(1)
+        assertThat(newHotseatItems[0]).isInstanceOf(TaskItemInfo::class.java)
+        assertThat((newHotseatItems[0] as TaskItemInfo).taskId).isEqualTo(1)
+        val shownTasks = recentAppsController.shownTasks.map { it.task1 }
+        assertThat(shownTasks).isEqualTo(listOf(task2))
     }
 
     @Test
@@ -311,12 +413,12 @@
         setInDesktopMode(false)
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
-            runningTaskPackages = emptyList(),
+            runningTasks = emptyList(),
             recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2, RECENT_PACKAGE_3)
         )
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
-            runningTaskPackages = emptyList(),
+            runningTasks = emptyList(),
             recentTaskPackages = listOf(RECENT_PACKAGE_2, RECENT_PACKAGE_3, RECENT_PACKAGE_1)
         )
         val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
@@ -327,15 +429,17 @@
     @Test
     fun onRecentTasksChanged_inDesktopMode_addTask_shownTasks_maintainsOrder() {
         setInDesktopMode(true)
+        val task1 = createTask(id = 1, RUNNING_APP_PACKAGE_1)
+        val task2 = createTask(id = 2, RUNNING_APP_PACKAGE_2)
+        val task3 = createTask(id = 3, RUNNING_APP_PACKAGE_3)
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
-            runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2),
+            runningTasks = listOf(task1, task2),
             recentTaskPackages = emptyList()
         )
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
-            runningTaskPackages =
-                listOf(RUNNING_APP_PACKAGE_2, RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_3),
+            runningTasks = listOf(task2, task1, task3),
             recentTaskPackages = emptyList()
         )
         val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
@@ -349,12 +453,12 @@
         setInDesktopMode(false)
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
-            runningTaskPackages = emptyList(),
+            runningTasks = emptyList(),
             recentTaskPackages = listOf(RECENT_PACKAGE_3, RECENT_PACKAGE_2)
         )
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
-            runningTaskPackages = emptyList(),
+            runningTasks = emptyList(),
             recentTaskPackages = listOf(RECENT_PACKAGE_2, RECENT_PACKAGE_3, RECENT_PACKAGE_1)
         )
         val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
@@ -365,15 +469,17 @@
     @Test
     fun onRecentTasksChanged_inDesktopMode_removeTask_shownTasks_maintainsOrder() {
         setInDesktopMode(true)
+        val task1 = createTask(id = 1, RUNNING_APP_PACKAGE_1)
+        val task2 = createTask(id = 2, RUNNING_APP_PACKAGE_2)
+        val task3 = createTask(id = 3, RUNNING_APP_PACKAGE_3)
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
-            runningTaskPackages =
-                listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2, RUNNING_APP_PACKAGE_3),
+            runningTasks = listOf(task1, task2, task3),
             recentTaskPackages = emptyList()
         )
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
-            runningTaskPackages = listOf(RUNNING_APP_PACKAGE_2, RUNNING_APP_PACKAGE_1),
+            runningTasks = listOf(task2, task1),
             recentTaskPackages = emptyList()
         )
         val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
@@ -385,12 +491,12 @@
         setInDesktopMode(false)
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
-            runningTaskPackages = emptyList(),
+            runningTasks = emptyList(),
             recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2, RECENT_PACKAGE_3)
         )
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
-            runningTaskPackages = emptyList(),
+            runningTasks = emptyList(),
             recentTaskPackages = listOf(RECENT_PACKAGE_2, RECENT_PACKAGE_3)
         )
         val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
@@ -401,27 +507,31 @@
     @Test
     fun onRecentTasksChanged_enterDesktopMode_shownTasks_onlyIncludesRunningTasks() {
         setInDesktopMode(false)
-        val runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
+        val runningTask1 = createTask(id = 1, RUNNING_APP_PACKAGE_1)
+        val runningTask2 = createTask(id = 2, RUNNING_APP_PACKAGE_2)
         val recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2)
+
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
-            runningTaskPackages = runningTaskPackages,
+            runningTasks = listOf(runningTask1, runningTask2),
             recentTaskPackages = recentTaskPackages
         )
+
         setInDesktopMode(true)
         recentTasksChangedListener.onRecentTasksChanged()
         val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
-        assertThat(shownPackages).containsExactlyElementsIn(runningTaskPackages)
+        assertThat(shownPackages).containsExactly(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
     }
 
     @Test
     fun onRecentTasksChanged_exitDesktopMode_shownTasks_onlyIncludesRecentTasks() {
         setInDesktopMode(true)
-        val runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
+        val runningTask1 = createTask(id = 1, RUNNING_APP_PACKAGE_1)
+        val runningTask2 = createTask(id = 2, RUNNING_APP_PACKAGE_2)
         val recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2, RECENT_PACKAGE_3)
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
-            runningTaskPackages = runningTaskPackages,
+            runningTasks = listOf(runningTask1, runningTask2),
             recentTaskPackages = recentTaskPackages
         )
         setInDesktopMode(false)
@@ -437,7 +547,7 @@
         setInDesktopMode(false)
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
-            runningTaskPackages = emptyList(),
+            runningTasks = emptyList(),
             recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2, RECENT_PACKAGE_3)
         )
         val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
@@ -449,9 +559,11 @@
     @Test
     fun onRecentTasksChanged_notInDesktopMode_hasRecentAndRunningTasks_shownTasks_returnsRecentTaskAndDesktopTile() {
         setInDesktopMode(false)
+        val runningTask1 = createTask(id = 1, RUNNING_APP_PACKAGE_1)
+        val runningTask2 = createTask(id = 2, RUNNING_APP_PACKAGE_2)
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
-            runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2),
+            runningTasks = listOf(runningTask1, runningTask2),
             recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2)
         )
         val shownPackages = recentAppsController.shownTasks.map { it.packageNames }
@@ -467,7 +579,7 @@
         setInDesktopMode(false)
         prepareHotseatAndRunningAndRecentApps(
             hotseatPackages = emptyList(),
-            runningTaskPackages = emptyList(),
+            runningTasks = emptyList(),
             recentTaskPackages = listOf(RECENT_SPLIT_PACKAGES_1, RECENT_PACKAGE_1, RECENT_PACKAGE_2)
         )
         val shownPackages = recentAppsController.shownTasks.map { it.packageNames }
@@ -478,21 +590,111 @@
         assertThat(shownPackages).containsExactlyElementsIn(expectedPackages)
     }
 
+    @Test
+    fun onRecentTasksChanged_notInDesktopMode_noActualChangeToRecents_commitRunningAppsToUI_notCalled() {
+        setInDesktopMode(false)
+        prepareHotseatAndRunningAndRecentApps(
+            hotseatPackages = emptyList(),
+            runningTasks = emptyList(),
+            recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2)
+        )
+        verify(taskbarViewController, times(1)).commitRunningAppsToUI()
+        // Call onRecentTasksChanged() again with the same tasks, verify it's a no-op.
+        prepareHotseatAndRunningAndRecentApps(
+            hotseatPackages = emptyList(),
+            runningTasks = emptyList(),
+            recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2)
+        )
+        verify(taskbarViewController, times(1)).commitRunningAppsToUI()
+    }
+
+    @Test
+    fun onRecentTasksChanged_inDesktopMode_noActualChangeToRunning_commitRunningAppsToUI_notCalled() {
+        setInDesktopMode(true)
+        val runningTask1 = createTask(id = 1, RUNNING_APP_PACKAGE_1)
+        val runningTask2 = createTask(id = 2, RUNNING_APP_PACKAGE_2)
+        prepareHotseatAndRunningAndRecentApps(
+            hotseatPackages = emptyList(),
+            runningTasks = listOf(runningTask1, runningTask2),
+            recentTaskPackages = emptyList()
+        )
+        verify(taskbarViewController, times(1)).commitRunningAppsToUI()
+        // Call onRecentTasksChanged() again with the same tasks, verify it's a no-op.
+        prepareHotseatAndRunningAndRecentApps(
+            hotseatPackages = emptyList(),
+            runningTasks = listOf(runningTask1, runningTask2),
+            recentTaskPackages = emptyList()
+        )
+        verify(taskbarViewController, times(1)).commitRunningAppsToUI()
+    }
+
+    @Test
+    fun onRecentTasksChanged_onlyMinimizedChanges_commitRunningAppsToUI_isCalled() {
+        setInDesktopMode(true)
+        val task1Minimized = createTask(id = 1, RUNNING_APP_PACKAGE_1, isVisible = false)
+        val task2Visible = createTask(id = 2, RUNNING_APP_PACKAGE_2)
+        val task2Minimized = createTask(id = 2, RUNNING_APP_PACKAGE_2, isVisible = false)
+        prepareHotseatAndRunningAndRecentApps(
+            hotseatPackages = emptyList(),
+            runningTasks = listOf(task1Minimized, task2Visible),
+            recentTaskPackages = emptyList()
+        )
+        verify(taskbarViewController, times(1)).commitRunningAppsToUI()
+
+        // Call onRecentTasksChanged() again with a new minimized app, verify we update UI.
+        prepareHotseatAndRunningAndRecentApps(
+            hotseatPackages = emptyList(),
+            runningTasks = listOf(task1Minimized, task2Minimized),
+            recentTaskPackages = emptyList()
+        )
+
+        verify(taskbarViewController, times(2)).commitRunningAppsToUI()
+    }
+
+    @Test
+    fun onRecentTasksChanged_hotseatAppStartsRunning_commitRunningAppsToUI_isCalled() {
+        setInDesktopMode(true)
+        val hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2)
+        val originalTasks = listOf(createTask(id = 1, RUNNING_APP_PACKAGE_1))
+        val newTasks =
+            listOf(createTask(id = 1, RUNNING_APP_PACKAGE_1), createTask(id = 2, HOTSEAT_PACKAGE_1))
+        prepareHotseatAndRunningAndRecentApps(
+            hotseatPackages = hotseatPackages,
+            runningTasks = originalTasks,
+            recentTaskPackages = emptyList()
+        )
+        verify(taskbarViewController, times(1)).commitRunningAppsToUI()
+
+        // Call onRecentTasksChanged() again with a new running app, verify we update UI.
+        prepareHotseatAndRunningAndRecentApps(
+            hotseatPackages = hotseatPackages,
+            runningTasks = newTasks,
+            recentTaskPackages = emptyList()
+        )
+
+        verify(taskbarViewController, times(2)).commitRunningAppsToUI()
+    }
+
     private fun prepareHotseatAndRunningAndRecentApps(
         hotseatPackages: List<String>,
-        runningTaskPackages: List<String>,
-        minimizedTaskIndices: Set<Int> = emptySet(),
+        runningTasks: List<Task>,
         recentTaskPackages: List<String>,
     ): Array<ItemInfo?> {
         val hotseatItems = createHotseatItemsFromPackageNames(hotseatPackages)
-        val newHotseatItems =
-            recentAppsController.updateHotseatItemInfos(hotseatItems.toTypedArray())
-        val runningTasks = createDesktopTask(runningTaskPackages, minimizedTaskIndices)
+        recentAppsController.updateHotseatItemInfos(hotseatItems.toTypedArray())
+        updateRecentTasks(runningTasks, recentTaskPackages)
+        return recentAppsController.shownHotseatItems.toTypedArray()
+    }
+
+    private fun updateRecentTasks(
+        runningTasks: List<Task>,
+        recentTaskPackages: List<String>,
+    ) {
         val recentTasks = createRecentTasksFromPackageNames(recentTaskPackages)
         val allTasks =
             ArrayList<GroupTask>().apply {
-                if (runningTasks != null) {
-                    add(runningTasks)
+                if (!runningTasks.isEmpty()) {
+                    add(DesktopTask(ArrayList(runningTasks)))
                 }
                 addAll(recentTasks)
             }
@@ -504,20 +706,21 @@
             .whenever(mockRecentsModel)
             .getTasks(any<Consumer<List<GroupTask>>>())
         recentTasksChangedListener.onRecentTasksChanged()
-        return newHotseatItems
     }
 
     private fun createHotseatItemsFromPackageNames(packageNames: List<String>): List<ItemInfo> {
-        return packageNames.map {
-            createTestAppInfo(packageName = it).apply {
-                container =
-                    if (it.startsWith("predicted")) {
-                        CONTAINER_HOTSEAT_PREDICTION
-                    } else {
-                        CONTAINER_HOTSEAT
-                    }
+        return packageNames
+            .map {
+                createTestAppInfo(packageName = it).apply {
+                    container =
+                        if (it.startsWith("predicted")) {
+                            CONTAINER_HOTSEAT_PREDICTION
+                        } else {
+                            CONTAINER_HOTSEAT
+                        }
+                }
             }
-        }
+            .map { it.makeWorkspaceItem(taskbarActivityContext) }
     }
 
     private fun createTestAppInfo(
@@ -525,40 +728,27 @@
         className: String = "testClassName"
     ) = AppInfo(ComponentName(packageName, className), className /* title */, userHandle, Intent())
 
-    private fun createDesktopTask(
-        packageNames: List<String>,
-        minimizedTaskIndices: Set<Int>
-    ): DesktopTask? {
-        if (packageNames.isEmpty()) return null
-
-        return DesktopTask(
-            ArrayList(
-                packageNames.mapIndexed { index, packageName ->
-                    createTask(packageName, index !in minimizedTaskIndices)
-                }
-            )
-        )
-    }
-
     private fun createRecentTasksFromPackageNames(packageNames: List<String>): List<GroupTask> {
-        return packageNames.map {
-            if (it.startsWith("split")) {
-                val splitPackages = it.split("_")
+        return packageNames.map { packageName ->
+            if (packageName.startsWith("split")) {
+                val splitPackages = packageName.split("_")
                 GroupTask(
-                    createTask(splitPackages[0]),
-                    createTask(splitPackages[1]),
+                    createTask(100, splitPackages[0]),
+                    createTask(101, splitPackages[1]),
                     /* splitBounds = */ null
                 )
             } else {
-                GroupTask(createTask(it))
+                // Use the number at the end of the test packageName as the id.
+                val id = 1000 + packageName[packageName.length - 1].code
+                GroupTask(createTask(id, packageName))
             }
         }
     }
 
-    private fun createTask(packageName: String, isVisible: Boolean = true): Task {
+    private fun createTask(id: Int, packageName: String, isVisible: Boolean = true): Task {
         return Task(
                 Task.TaskKey(
-                    nextTaskId++,
+                    id,
                     WINDOWING_MODE_FREEFORM,
                     Intent().apply { `package` = packageName },
                     ComponentName(packageName, "TestActivity"),
diff --git a/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt b/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt
index 50b5df1..d9d5585 100644
--- a/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt
+++ b/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt
@@ -32,6 +32,7 @@
 import com.android.launcher3.util.TransformingTouchDelegate
 import com.android.quickstep.TaskOverlayFactory.TaskOverlay
 import com.android.quickstep.views.LauncherRecentsView
+import com.android.quickstep.views.TaskContainer
 import com.android.quickstep.views.TaskThumbnailViewDeprecated
 import com.android.quickstep.views.TaskView
 import com.android.quickstep.views.TaskViewIcon
@@ -39,7 +40,7 @@
 import com.android.systemui.shared.recents.model.Task.TaskKey
 import com.android.window.flags.Flags
 import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource
-import com.android.wm.shell.shared.DesktopModeStatus
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
 import com.google.common.truth.Truth.assertThat
 import org.junit.After
 import org.junit.Before
@@ -186,10 +187,10 @@
         }
     }
 
-    private fun createTaskContainer(task: Task): TaskView.TaskContainer {
-        return taskView.TaskContainer(
+    private fun createTaskContainer(task: Task): TaskContainer {
+        return TaskContainer(
+            taskView,
             task,
-            thumbnailView = null,
             thumbnailViewDeprecated,
             iconView,
             transformingTouchDelegate,
diff --git a/quickstep/tests/src/com/android/quickstep/OrientationTouchTransformerTest.java b/quickstep/tests/src/com/android/quickstep/OrientationTouchTransformerTest.java
index 298dd6c..f5d082d 100644
--- a/quickstep/tests/src/com/android/quickstep/OrientationTouchTransformerTest.java
+++ b/quickstep/tests/src/com/android/quickstep/OrientationTouchTransformerTest.java
@@ -21,6 +21,7 @@
 
 import static com.android.launcher3.util.NavigationMode.NO_BUTTON;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
@@ -288,6 +289,34 @@
         assertTrue(mTouchTransformer.touchInValidSwipeRegions(inRegion2.getX(), inRegion2.getY()));
     }
 
+    @Test
+    public void testSimpleOrientationTouchTransformer() {
+        final DisplayController displayController = mock(DisplayController.class);
+        doReturn(mInfo).when(displayController).getInfo();
+        final SimpleOrientationTouchTransformer transformer =
+                new SimpleOrientationTouchTransformer(getApplicationContext(), displayController);
+        final MotionEvent move1 = generateMotionEvent(MotionEvent.ACTION_MOVE, 100, 10);
+        transformer.transform(move1, Surface.ROTATION_90);
+        // The position is transformed to 90 degree.
+        assertEquals(10, move1.getX(), 0f /* delta */);
+        assertEquals(NORMAL_SCREEN_SIZE.getWidth() - 100, move1.getY(), 0f /* delta */);
+
+        // If the touching state is specified, the position is still transformed to 90 degree even
+        // if the given rotation is changed.
+        final MotionEvent move2 = generateMotionEvent(MotionEvent.ACTION_MOVE, 100, 10);
+        transformer.updateTouchingOrientation(Surface.ROTATION_90);
+        transformer.transform(move2, Surface.ROTATION_0);
+        assertEquals(move1.getX(), move2.getX(), 0f /* delta */);
+        assertEquals(move1.getY(), move2.getY(), 0f /* delta */);
+
+        // If the touching state is cleared, it restores to use the given rotation.
+        final MotionEvent move3 = generateMotionEvent(MotionEvent.ACTION_MOVE, 100, 10);
+        transformer.clearTouchingOrientation();
+        transformer.transform(move3, Surface.ROTATION_0);
+        assertEquals(100, move3.getX(), 0f /* delta */);
+        assertEquals(10, move3.getY(), 0f /* delta */);
+    }
+
     private DisplayController.Info createDisplayInfo(Size screenSize, int rotation) {
         Point displaySize = new Point(screenSize.getWidth(), screenSize.getHeight());
         RotationUtils.rotateSize(displaySize, rotation);
diff --git a/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java b/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java
index ce16b70..5d00255 100644
--- a/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java
+++ b/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java
@@ -32,7 +32,7 @@
 
 import com.android.launcher3.util.LooperExecutor;
 import com.android.quickstep.util.GroupTask;
-import com.android.quickstep.views.TaskView;
+import com.android.quickstep.views.TaskViewType;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.wm.shell.util.GroupedRecentTaskInfo;
 
@@ -125,7 +125,7 @@
                 Integer.MAX_VALUE /* numTasks */, -1 /* requestId */, false /* loadKeysOnly */);
 
         assertEquals(1, taskList.size());
-        assertEquals(TaskView.Type.DESKTOP, taskList.get(0).taskViewType);
+        assertEquals(TaskViewType.DESKTOP, taskList.get(0).taskViewType);
         List<Task> actualFreeformTasks = taskList.get(0).getTasks();
         assertEquals(3, actualFreeformTasks.size());
         assertEquals(1, actualFreeformTasks.get(0).key.id);
diff --git a/quickstep/tests/src/com/android/quickstep/TaplDigitalWellBeingToastTest.java b/quickstep/tests/src/com/android/quickstep/TaplDigitalWellBeingToastTest.java
index 07d8f61..6e25b10 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplDigitalWellBeingToastTest.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplDigitalWellBeingToastTest.java
@@ -31,6 +31,7 @@
 import com.android.launcher3.Launcher;
 import com.android.quickstep.views.DigitalWellBeingToast;
 import com.android.quickstep.views.RecentsView;
+import com.android.quickstep.views.TaskContainer;
 import com.android.quickstep.views.TaskView;
 
 import org.junit.Test;
@@ -86,7 +87,7 @@
         final TaskView task = getOnceNotNull("No latest task", launcher -> getLatestTask(launcher));
 
         return getFromLauncher(launcher -> {
-            TaskView.TaskContainer taskContainer = task.getTaskContainers().get(0);
+            TaskContainer taskContainer = task.getTaskContainers().get(0);
             assertTrue("Latest task is not Calculator", CALCULATOR_PACKAGE.equals(
                     taskContainer.getTask().getTopComponent().getPackageName()));
             return taskContainer.getDigitalWellBeingToast();
diff --git a/res/anim-v33/shared_x_axis_activity_close_enter.xml b/res/anim-v33/shared_x_axis_activity_close_enter.xml
deleted file mode 100644
index 3d7ad2b..0000000
--- a/res/anim-v33/shared_x_axis_activity_close_enter.xml
+++ /dev/null
@@ -1,42 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2022 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.
-  -->
-
-<set xmlns:android="http://schemas.android.com/apk/res/android"
-    android:shareInterpolator="false"
-    android:showBackdrop="true">
-
-    <alpha
-        android:fromAlpha="0.0"
-        android:toAlpha="1.0"
-        android:fillEnabled="true"
-        android:fillBefore="true"
-        android:fillAfter="true"
-        android:interpolator="@interpolator/standard_decelerate_interpolator"
-        android:startOffset="100"
-        android:duration="350" />
-
-    <translate
-        android:fromXDelta="-25%"
-        android:toXDelta="0"
-        android:fillEnabled="true"
-        android:fillBefore="true"
-        android:fillAfter="true"
-        android:interpolator="@interpolator/emphasized_interpolator"
-        android:startOffset="0"
-        android:duration="450" />
-
-</set>
\ No newline at end of file
diff --git a/res/anim-v33/shared_x_axis_activity_close_exit.xml b/res/anim-v33/shared_x_axis_activity_close_exit.xml
deleted file mode 100644
index fb63602..0000000
--- a/res/anim-v33/shared_x_axis_activity_close_exit.xml
+++ /dev/null
@@ -1,41 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2022 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.
-  -->
-
-<set xmlns:android="http://schemas.android.com/apk/res/android"
-    android:shareInterpolator="false">
-
-    <alpha
-        android:fromAlpha="1.0"
-        android:toAlpha="0.0"
-        android:fillEnabled="true"
-        android:fillBefore="true"
-        android:fillAfter="true"
-        android:interpolator="@interpolator/standard_accelerate_interpolator"
-        android:startOffset="0"
-        android:duration="100" />
-
-    <translate
-        android:fromXDelta="0"
-        android:toXDelta="25%"
-        android:fillEnabled="true"
-        android:fillBefore="true"
-        android:fillAfter="true"
-        android:interpolator="@interpolator/emphasized_interpolator"
-        android:startOffset="0"
-        android:duration="450" />
-
-</set>
\ No newline at end of file
diff --git a/res/anim-v33/shared_x_axis_activity_open_enter.xml b/res/anim-v33/shared_x_axis_activity_open_enter.xml
deleted file mode 100644
index cba74ba..0000000
--- a/res/anim-v33/shared_x_axis_activity_open_enter.xml
+++ /dev/null
@@ -1,42 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2022 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.
-  -->
-
-<set xmlns:android="http://schemas.android.com/apk/res/android"
-    android:shareInterpolator="false"
-    android:showBackdrop="true">
-
-    <alpha
-        android:fromAlpha="0.0"
-        android:toAlpha="1.0"
-        android:fillEnabled="true"
-        android:fillBefore="true"
-        android:fillAfter="true"
-        android:interpolator="@interpolator/standard_decelerate_interpolator"
-        android:startOffset="100"
-        android:duration="350" />
-
-    <translate
-        android:fromXDelta="25%"
-        android:toXDelta="0"
-        android:fillEnabled="true"
-        android:fillBefore="true"
-        android:fillAfter="true"
-        android:interpolator="@interpolator/emphasized_interpolator"
-        android:startOffset="0"
-        android:duration="450" />
-
-</set>
\ No newline at end of file
diff --git a/res/anim-v33/shared_x_axis_activity_open_exit.xml b/res/anim-v33/shared_x_axis_activity_open_exit.xml
deleted file mode 100644
index 22e878d..0000000
--- a/res/anim-v33/shared_x_axis_activity_open_exit.xml
+++ /dev/null
@@ -1,41 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2022 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.
-  -->
-
-<set xmlns:android="http://schemas.android.com/apk/res/android"
-    android:shareInterpolator="false">
-
-    <alpha
-        android:fromAlpha="1.0"
-        android:toAlpha="0.0"
-        android:fillEnabled="true"
-        android:fillBefore="true"
-        android:fillAfter="true"
-        android:interpolator="@interpolator/standard_accelerate_interpolator"
-        android:startOffset="0"
-        android:duration="100" />
-
-    <translate
-        android:fromXDelta="0"
-        android:toXDelta="-25%"
-        android:fillEnabled="true"
-        android:fillBefore="true"
-        android:fillAfter="true"
-        android:interpolator="@interpolator/emphasized_interpolator"
-        android:startOffset="0"
-        android:duration="450" />
-
-</set>
\ No newline at end of file
diff --git a/res/drawable/ic_close_work_edu.xml b/res/drawable/ic_close_work_edu.xml
new file mode 100644
index 0000000..f336eea
--- /dev/null
+++ b/res/drawable/ic_close_work_edu.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ 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.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="960"
+    android:viewportHeight="960">
+    <path
+        android:fillColor="@color/material_color_on_surface"
+        android:pathData="M256,760L200,704L424,480L200,256L256,200L480,424L704,200L760,256L536,480L760,704L704,760L480,536L256,760Z"/>
+</vector>
diff --git a/res/drawable/rounded_action_button.xml b/res/drawable/rounded_action_button.xml
index 81e94f7..e283d3f 100644
--- a/res/drawable/rounded_action_button.xml
+++ b/res/drawable/rounded_action_button.xml
@@ -18,11 +18,11 @@
 <shape xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
     android:shape="rectangle">
-    <solid android:color="?androidprv:attr/colorSurfaceVariant" />
+    <solid android:color="@color/material_color_surface_container_low" />
     <corners android:radius="@dimen/rounded_button_radius" />
     <stroke
         android:width="1dp"
-        android:color="?androidprv:attr/colorSurfaceVariant" />
+        android:color="@color/material_color_surface_container_low" />
     <padding
         android:left="@dimen/rounded_button_padding"
         android:right="@dimen/rounded_button_padding" />
diff --git a/res/drawable/work_card.xml b/res/drawable/work_card.xml
index 4a66cac..9bf2b8d 100644
--- a/res/drawable/work_card.xml
+++ b/res/drawable/work_card.xml
@@ -16,9 +16,8 @@
 
 
 <shape xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
     android:shape="rectangle">
-    <solid android:color="?androidprv:attr/colorSurface" />
+    <solid android:color="@color/material_color_surface_container_highest" />
     <corners android:radius="@dimen/work_edu_card_radius" />
 </shape>
 
diff --git a/res/layout/widgets_two_pane_sheet.xml b/res/layout/widgets_two_pane_sheet.xml
index bb2b7bd..ce5eed9 100644
--- a/res/layout/widgets_two_pane_sheet.xml
+++ b/res/layout/widgets_two_pane_sheet.xml
@@ -48,20 +48,23 @@
             android:textSize="24sp" />
 
         <TextView
-            android:id="@+id/no_widgets_text"
-            style="@style/PrimaryHeadline"
+            android:id="@+id/widget_picker_description"
             android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:gravity="center"
-            android:textSize="18sp"
+            android:layout_height="wrap_content"
+            android:gravity="center_horizontal"
+            android:layout_below="@id/title"
+            android:maxLines="1"
+            android:paddingHorizontal="@dimen/widget_list_horizontal_margin_two_pane"
+            android:textColor="?attr/widgetPickerDescriptionColor"
             android:visibility="gone"
-            tools:text="@string/no_widgets_available" />
+            android:lineHeight="20sp"
+            android:textSize="14sp" />
 
         <LinearLayout
             android:id="@+id/linear_layout_container"
             android:layout_width="match_parent"
             android:layout_height="match_parent"
-            android:layout_below="@id/title">
+            android:layout_below="@id/widget_picker_description">
 
             <FrameLayout
                 android:id="@+id/recycler_view_container"
@@ -124,6 +127,16 @@
                             android:background="@drawable/widgets_surface_background"
                             android:importantForAccessibility="yes"
                             android:id="@+id/right_pane">
+                            <TextView
+                                android:id="@+id/no_widgets_text"
+                                style="@style/PrimaryHeadline"
+                                android:layout_width="match_parent"
+                                android:layout_height="match_parent"
+                                android:gravity="center"
+                                android:textSize="18sp"
+                                android:visibility="gone"
+                                tools:text="@string/no_widgets_available" />
+
                             <!-- Shown when there are recommendations to display -->
                             <LinearLayout
                                 android:id="@+id/widget_recommendations_container"
diff --git a/res/layout/work_apps_edu.xml b/res/layout/work_apps_edu.xml
index 99db8c6..c581ae3 100644
--- a/res/layout/work_apps_edu.xml
+++ b/res/layout/work_apps_edu.xml
@@ -54,7 +54,7 @@
                 android:layout_gravity="center"
                 android:contentDescription="@string/accessibility_close"
                 android:background="@android:color/transparent"
-                android:src="@drawable/ic_remove_no_shadow" />
+                android:src="@drawable/ic_close_work_edu" />
         </FrameLayout>
     </LinearLayout>
 </com.android.launcher3.allapps.WorkEduCard>
diff --git a/res/values-af/strings.xml b/res/values-af/strings.xml
index 90e784d..1a8f8e2 100644
--- a/res/values-af/strings.xml
+++ b/res/values-af/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Legstukke gedeaktiveer in Veiligmodus"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Kortpad is nie beskikbaar nie"</string>
     <string name="home_screen" msgid="5629429142036709174">"Tuis"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Stel <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> as verstektuisskermapp in Instellings"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Verdeelde skerm"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Programinligting vir %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Gebruikinstellings vir %1$s"</string>
diff --git a/res/values-am/strings.xml b/res/values-am/strings.xml
index 1289b8c..80447c5 100644
--- a/res/values-am/strings.xml
+++ b/res/values-am/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"ምግብሮች በደህንነቱ የተጠበቀ ሁኔታ ተሰናክለዋል"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"አቋራጭ አይገኝም"</string>
     <string name="home_screen" msgid="5629429142036709174">"መነሻ"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"በቅንብሮች ውስጥ <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g>ን እንደ ነባሪ የHome መተግበሪያ ያቀናብሩ"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"የተከፈለ ማያ ገፅ"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"የመተግበሪያ መረጃ ለ%1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"የ%1$s የአጠቃቀም ቅንብሮች"</string>
diff --git a/res/values-ar/strings.xml b/res/values-ar/strings.xml
index 37d6fba..4eee121 100644
--- a/res/values-ar/strings.xml
+++ b/res/values-ar/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"الأدوات غير مفعّلة في الوضع الآمن"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"الاختصار غير متاح"</string>
     <string name="home_screen" msgid="5629429142036709174">"الشاشة الرئيسية"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"يمكن ضبط \"<xliff:g id="LAUNCHER_NAME">%1$s</xliff:g>\" كتطبيق الشاشة الرئيسية التلقائي من خلال \"الإعدادات\""</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"تقسيم الشاشة"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"‏معلومات تطبيق %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"‏إعدادات استخدام \"%1$s\""</string>
diff --git a/res/values-as/strings.xml b/res/values-as/strings.xml
index e983ce8..52ec7ea 100644
--- a/res/values-as/strings.xml
+++ b/res/values-as/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"ৱিজেটবোৰক সুৰক্ষিত ম\'ডত অক্ষম কৰা হ’ল"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"শ্বৰ্টকাট নাই"</string>
     <string name="home_screen" msgid="5629429142036709174">"গৃহ স্ক্ৰীন"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"ছেটিঙত <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g>ক ডিফ’ল্ট গৃহপৃষ্ঠা এপ্‌ হিচাপে ছেট কৰক"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"বিভাজিত স্ক্ৰীন"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$sৰ বাবে এপৰ তথ্য"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"%1$sৰ বাবে ব্যৱহাৰৰ ছেটিং"</string>
diff --git a/res/values-az/strings.xml b/res/values-az/strings.xml
index f390c7c..6c1cc46 100644
--- a/res/values-az/strings.xml
+++ b/res/values-az/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Vidcetlər Güvənli rejimdə deaktiv edilib"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Qısayol əlçatan deyil"</string>
     <string name="home_screen" msgid="5629429142036709174">"Əsas səhifə"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Ayarlarda <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> tətbiqini defolt əsas ekran tətbiqi kimi ayarlayın"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Ekran bölünməsi"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$s ilə bağlı tətbiq məlumatı"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"%1$s üzrə istifadə ayarları"</string>
diff --git a/res/values-b+sr+Latn/strings.xml b/res/values-b+sr+Latn/strings.xml
index fc71eeb..24328cf 100644
--- a/res/values-b+sr+Latn/strings.xml
+++ b/res/values-b+sr+Latn/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Vidžeti su onemogućeni u Bezbednom režimu"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Prečica nije dostupna"</string>
     <string name="home_screen" msgid="5629429142036709174">"Početni ekran"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Podesite <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> kao podrazumevanu početnu aplikaciju u Podešavanjima"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Podeljeni ekran"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Informacije o aplikaciji za: %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Podešavanja potrošnje za %1$s"</string>
diff --git a/res/values-be/strings.xml b/res/values-be/strings.xml
index 4589125..ebbb378 100644
--- a/res/values-be/strings.xml
+++ b/res/values-be/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Віджэты адключаны ў Бяспечным рэжыме"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Ярлык недаступны"</string>
     <string name="home_screen" msgid="5629429142036709174">"Галоўны экран"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Зрабіць <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> стандартнай праграмай для галоўнага экрана, перайшоўшы ў Налады"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Падзелены экран"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Інфармацыя пра праграму для: %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"%1$s: налады выкарыстання"</string>
diff --git a/res/values-bg/strings.xml b/res/values-bg/strings.xml
index 643c24e..d5d948e 100644
--- a/res/values-bg/strings.xml
+++ b/res/values-bg/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Приспособленията са деактивирани в безопасния режим"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Няма достъп до прекия път"</string>
     <string name="home_screen" msgid="5629429142036709174">"Начален екран"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"От настройките задайте <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> като основното приложение за начален екран"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Разделен екран"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Информация за приложението за %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Настройки за използването на %1$s"</string>
diff --git a/res/values-bn/strings.xml b/res/values-bn/strings.xml
index 7e10a0e..cf75fb58 100644
--- a/res/values-bn/strings.xml
+++ b/res/values-bn/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"সুরক্ষিত মোডে উইজেট নিষ্ক্রিয় থাকে"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"শর্টকাটগুলি অনুপলব্ধ"</string>
     <string name="home_screen" msgid="5629429142036709174">"হোম"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"সেটিংসে গিয়ে <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> অ্যাপকে ডিফল্ট হোম অ্যাপ হিসেবে সেট করুন"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"স্প্লিট স্ক্রিন"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$s-এর জন্য অ্যাপ সম্পর্কিত তথ্য"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"%1$s-এর জন্য ব্যবহারের সেটিংস"</string>
diff --git a/res/values-bs/strings.xml b/res/values-bs/strings.xml
index 10aa86a..1758c39 100644
--- a/res/values-bs/strings.xml
+++ b/res/values-bs/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Vidžeti su onemogućeni u sigurnom načinu rada."</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Prečica nije dostupna"</string>
     <string name="home_screen" msgid="5629429142036709174">"Početni ekran"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Postavite <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> kao zadanu aplikaciju za početni ekran u Postavkama"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Podijeljeni ekran"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Informacije o aplikaciji %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Postavke korištenja za: %1$s"</string>
diff --git a/res/values-ca/strings.xml b/res/values-ca/strings.xml
index 3d2ff4a..bf578f5 100644
--- a/res/values-ca/strings.xml
+++ b/res/values-ca/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"En Mode segur, els widgets estan desactivats."</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"La drecera no està disponible"</string>
     <string name="home_screen" msgid="5629429142036709174">"Inici"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Defineix <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> com a aplicació d\'inici predeterminada a Configuració"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Pantalla dividida"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Informació de l\'aplicació %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Configuració d\'ús de %1$s"</string>
diff --git a/res/values-cs/strings.xml b/res/values-cs/strings.xml
index 14a3583..9a8fc6f 100644
--- a/res/values-cs/strings.xml
+++ b/res/values-cs/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"V nouzovém režimu jsou widgety zakázány."</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Zkratka není k dispozici"</string>
     <string name="home_screen" msgid="5629429142036709174">"Domů"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Nastavit <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> jako výchozí vstupní aplikaci v Nastavení"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Rozdělit obrazovku"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Informace o aplikaci %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Nastavení využití pro aplikaci %1$s"</string>
diff --git a/res/values-da/strings.xml b/res/values-da/strings.xml
index f18e1e8..208c2ef 100644
--- a/res/values-da/strings.xml
+++ b/res/values-da/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Widgets er deaktiveret i Beskyttet tilstand"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Genvejen er ikke tilgængelig"</string>
     <string name="home_screen" msgid="5629429142036709174">"Startskærm"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Angiv <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> som standardstartapp i Indstillinger"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Opdel skærm"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Appinfo for %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Indstillinger for brug af %1$s"</string>
diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml
index 9a4e5e2..380030b 100644
--- a/res/values-de/strings.xml
+++ b/res/values-de/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Widgets im abgesicherten Modus deaktiviert"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Verknüpfung nicht verfügbar"</string>
     <string name="home_screen" msgid="5629429142036709174">"Startbildschirm"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"<xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> in den Einstellungen als Stand-Start-App festlegen"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Splitscreen"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"App-Info für %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Nutzungseinstellungen für %1$s"</string>
@@ -37,7 +36,7 @@
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Dieses App-Paar wird auf diesem Gerät nicht unterstützt"</string>
     <string name="app_pair_needs_unfold" msgid="4588897528143807002">"Gerät aufklappen, um dieses App-Paar zu verwenden"</string>
     <string name="app_pair_not_available" msgid="3556767440808032031">"App-Paar nicht verfügbar"</string>
-    <string name="long_press_widget_to_add" msgid="3587712543577675817">"Zum Verschieben des Widgets berühren und halten"</string>
+    <string name="long_press_widget_to_add" msgid="3587712543577675817">"Zum Verschieben des Widgets gedrückt halten"</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Doppeltippen und halten, um ein Widget zu bewegen oder benutzerdefinierte Aktionen zu nutzen."</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d breit und %2$d hoch"</string>
@@ -75,7 +74,7 @@
     <string name="label_application" msgid="8531721983832654978">"App"</string>
     <string name="all_apps_label" msgid="5015784846527570951">"Alle Apps"</string>
     <string name="notifications_header" msgid="1404149926117359025">"Benachrichtigungen"</string>
-    <string name="long_press_shortcut_to_add" msgid="5405328730817637737">"Zum Verschieben einer Verknüpfung berühren und halten"</string>
+    <string name="long_press_shortcut_to_add" msgid="5405328730817637737">"Zum Verschieben einer Verknüpfung gedrückt halten"</string>
     <string name="long_accessible_way_to_add_shortcut" msgid="2199537273817090740">"Doppeltippen und halten, um eine Verknüpfung zu bewegen oder benutzerdefinierte Aktionen zu nutzen."</string>
     <string name="out_of_space" msgid="6455557115204099579">"Auf diesem Startbildschirm ist kein Platz mehr vorhanden"</string>
     <string name="hotseat_out_of_space" msgid="7448809638125333693">"Ablage \"Favoriten\" ist voll."</string>
diff --git a/res/values-el/strings.xml b/res/values-el/strings.xml
index 840ff5b..e86ebae 100644
--- a/res/values-el/strings.xml
+++ b/res/values-el/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Τα γραφικά στοιχεία απενεργοποιήθηκαν στην ασφαλή λειτουργία"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Η συντόμευση δεν είναι διαθέσιμη"</string>
     <string name="home_screen" msgid="5629429142036709174">"Αρχική οθόνη"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Ορίστε το <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> ως την προεπιλεγμένη εφαρμογή αρχικής οθόνης στις Ρυθμίσεις"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Διαχωρισμός οθόνης"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Πληροφορίες εφαρμογής για %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Ρυθμίσεις χρήσης για %1$s"</string>
diff --git a/res/values-en-rAU/strings.xml b/res/values-en-rAU/strings.xml
index 4deacb6..50c5976 100644
--- a/res/values-en-rAU/strings.xml
+++ b/res/values-en-rAU/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Widgets disabled in Safe mode"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Shortcut isn\'t available"</string>
     <string name="home_screen" msgid="5629429142036709174">"Home"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Set <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> as the default home app in Settings"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Split screen"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"App info for %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Usage settings for %1$s"</string>
diff --git a/res/values-en-rGB/strings.xml b/res/values-en-rGB/strings.xml
index 4deacb6..50c5976 100644
--- a/res/values-en-rGB/strings.xml
+++ b/res/values-en-rGB/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Widgets disabled in Safe mode"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Shortcut isn\'t available"</string>
     <string name="home_screen" msgid="5629429142036709174">"Home"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Set <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> as the default home app in Settings"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Split screen"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"App info for %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Usage settings for %1$s"</string>
diff --git a/res/values-en-rIN/strings.xml b/res/values-en-rIN/strings.xml
index 4deacb6..50c5976 100644
--- a/res/values-en-rIN/strings.xml
+++ b/res/values-en-rIN/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Widgets disabled in Safe mode"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Shortcut isn\'t available"</string>
     <string name="home_screen" msgid="5629429142036709174">"Home"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Set <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> as the default home app in Settings"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Split screen"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"App info for %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Usage settings for %1$s"</string>
diff --git a/res/values-es-rUS/strings.xml b/res/values-es-rUS/strings.xml
index e734744..5879129 100644
--- a/res/values-es-rUS/strings.xml
+++ b/res/values-es-rUS/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Widgets inhabilitados en modo seguro"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"El acceso directo no está disponible"</string>
     <string name="home_screen" msgid="5629429142036709174">"Pantalla principal"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Establece <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> como la app de inicio predeterminada en Configuración"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Pantalla dividida"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Información de la app de %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Configuración del uso de %1$s"</string>
diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml
index 3e0e516..8d384ac 100644
--- a/res/values-es/strings.xml
+++ b/res/values-es/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Widgets inhabilitados en modo Seguro"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Acceso directo no disponible"</string>
     <string name="home_screen" msgid="5629429142036709174">"Inicio"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Define <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> como aplicación de inicio predeterminada en Ajustes"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Pantalla dividida"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Información de la aplicación %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Ajustes de uso para %1$s"</string>
diff --git a/res/values-et/strings.xml b/res/values-et/strings.xml
index bab7da3..44448a6 100644
--- a/res/values-et/strings.xml
+++ b/res/values-et/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Turvarežiimis on vidinad keelatud"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Otsetee pole saadaval"</string>
     <string name="home_screen" msgid="5629429142036709174">"Avakuva"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Määrake rakendus <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> seadetes avakuva vaikerakenduseks"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Jagatud ekraanikuva"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Rakenduse teave: %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Kasutuse seaded: %1$s"</string>
diff --git a/res/values-eu/strings.xml b/res/values-eu/strings.xml
index b8017d6..6fc4cf4 100644
--- a/res/values-eu/strings.xml
+++ b/res/values-eu/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Widgetak desgaitu egin dira modu seguruan"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Lasterbideak ez daude erabilgarri"</string>
     <string name="home_screen" msgid="5629429142036709174">"Orri nagusia"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Ezarri <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> hasierako aplikazio lehenetsi gisa ezarpenetan"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Pantaila zatitzea"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$s aplikazioari buruzko informazioa"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"%1$s aplikazioaren erabilera-ezarpenak"</string>
diff --git a/res/values-fa/strings.xml b/res/values-fa/strings.xml
index 27ce075..fffce13 100644
--- a/res/values-fa/strings.xml
+++ b/res/values-fa/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"ابزارک‌ها در حالت ایمن غیرفعال هستند"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"میان‌بر دردسترس نیست"</string>
     <string name="home_screen" msgid="5629429142036709174">"صفحه اصلی"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"تنظیم <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> به‌عنوان برنامه صفحه اصلی پیش‌فرض در «تنظیمات»"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"صفحهٔ دونیمه"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"‏اطلاعات برنامه %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"‏تنظیمات مصرف برای %1$s"</string>
@@ -38,7 +37,7 @@
     <string name="app_pair_needs_unfold" msgid="4588897528143807002">"برای استفاده از این جفت برنامه، دستگاه را باز کنید"</string>
     <string name="app_pair_not_available" msgid="3556767440808032031">"جفت برنامه دردسترس نیست"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"برای جابه‌جا کردن ابزارک، لمس کنید و نگه دارید."</string>
-    <string name="long_accessible_way_to_add" msgid="2733588281439571974">"برای جابه‌جا کردن ابزارک یا استفاده از کنش‌های سفارشی، دوضربه بزنید و نگه دارید."</string>
+    <string name="long_accessible_way_to_add" msgid="2733588281439571974">"برای جابه‌جا کردن ابزارک یا استفاده از کنش‌های سفارشی، دو تک‌ضرب بزنید و نگه دارید."</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <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>
@@ -76,7 +75,7 @@
     <string name="all_apps_label" msgid="5015784846527570951">"همه برنامه‌ها"</string>
     <string name="notifications_header" msgid="1404149926117359025">"اعلان‌ها"</string>
     <string name="long_press_shortcut_to_add" msgid="5405328730817637737">"برای جابه‌جا کردن میان‌بر، لمس کنید و نگه دارید."</string>
-    <string name="long_accessible_way_to_add_shortcut" msgid="2199537273817090740">"برای جابه‌جا کردن میان‌بر یا استفاده از کنش‌های سفارشی، دوضربه بزنید و نگه دارید."</string>
+    <string name="long_accessible_way_to_add_shortcut" msgid="2199537273817090740">"برای جابه‌جا کردن میان‌بر یا استفاده از کنش‌های سفارشی، دو تک‌ضرب بزنید و نگه دارید."</string>
     <string name="out_of_space" msgid="6455557115204099579">"فضای خالی در این صفحه اصلی وجود ندارد"</string>
     <string name="hotseat_out_of_space" msgid="7448809638125333693">"فضای بیشتری در سینی موارد دلخواه وجود ندارد"</string>
     <string name="all_apps_button_label" msgid="8130441508702294465">"فهرست برنامه‌ها"</string>
diff --git a/res/values-fi/strings.xml b/res/values-fi/strings.xml
index 181c89e..310053e 100644
--- a/res/values-fi/strings.xml
+++ b/res/values-fi/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Widgetit poistettu käytöstä vikasietotilassa"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Pikakuvake ei ole käytettävissä."</string>
     <string name="home_screen" msgid="5629429142036709174">"Etusivu"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Valitse <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> oletusaloitusnäyttösovellukseksi asetuksissa"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Jaettu näyttö"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Sovellustiedot: %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Käyttöasetus tälle: %1$s"</string>
diff --git a/res/values-fr-rCA/strings.xml b/res/values-fr-rCA/strings.xml
index f3b08cb..11b2c01 100644
--- a/res/values-fr-rCA/strings.xml
+++ b/res/values-fr-rCA/strings.xml
@@ -21,22 +21,21 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Lanceur3"</string>
     <string name="work_folder_name" msgid="3753320833950115786">"Travail"</string>
-    <string name="activity_not_found" msgid="8071924732094499514">"L\'application n\'est pas installée."</string>
-    <string name="activity_not_available" msgid="7456344436509528827">"Application indisponible"</string>
-    <string name="safemode_shortcut_error" msgid="9160126848219158407">"L\'application téléchargée est désactivée en mode sans échec."</string>
+    <string name="activity_not_found" msgid="8071924732094499514">"L\'appli n\'est pas installée."</string>
+    <string name="activity_not_available" msgid="7456344436509528827">"Appli indisponible"</string>
+    <string name="safemode_shortcut_error" msgid="9160126848219158407">"L\'appli téléchargée est désactivée en mode sans échec."</string>
     <string name="safemode_widget_error" msgid="4863470563535682004">"Widgets désactivés en mode sans échec"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Le raccourci n\'est pas disponible"</string>
     <string name="home_screen" msgid="5629429142036709174">"Accueil"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Définissez <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> comme appli d\'accueil par défaut dans les paramètres"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Écran divisé"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Renseignements sur l\'appli pour %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Paramètres d\'utilisation pour %1$s"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Enr. paire d\'applis"</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">"Cette paire d\'applications n\'est pas prise en charge sur cet appareil"</string>
-    <string name="app_pair_needs_unfold" msgid="4588897528143807002">"Déplier l\'appareil pour utiliser cette paire d\'applications"</string>
-    <string name="app_pair_not_available" msgid="3556767440808032031">"La Paire d\'applications n\'est pas offerte"</string>
+    <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Cette paire d\'applis n\'est pas prise en charge sur cet appareil"</string>
+    <string name="app_pair_needs_unfold" msgid="4588897528143807002">"Déplier l\'appareil pour utiliser cette paire d\'applis"</string>
+    <string name="app_pair_not_available" msgid="3556767440808032031">"La Paire d\'applis n\'est pas offerte"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Maintenez le doigt sur un widget pour le déplacer."</string>
     <string name="long_accessible_way_to_add" msgid="2733588281439571974">"Touchez 2x un widget et maintenez le doigt dessus pour le déplacer ou utiliser des actions personnalisées."</string>
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
@@ -69,20 +68,20 @@
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Ajoutez le widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Touchez pour modifier les paramètres du widget"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Modifier les paramètres du widget"</string>
-    <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Rechercher dans les applications"</string>
-    <string name="all_apps_loading_message" msgid="5813968043155271636">"Chargement des applications en cours…"</string>
-    <string name="all_apps_no_search_results" msgid="3200346862396363786">"Aucune application trouvée correspondant à « <xliff:g id="QUERY">%1$s</xliff:g> »"</string>
-    <string name="label_application" msgid="8531721983832654978">"Application"</string>
-    <string name="all_apps_label" msgid="5015784846527570951">"Toutes les applications"</string>
+    <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Rechercher dans les applis"</string>
+    <string name="all_apps_loading_message" msgid="5813968043155271636">"Chargement des applis en cours…"</string>
+    <string name="all_apps_no_search_results" msgid="3200346862396363786">"Aucune appli trouvée correspondant à « <xliff:g id="QUERY">%1$s</xliff:g> »"</string>
+    <string name="label_application" msgid="8531721983832654978">"Appli"</string>
+    <string name="all_apps_label" msgid="5015784846527570951">"Toutes les applis"</string>
     <string name="notifications_header" msgid="1404149926117359025">"Notifications"</string>
     <string name="long_press_shortcut_to_add" msgid="5405328730817637737">"Maintenez le doigt sur un raccourci pour le déplacer."</string>
     <string name="long_accessible_way_to_add_shortcut" msgid="2199537273817090740">"Touchez deux fois un raccourci et maintenez le doigt dessus pour le déplacer ou utiliser des actions personnalisées."</string>
     <string name="out_of_space" msgid="6455557115204099579">"Pas d\'espace libre sur cet écran d\'accueil"</string>
     <string name="hotseat_out_of_space" msgid="7448809638125333693">"Il n\'y a plus d\'espace dans la zone des favoris"</string>
-    <string name="all_apps_button_label" msgid="8130441508702294465">"Liste des applications"</string>
+    <string name="all_apps_button_label" msgid="8130441508702294465">"Liste des applis"</string>
     <string name="all_apps_search_results" msgid="5889367432531296759">"Résultats de recherche"</string>
-    <string name="all_apps_button_personal_label" msgid="1315764287305224468">"Liste des applications personnelles"</string>
-    <string name="all_apps_button_work_label" msgid="7270707118948892488">"Liste des applications professionnelles"</string>
+    <string name="all_apps_button_personal_label" msgid="1315764287305224468">"Liste des applis personnelles"</string>
+    <string name="all_apps_button_work_label" msgid="7270707118948892488">"Liste des applis professionnelles"</string>
     <string name="remove_drop_target_label" msgid="7812859488053230776">"Supprimer"</string>
     <string name="uninstall_drop_target_label" msgid="4722034217958379417">"Désinstaller"</string>
     <string name="app_info_drop_target_label" msgid="692894985365717661">"Détails de l\'appli"</string>
@@ -92,17 +91,17 @@
     <string name="dismiss_prediction_label" msgid="3357562989568808658">"Ne pas suggérer d\'appli"</string>
     <string name="pin_prediction" msgid="4196423321649756498">"Épingler la prédiction"</string>
     <string name="permlab_install_shortcut" msgid="5632423390354674437">"installer des raccourcis"</string>
-    <string name="permdesc_install_shortcut" msgid="923466509822011139">"Permet à une application d\'ajouter des raccourcis sans l\'intervention de l\'utilisateur."</string>
+    <string name="permdesc_install_shortcut" msgid="923466509822011139">"Permet à une appli d\'ajouter des raccourcis sans l\'intervention de l\'utilisateur."</string>
     <string name="permlab_read_settings" msgid="5136500343007704955">"lire les paramètres et les raccourcis de la page d\'accueil"</string>
-    <string name="permdesc_read_settings" msgid="4208061150510996676">"Permet à l\'application de lire les paramètres et les raccourcis de l\'écran d\'accueil."</string>
+    <string name="permdesc_read_settings" msgid="4208061150510996676">"Permet à l\'appli de lire les paramètres et les raccourcis de l\'écran d\'accueil."</string>
     <string name="permlab_write_settings" msgid="4820028712156303762">"modifier les paramètres et les raccourcis de la page d\'accueil"</string>
-    <string name="permdesc_write_settings" msgid="726859348127868466">"Permet à l\'application de modifier les paramètres et les raccourcis de l\'écran d\'accueil."</string>
+    <string name="permdesc_write_settings" msgid="726859348127868466">"Permet à l\'appli de modifier les paramètres et les raccourcis de l\'écran d\'accueil."</string>
     <string name="gadget_error_text" msgid="740356548025791839">"Impossible de charger le widget"</string>
     <string name="gadget_setup_text" msgid="8348374825537681407">"Paramètres du widget"</string>
     <string name="gadget_complete_setup_text" msgid="309040266978007925">"Touchez pour terminer la configuration"</string>
-    <string name="uninstall_system_app_text" msgid="4172046090762920660">"Impossible de désinstaller cette application, car il s\'agit d\'une application système."</string>
+    <string name="uninstall_system_app_text" msgid="4172046090762920660">"Impossible de désinstaller cette appli, car il s\'agit d\'une appli système."</string>
     <string name="folder_hint_text" msgid="5174843001373488816">"Modifier le nom"</string>
-    <string name="disabled_app_label" msgid="6673129024321402780">"L\'application <xliff:g id="APP_NAME">%1$s</xliff:g> est désactivée"</string>
+    <string name="disabled_app_label" msgid="6673129024321402780">"L\'appli <xliff:g id="APP_NAME">%1$s</xliff:g> est désactivée"</string>
     <string name="dotted_app_label" msgid="1865617679843363410">"{count,plural, =1{{app_name} a # notification}one{{app_name} a # notification}other{{app_name} a # notifications}}"</string>
     <string name="default_scroll_format" msgid="7475544710230993317">"Page %1$d sur %2$d"</string>
     <string name="workspace_scroll_format" msgid="8458889198184077399">"Écran d\'accueil %1$d sur %2$d"</string>
@@ -114,7 +113,7 @@
     <string name="folder_renamed" msgid="1794088362165669656">"Nouveau nom du dossier : <xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="folder_name_format_exact" msgid="8626242716117004803">"Dossier : <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> élément(s)"</string>
     <string name="folder_name_format_overflow" msgid="4270108890534995199">"Dossier : <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="SIZE">%2$d</xliff:g> éléments ou plus"</string>
-    <string name="app_pair_name_format" msgid="8134106404716224054">"Paire d\'applications : <xliff:g id="APP1">%1$s</xliff:g> et <xliff:g id="APP2">%2$s</xliff:g>"</string>
+    <string name="app_pair_name_format" msgid="8134106404716224054">"Paire d\'applis : <xliff:g id="APP1">%1$s</xliff:g> et <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Fond d\'écran et style"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"Modifier l\'écran d\'accueil"</string>
     <string name="settings_button_text" msgid="8873672322605444408">"Paramètres d\'accueil"</string>
@@ -125,23 +124,23 @@
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Activé"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Désactivé"</string>
     <string name="title_missing_notification_access" msgid="7503287056163941064">"L\'accès aux notifications est requis"</string>
-    <string name="msg_missing_notification_access" msgid="281113995110910548">"Pour afficher les points de notification, activez les notifications d\'application pour <xliff:g id="NAME">%1$s</xliff:g>"</string>
+    <string name="msg_missing_notification_access" msgid="281113995110910548">"Pour afficher les points de notification, activez les notifications d\'appli pour <xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="title_change_settings" msgid="1376365968844349552">"Modifier les paramètres"</string>
     <string name="notification_dots_service_title" msgid="4284221181793592871">"Afficher les points de notification"</string>
     <string name="developer_options_title" msgid="700788437593726194">"Options pour les développeurs"</string>
-    <string name="auto_add_shortcuts_label" msgid="4926805029653694105">"Ajouter les icônes des applications à l\'écran d\'accueil"</string>
-    <string name="auto_add_shortcuts_description" msgid="7117251166066978730">"Pour les nouvelles applications"</string>
+    <string name="auto_add_shortcuts_label" msgid="4926805029653694105">"Ajouter les icônes des applis à l\'écran d\'accueil"</string>
+    <string name="auto_add_shortcuts_description" msgid="7117251166066978730">"Pour les nouvelles applis"</string>
     <string name="package_state_unknown" msgid="7592128424511031410">"Inconnu"</string>
     <string name="abandoned_clean_this" msgid="7610119707847920412">"Supprimer"</string>
     <string name="abandoned_search" msgid="891119232568284442">"Rechercher"</string>
-    <string name="abandoned_promises_title" msgid="7096178467971716750">"Cette application n\'est pas installée"</string>
-    <string name="abandoned_promise_explanation" msgid="3990027586878167529">"L\'application liée à cette icône n\'est pas installée. Vous pouvez la supprimer ou rechercher l\'application et l\'installer manuellement."</string>
-    <string name="app_installing_title" msgid="5864044122733792085">"Installation de l\'application <xliff:g id="NAME">%1$s</xliff:g> en cours, <xliff:g id="PROGRESS">%2$s</xliff:g> terminée"</string>
+    <string name="abandoned_promises_title" msgid="7096178467971716750">"Cette appli n\'est pas installée"</string>
+    <string name="abandoned_promise_explanation" msgid="3990027586878167529">"L\'appli liée à cette icône n\'est pas installée. Vous pouvez la supprimer ou rechercher l\'appli et l\'installer manuellement."</string>
+    <string name="app_installing_title" msgid="5864044122733792085">"Installation de l\'appli <xliff:g id="NAME">%1$s</xliff:g> en cours, <xliff:g id="PROGRESS">%2$s</xliff:g> terminée"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"Téléchargement de <xliff:g id="NAME">%1$s</xliff:g> : <xliff:g id="PROGRESS">%2$s</xliff:g>"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> en attente d\'installation"</string>
     <string name="app_archived_title" msgid="7717956158562544081">"L\'appli <xliff:g id="NAME">%1$s</xliff:g> est archivée. Touchez le bouton pour télécharger et restaurer l\'appli."</string>
-    <string name="dialog_update_title" msgid="114234265740994042">"Mise à jour de l\'application requise"</string>
-    <string name="dialog_update_message" msgid="4176784553982226114">"L\'application pour cette icône n\'est pas à jour. Vous pouvez soit la mettre à jour manuellement pour réactiver ce raccourci, soit retirer l\'icône."</string>
+    <string name="dialog_update_title" msgid="114234265740994042">"Mise à jour de l\'appli requise"</string>
+    <string name="dialog_update_message" msgid="4176784553982226114">"L\'appli pour cette icône n\'est pas à jour. Vous pouvez soit la mettre à jour manuellement pour réactiver ce raccourci, soit retirer l\'icône."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Mettre à jour"</string>
     <string name="dialog_remove" msgid="6510806469849709407">"Retirer"</string>
     <string name="widgets_list" msgid="796804551140113767">"Liste des widgets"</string>
@@ -174,15 +173,15 @@
     <string name="all_apps_personal_tab" msgid="4190252696685155002">"Personnel"</string>
     <string name="all_apps_work_tab" msgid="4884822796154055118">"Travail"</string>
     <string name="work_profile_toggle_label" msgid="3081029915775481146">"Profil professionnel"</string>
-    <string name="work_profile_edu_work_apps" msgid="7895468576497746520">"Les applications professionnelles sont indiquées par un badge et elles sont visibles pour votre administrateur informatique"</string>
+    <string name="work_profile_edu_work_apps" msgid="7895468576497746520">"Les applis professionnelles sont indiquées par un badge et elles sont visibles pour votre administrateur informatique"</string>
     <string name="work_profile_edu_accept" msgid="6069788082535149071">"OK"</string>
-    <string name="work_apps_paused_title" msgid="3040901117349444598">"Les applications professionnelles sont interrompues"</string>
-    <string name="work_apps_paused_info_body" msgid="1687828929959237477">"Vous ne recevrez pas de notifications de vos applications professionnelles"</string>
-    <string name="work_apps_paused_body" msgid="261634750995824906">"Les applications professionnelles ne peuvent ni vous envoyer de notifications, ni utiliser la pile, ni accéder à votre position"</string>
-    <string name="work_apps_paused_telephony_unavailable_body" msgid="8358872357502756790">"Vous ne recevrez pas d\'appels téléphoniques, de messages texte ni de notifications de vos applications professionnelles"</string>
-    <string name="work_apps_paused_edu_banner" msgid="8872412121608402058">"Les applications professionnelles sont indiquées par un badge et sont visibles pour votre administrateur informatique"</string>
+    <string name="work_apps_paused_title" msgid="3040901117349444598">"Les applis professionnelles sont interrompues"</string>
+    <string name="work_apps_paused_info_body" msgid="1687828929959237477">"Vous ne recevrez pas de notifications de vos applis professionnelles"</string>
+    <string name="work_apps_paused_body" msgid="261634750995824906">"Les applis professionnelles ne peuvent ni vous envoyer de notifications, ni utiliser la pile, ni accéder à votre position"</string>
+    <string name="work_apps_paused_telephony_unavailable_body" msgid="8358872357502756790">"Vous ne recevrez pas d\'appels téléphoniques, de messages texte ni de notifications de vos applis professionnelles"</string>
+    <string name="work_apps_paused_edu_banner" msgid="8872412121608402058">"Les applis professionnelles sont indiquées par un badge et sont visibles pour votre administrateur informatique"</string>
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"OK"</string>
-    <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Mettre en pause les applications professionnelles"</string>
+    <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Mettre en pause les applis professionnelles"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Réactiver"</string>
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filtrer"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Échec : <xliff:g id="WHAT">%1$s</xliff:g>"</string>
@@ -195,5 +194,5 @@
     <string name="ps_container_lock_title" msgid="2640257399982364682">"Verrouiller"</string>
     <string name="ps_container_transition" msgid="8667331812048014412">"Transition vers l\'Espace privé"</string>
     <string name="ps_add_button_label" msgid="8127988716897128773">"Installer"</string>
-    <string name="ps_add_button_content_description" msgid="3254274107740952556">"Installer des applications dans l\'Espace privé"</string>
+    <string name="ps_add_button_content_description" msgid="3254274107740952556">"Installer des applis dans l\'Espace privé"</string>
 </resources>
diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml
index 4a7c7fb..b4c5ec6 100644
--- a/res/values-fr/strings.xml
+++ b/res/values-fr/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Les widgets sont désactivés en mode sécurisé."</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Raccourci non disponible"</string>
     <string name="home_screen" msgid="5629429142036709174">"Accueil"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Définissez <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> comme application d\'accueil par défaut dans Paramètres"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Écran partagé"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Infos sur l\'appli pour %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Paramètres d\'utilisation pour %1$s"</string>
diff --git a/res/values-gl/strings.xml b/res/values-gl/strings.xml
index 0430a9a..ac72802 100644
--- a/res/values-gl/strings.xml
+++ b/res/values-gl/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Os widgets están desactivados no modo seguro"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"O atallo non está dispoñible"</string>
     <string name="home_screen" msgid="5629429142036709174">"Inicio"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Define <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> como aplicación de inicio predeterminada en Configuración"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Pantalla dividida"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Información da aplicación para %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Configuración de uso para %1$s"</string>
diff --git a/res/values-gu/strings.xml b/res/values-gu/strings.xml
index 3c248db..74747d0 100644
--- a/res/values-gu/strings.xml
+++ b/res/values-gu/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"સુરક્ષિત મોડમાં વિજેટ્સ અક્ષમ કર્યા"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"શૉર્ટકટ ઉપલબ્ધ નથી"</string>
     <string name="home_screen" msgid="5629429142036709174">"હોમ સ્ક્રીન"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"સેટિંગમાં <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g>ને ડિફૉલ્ટ હોમ ઍપ તરીકે સેટ કરો"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"સ્ક્રીનને વિભાજિત કરો"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$s માટે ઍપ માહિતી"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"%1$sના વપરાશ સંબંધિત સેટિંગ"</string>
diff --git a/res/values-hi/strings.xml b/res/values-hi/strings.xml
index 50d4762..6071935 100644
--- a/res/values-hi/strings.xml
+++ b/res/values-hi/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"विजेट सुरक्षित मोड में अक्षम हैं"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"शॉर्टकट उपलब्ध नहीं है"</string>
     <string name="home_screen" msgid="5629429142036709174">"होम स्क्रीन"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"सेटिंग में जाकर, <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> को डिफ़ॉल्ट होम ऐप्लिकेशन के तौर पर सेट करें"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"स्प्लिट स्क्रीन"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$s के लिए ऐप्लिकेशन की जानकारी"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"%1$s के लिए खर्च की सेटिंग"</string>
diff --git a/res/values-hr/strings.xml b/res/values-hr/strings.xml
index 7a1fd94..cf7a91a 100644
--- a/res/values-hr/strings.xml
+++ b/res/values-hr/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Widgeti su onemogućeni u Sigurnom načinu rada"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Prečac nije dostupan"</string>
     <string name="home_screen" msgid="5629429142036709174">"Početni zaslon"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Postavite aplikaciju <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> kao zadanu aplikaciju početnog zaslona u postavkama"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Podijeljeni zaslon"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Informacije o aplikaciji %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Postavke upotrebe za %1$s"</string>
diff --git a/res/values-hu/strings.xml b/res/values-hu/strings.xml
index 3b556da..f306110 100644
--- a/res/values-hu/strings.xml
+++ b/res/values-hu/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"A modulok ki vannak kapcsolva Csökkentett módban"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"A gyorsparancs nem áll rendelkezésre"</string>
     <string name="home_screen" msgid="5629429142036709174">"Kezdőképernyő"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"A(z) <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> appot a Beállításokban adhatja meg alapértelmezett kezdőalkalmazásként."</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Osztott képernyő"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Alkalmazásinformáció a következőhöz: %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"A(z) %1$s használati beállításai"</string>
@@ -188,7 +187,7 @@
     <string name="remote_action_failed" msgid="1383965239183576790">"Sikertelen: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Privát terület"</string>
     <string name="private_space_secondary_label" msgid="9203933341714508907">"Koppintson a beállításhoz vagy a megnyitáshoz"</string>
-    <string name="ps_container_title" msgid="4391796149519594205">"Privát terület"</string>
+    <string name="ps_container_title" msgid="4391796149519594205">"Privát"</string>
     <string name="ps_container_settings" msgid="6059734123353320479">"Privát terület beállításai"</string>
     <string name="ps_container_unlock_button_content_description" msgid="9181551784092204234">"Privát, feloldott."</string>
     <string name="ps_container_lock_button_content_description" msgid="5961993384382649530">"Privát, zárolt."</string>
diff --git a/res/values-hy/strings.xml b/res/values-hy/strings.xml
index 491ba05..2d345f2 100644
--- a/res/values-hy/strings.xml
+++ b/res/values-hy/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Վիջեթներն անջատված են անվտանգ ռեժիմում"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Դյուրանցումն անհասանելի է"</string>
     <string name="home_screen" msgid="5629429142036709174">"Հիմնական էկրան"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Սահմանել <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> գործարկիչը որպես մեկնարկի կանխադրված հավելված Կարգավորումներում"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Տրոհել էկրանը"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Տեղեկություններ %1$s հավելվածի մասին"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Օգտագործման կարգավորումներ (%1$s)"</string>
diff --git a/res/values-in/strings.xml b/res/values-in/strings.xml
index 1335385..9ced9f4 100644
--- a/res/values-in/strings.xml
+++ b/res/values-in/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Widget dinonaktifkan dalam mode Aman"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Pintasan tidak tersedia"</string>
     <string name="home_screen" msgid="5629429142036709174">"Layar utama"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Jadikan <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> sebagai aplikasi layar utama default di Setelan"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Layar terpisah"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Info aplikasi untuk %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Setelan penggunaan untuk %1$s"</string>
diff --git a/res/values-is/strings.xml b/res/values-is/strings.xml
index 4865264..2ab7817 100644
--- a/res/values-is/strings.xml
+++ b/res/values-is/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Græjur eru óvirkar í öruggri stillingu"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Flýtileið er ekki tiltæk"</string>
     <string name="home_screen" msgid="5629429142036709174">"Heim"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Stilltu <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> sem sjálfgefið heimaforrit í stillingunum"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Skipta skjá"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Upplýsingar um forrit fyrir %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Notkunarstillingar fyrir %1$s"</string>
diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml
index 41b4b60..a59de6c 100644
--- a/res/values-it/strings.xml
+++ b/res/values-it/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Widget disabilitati in modalità provvisoria"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"La scorciatoia non è disponibile"</string>
     <string name="home_screen" msgid="5629429142036709174">"Home"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Imposta <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> come app iniziale predefinita nelle Impostazioni"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Schermo diviso"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Informazioni sull\'app %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Impostazioni di utilizzo per %1$s"</string>
diff --git a/res/values-iw/strings.xml b/res/values-iw/strings.xml
index ae71edd..89a0c8a 100644
--- a/res/values-iw/strings.xml
+++ b/res/values-iw/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"ווידג\'טים מושבתים במצב בטוח"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"קיצור הדרך אינו זמין"</string>
     <string name="home_screen" msgid="5629429142036709174">"בית"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"הגדרה של <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> כאפליקציית הבית ב\'הגדרות\'"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"מסך מפוצל"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"‏פרטים על האפליקציה %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"‏הגדרות שימוש ב-%1$s"</string>
diff --git a/res/values-ja/strings.xml b/res/values-ja/strings.xml
index c64d335..b48c7be 100644
--- a/res/values-ja/strings.xml
+++ b/res/values-ja/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"セーフモードではウィジェットは無効です"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"ショートカットは使用できません"</string>
     <string name="home_screen" msgid="5629429142036709174">"ホーム"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"[設定] で <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> をデフォルトのホームアプリに設定します"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"分割画面"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$s のアプリ情報"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"%1$s の使用設定"</string>
diff --git a/res/values-ka/strings.xml b/res/values-ka/strings.xml
index bcced9a..76b8b5d 100644
--- a/res/values-ka/strings.xml
+++ b/res/values-ka/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"უსაფრთხო რეჟიმში ვიჯეტი გამორთულია"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"მალსახმობი მიუწვდომელია"</string>
     <string name="home_screen" msgid="5629429142036709174">"მთავარი გვერდი"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"<xliff:g id="LAUNCHER_NAME">%1$s</xliff:g>-ის დაყენება ნაგულისხმევ Home აპად პარამეტრებში"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"ეკრანის გაყოფა"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$s-ის აპის ინფო"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"გამოყენების პარამეტრები %1$s-ისთვის"</string>
diff --git a/res/values-kk/strings.xml b/res/values-kk/strings.xml
index 62bb983..95d4420 100644
--- a/res/values-kk/strings.xml
+++ b/res/values-kk/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Қауіпсіз режимде виджеттер өшіріледі"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Таңбаша қолжетімді емес"</string>
     <string name="home_screen" msgid="5629429142036709174">"Негізгі экран"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Параметрлерден <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> қолданбасын әдепкі негізгі экран қолданбасы ретінде орнатыңыз."</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Экранды бөлу"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$s қолданбасы туралы ақпарат"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"%1$s пайдалану параметрлері"</string>
diff --git a/res/values-km/strings.xml b/res/values-km/strings.xml
index b733699..5c71276 100644
--- a/res/values-km/strings.xml
+++ b/res/values-km/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"បាន​បិទ​ធាតុ​ក្រាហ្វិក​ក្នុង​របៀប​សុវត្ថិភាព"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"ផ្លូវកាត់មិនអាចប្រើបានទេ"</string>
     <string name="home_screen" msgid="5629429142036709174">"អេក្រង់ដើម"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"កំណត់ <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> ជាកម្មវិធីអេក្រង់ដើមលំនាំដើមនៅក្នុង \"ការកំណត់\""</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"មុខងារ​បំបែកអេក្រង់"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"ព័ត៌មានកម្មវិធី​សម្រាប់ %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"ការកំណត់ការប្រើប្រាស់សម្រាប់ %1$s"</string>
diff --git a/res/values-kn/strings.xml b/res/values-kn/strings.xml
index 16b39f8..931ca30 100644
--- a/res/values-kn/strings.xml
+++ b/res/values-kn/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"ಸುರಕ್ಷಿತ ಮೋಡ್‌ನಲ್ಲಿ ವಿಜೆಟ್‌ಗಳನ್ನು ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಲಾಗಿದೆ"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"ಶಾರ್ಟ್‌ಕಟ್ ಲಭ್ಯವಿಲ್ಲ"</string>
     <string name="home_screen" msgid="5629429142036709174">"ಹೋಮ್"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"ಸೆಟ್ಟಿಂಗ್‌ಗಳಲ್ಲಿ <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> ಅನ್ನು ಡೀಫಾಲ್ಟ್ ಹೋಮ್ ಆ್ಯಪ್ ಆಗಿ ಸೆಟ್‌ ಮಾಡಿ"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"ಸ್ಪ್ಲಿಟ್ ಸ್ಕ್ರೀನ್"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$s ಗಾಗಿ ಆ್ಯಪ್ ಮಾಹಿತಿ"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"%1$s ಗೆ ಸಂಬಂಧಿಸಿದ ಬಳಕೆಯ ಸೆಟ್ಟಿಂಗ್‌ಗಳು"</string>
diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml
index 1d5c58f..9f64cfb 100644
--- a/res/values-ko/strings.xml
+++ b/res/values-ko/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"안전 모드에서 위젯 사용 중지됨"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"바로가기를 사용할 수 없음"</string>
     <string name="home_screen" msgid="5629429142036709174">"홈"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"설정에서 <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> 런처를 기본 홈 앱으로 설정하세요."</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"화면 분할"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$s 앱 정보"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"%1$s의 사용량 설정"</string>
diff --git a/res/values-ky/strings.xml b/res/values-ky/strings.xml
index 6f01db3..3e67c36 100644
--- a/res/values-ky/strings.xml
+++ b/res/values-ky/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Виджеттер Коопсуз режимде өчүрүлгөн"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Кыска жол жок"</string>
     <string name="home_screen" msgid="5629429142036709174">"Башкы экран"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Параметрлерден <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> жүргүзгүчүн демейки башкы бет колдонмосу катары коюу"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Экранды бөлүү"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$s колдонмосу жөнүндө маалымат"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"%1$s колдонмосун пайдалануу параметрлери"</string>
diff --git a/res/values-lo/strings.xml b/res/values-lo/strings.xml
index 7ec103b..e0cd3ee 100644
--- a/res/values-lo/strings.xml
+++ b/res/values-lo/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"​ວິດ​ເຈັດ​ຖືກ​ປິດ​ໃນ Safe mode"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"ບໍ່ສາມາດໃຊ້ທາງລັດໄດ້"</string>
     <string name="home_screen" msgid="5629429142036709174">"ໂຮມສະກຣີນ"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"ຕັ້ງ <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> ເປັນແອັບໂຮມເລີ່ມຕົ້ນໃນການຕັ້ງຄ່າ"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"ແບ່ງໜ້າຈໍ"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"ຂໍ້ມູນແອັບສຳລັບ %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"ການຕັ້ງຄ່າການນຳໃຊ້ສຳລັບ %1$s"</string>
diff --git a/res/values-lt/strings.xml b/res/values-lt/strings.xml
index 1112474..9846525 100644
--- a/res/values-lt/strings.xml
+++ b/res/values-lt/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Valdikliai išjungti Saugiame režime"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Sparčiojo klavišo negalima naudoti"</string>
     <string name="home_screen" msgid="5629429142036709174">"Pagrindinis"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <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="split_app_info_accessibility" msgid="5475288491241414932">"Programos „%1$s“ informacija"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"„%1$s“ naudojimo nustatymai"</string>
diff --git a/res/values-lv/strings.xml b/res/values-lv/strings.xml
index 4f1a92c..49f7ffe 100644
--- a/res/values-lv/strings.xml
+++ b/res/values-lv/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Logrīki atspējoti drošajā režīmā"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Saīsne nav pieejama."</string>
     <string name="home_screen" msgid="5629429142036709174">"Sākums"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Iestatiet <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> kā noklusējuma sākuma lietotni iestatījumos."</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Sadalīt ekrānu"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$s: informācija par lietotni"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Lietojuma iestatījumi: %1$s"</string>
diff --git a/res/values-mk/strings.xml b/res/values-mk/strings.xml
index d984ca0..2bfa089 100644
--- a/res/values-mk/strings.xml
+++ b/res/values-mk/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Додатоците се оневозможени во безбеден режим"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Кратенката не е достапна"</string>
     <string name="home_screen" msgid="5629429142036709174">"Почетен екран"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Поставете <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> да биде стандардна апликација за почетен екран во „Поставки“"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Поделен екран"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Податоци за апликација за %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Поставки за користење за %1$s"</string>
diff --git a/res/values-ml/strings.xml b/res/values-ml/strings.xml
index ea8849e..a2babd5 100644
--- a/res/values-ml/strings.xml
+++ b/res/values-ml/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"സുരക്ഷിത മോഡിൽ വിജറ്റുകൾ പ്രവർത്തനരഹിതമാക്കി"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"കുറുക്കുവഴി ലഭ്യമല്ല"</string>
     <string name="home_screen" msgid="5629429142036709174">"ഹോം"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"ക്രമീകരണത്തിൽ <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> എന്നത് ഡിഫോൾട്ട് ഹോം ആപ്പായി സജ്ജീകരിക്കുക"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"സ്‌ക്രീൻ വിഭജന മോഡ്"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$s എന്നതിന്റെ ആപ്പ് വിവരങ്ങൾ"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"%1$s എന്നതിനുള്ള ഉപയോഗ ക്രമീകരണം"</string>
diff --git a/res/values-mn/strings.xml b/res/values-mn/strings.xml
index 8554717..93a1c9c 100644
--- a/res/values-mn/strings.xml
+++ b/res/values-mn/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Safe горимд виджетүүдийг идэвхгүйжүүлсэн"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Товчлол алга"</string>
     <string name="home_screen" msgid="5629429142036709174">"Нүүр"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"<xliff:g id="LAUNCHER_NAME">%1$s</xliff:g>-г Тохиргоонд өгөгдмөл үндсэн аппаар тохируулна уу"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Дэлгэцийг хуваах"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$s-н аппын мэдээлэл"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"%1$s-н ашиглалтын тохиргоо"</string>
diff --git a/res/values-mr/strings.xml b/res/values-mr/strings.xml
index dcdf25c..c084fab 100644
--- a/res/values-mr/strings.xml
+++ b/res/values-mr/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"विजेट सुरक्षित मोडमध्ये अक्षम झाले"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"शॉर्टकट उपलब्ध नाही"</string>
     <string name="home_screen" msgid="5629429142036709174">"होम"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"सेटिंग्ज मध्ये <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> हे डीफॉल्ट होम अ‍ॅप म्हणून सेट करा"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"स्प्लिट स्क्रीन"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$s साठी ॲपशी संबंधित माहिती"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"%1$s साठी वापरासंबंधित सेटिंग्ज"</string>
diff --git a/res/values-ms/strings.xml b/res/values-ms/strings.xml
index 22ee2cb..72d92e2 100644
--- a/res/values-ms/strings.xml
+++ b/res/values-ms/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Widget dilumpuhkan dalam mod Selamat"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Pintasan tidak tersedia"</string>
     <string name="home_screen" msgid="5629429142036709174">"Rumah"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Tetapkan <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> sebagai apl skrin utama lalai dalam Tetapan"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Skrin pisah"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Maklumat apl untuk %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Tetapan penggunaan sebanyak %1$s"</string>
diff --git a/res/values-my/strings.xml b/res/values-my/strings.xml
index bca1c6e..194ece7 100644
--- a/res/values-my/strings.xml
+++ b/res/values-my/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"လုံခြုံရေး မုဒ်ထဲမှာ ဝီဂျက်များကို ပိတ်ထား"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"ဖြတ်လမ်း မရနိုင်ပါ"</string>
     <string name="home_screen" msgid="5629429142036709174">"ပင်မစာမျက်နှာ"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"ဆက်တင်များတွင် <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> ကို မူရင်းပင်မစာမျက်နှာအက်ပ်အဖြစ် သတ်မှတ်ရန်"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"မျက်နှာပြင် ခွဲ၍ပြသခြင်း"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$s အတွက် အက်ပ်အချက်အလက်"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"%1$s အတွက် အသုံးပြုမှုဆက်တင်များ"</string>
diff --git a/res/values-nb/strings.xml b/res/values-nb/strings.xml
index 1440443..a9e6c5d 100644
--- a/res/values-nb/strings.xml
+++ b/res/values-nb/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Moduler er deaktivert i sikker modus"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Snarveien er ikke tilgjengelig"</string>
     <string name="home_screen" msgid="5629429142036709174">"Startskjerm"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Angi <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> som standard startsideapp i innstillingene"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Delt skjerm"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Appinformasjon for %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Bruksinnstillinger for %1$s"</string>
diff --git a/res/values-ne/strings.xml b/res/values-ne/strings.xml
index 52613cb..7880c36 100644
--- a/res/values-ne/strings.xml
+++ b/res/values-ne/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"सुरक्षित मोडमा विगेटहरू अक्षम गरियो"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"सर्टकट उपलब्ध छैन"</string>
     <string name="home_screen" msgid="5629429142036709174">"होम"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"सेटिङमा गई <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> लाई डिफल्ट होम एपका रूपमा सेट गर्नुहोस्"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"स्प्लिट स्क्रिन"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$s का हकमा एपसम्बन्धी जानकारी"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"%1$s को प्रयोगसम्बन्धी सेटिङ"</string>
diff --git a/res/values-night-v31/colors.xml b/res/values-night-v31/colors.xml
index d23f4d1..2688b83 100644
--- a/res/values-night-v31/colors.xml
+++ b/res/values-night-v31/colors.xml
@@ -26,6 +26,8 @@
     <color name="home_settings_track_off_color">@android:color/system_neutral1_700</color>
 
     <color name="widget_picker_title_color_dark">@android:color/system_neutral1_100</color>
+    <color name="widget_picker_description_color_dark">
+        @android:color/system_neutral2_200</color>
     <color name="widget_picker_header_app_title_color_dark">
         @android:color/system_neutral1_100</color>
     <color name="widget_picker_header_app_subtitle_color_dark">
diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml
index 90dcd46..7b3f563 100644
--- a/res/values-nl/strings.xml
+++ b/res/values-nl/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Widgets uitgezet in veilige modus"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Snelkoppeling is niet beschikbaar"</string>
     <string name="home_screen" msgid="5629429142036709174">"Startscherm"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Stel <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> in als de standaard startscherm-app in Instellingen"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Gesplitst scherm"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"App-info voor %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Gebruiksinstellingen voor %1$s"</string>
diff --git a/res/values-or/strings.xml b/res/values-or/strings.xml
index 6d2f87e..33b7664 100644
--- a/res/values-or/strings.xml
+++ b/res/values-or/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"ନିରାପଦ ମୋଡରେ ୱିଜେଟ୍‌ ଅକ୍ଷମ କରାଗଲା"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"ଶର୍ଟକଟ୍‌ ଉପଲବ୍ଧ ନାହିଁ"</string>
     <string name="home_screen" msgid="5629429142036709174">"ହୋମ"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"ସେଟିଂସରେ <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g>କୁ ଡିଫଲ୍ଟ Home ଆପ ଭାବରେ ସେଟ କରନ୍ତୁ"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"ସ୍କ୍ରିନ‌କୁ ସ୍ପ୍ଲିଟ କରନ୍ତୁ"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$s ପାଇଁ ଆପ ସୂଚନା"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"%1$s ପାଇଁ ବ୍ୟବହାର ସେଟିଂସ"</string>
diff --git a/res/values-pa/strings.xml b/res/values-pa/strings.xml
index b287b2f..71c25eb 100644
--- a/res/values-pa/strings.xml
+++ b/res/values-pa/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"ਵਿਜੇਟ ਸੁਰੱਖਿਅਤ ਮੋਡ ਵਿੱਚ ਅਸਮਰਥਿਤ"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"ਸ਼ਾਰਟਕੱਟ ਉਪਲਬਧ ਨਹੀਂ ਹੈ"</string>
     <string name="home_screen" msgid="5629429142036709174">"ਮੁੱਖ ਪੰਨਾ"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"ਸੈਟਿੰਗਾਂ ਵਿੱਚ ਜਾ ਕੇ <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> ਨੂੰ ਪੂਰਵ-ਨਿਰਧਾਰਿਤ ਹੋਮ ਐਪ ਵਜੋਂ ਸੈੱਟ ਕਰੋ"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"ਸਪਲਿਟ ਸਕ੍ਰੀਨ"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$s ਲਈ ਐਪ ਜਾਣਕਾਰੀ"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"%1$s ਲਈ ਵਰਤੋਂ ਸੈਟਿੰਗਾਂ"</string>
@@ -186,14 +185,14 @@
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"ਰੋਕ ਹਟਾਓ"</string>
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"ਫਿਲਟਰ"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"ਇਹ ਕਾਰਵਾਈ ਅਸਫਲ ਹੋਈ: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
-    <string name="private_space_label" msgid="2359721649407947001">"ਨਿੱਜੀ ਸਪੇਸ"</string>
+    <string name="private_space_label" msgid="2359721649407947001">"ਪ੍ਰਾਈਵੇਟ ਸਪੇਸ"</string>
     <string name="private_space_secondary_label" msgid="9203933341714508907">"ਸੈੱਟਅੱਪ ਕਰਨ ਜਾਂ ਖੋਲ੍ਹਣ ਲਈ ਟੈਪ ਕਰੋ"</string>
     <string name="ps_container_title" msgid="4391796149519594205">"ਨਿੱਜੀ"</string>
-    <string name="ps_container_settings" msgid="6059734123353320479">"ਨਿੱਜੀ ਸਪੇਸ ਸੰਬੰਧੀ ਸੈਟਿੰਗਾਂ"</string>
+    <string name="ps_container_settings" msgid="6059734123353320479">"ਪ੍ਰਾਈਵੇਟ ਸਪੇਸ ਸੰਬੰਧੀ ਸੈਟਿੰਗਾਂ"</string>
     <string name="ps_container_unlock_button_content_description" msgid="9181551784092204234">"ਨਿੱਜੀ, ਅਣਲਾਕ ਕੀਤਾ ਗਿਆ।"</string>
     <string name="ps_container_lock_button_content_description" msgid="5961993384382649530">"ਨਿੱਜੀ, ਲਾਕ ਕੀਤਾ ਗਿਆ।"</string>
     <string name="ps_container_lock_title" msgid="2640257399982364682">"ਲਾਕ ਕਰੋ"</string>
-    <string name="ps_container_transition" msgid="8667331812048014412">"ਨਿੱਜੀ ਸਪੇਸ ਨੂੰ ਤਬਦੀਲ ਕਰਨਾ"</string>
+    <string name="ps_container_transition" msgid="8667331812048014412">"ਪ੍ਰਾਈਵੇਟ ਸਪੇਸ ਨੂੰ ਤਬਦੀਲ ਕਰਨਾ"</string>
     <string name="ps_add_button_label" msgid="8127988716897128773">"ਸਥਾਪਤ ਕਰੋ"</string>
     <string name="ps_add_button_content_description" msgid="3254274107740952556">"ਪ੍ਰਾਈਵੇਟ ਸਪੇਸ ਵਿੱਚ ਐਪਾਂ ਸਥਾਪਤ ਕਰੋ"</string>
 </resources>
diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml
index 685b7d9..587c40f 100644
--- a/res/values-pl/strings.xml
+++ b/res/values-pl/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Widżety są wyłączone w trybie bezpiecznym"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Skrót nie jest dostępny"</string>
     <string name="home_screen" msgid="5629429142036709174">"Ekran główny"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Otwórz Ustawienia i ustaw aplikację <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> jako domyślną aplikację ekranu głównego"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Podziel ekran"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Informacje o aplikacji: %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"%1$s – ustawienia użycia"</string>
diff --git a/res/values-pt-rPT/strings.xml b/res/values-pt-rPT/strings.xml
index 14a3cf5..67d93de 100644
--- a/res/values-pt-rPT/strings.xml
+++ b/res/values-pt-rPT/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Widgets desativados no Modo de segurança"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"O atalho não está disponível"</string>
     <string name="home_screen" msgid="5629429142036709174">"Página inicial"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Defina a app <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> como a app inicial predefinida nas Definições"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Ecrã dividido"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Informações da app para %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Definições de utilização para %1$s"</string>
diff --git a/res/values-pt/strings.xml b/res/values-pt/strings.xml
index 4c4b13b..4302588 100644
--- a/res/values-pt/strings.xml
+++ b/res/values-pt/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Widgets desativados no modo de segurança"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"O atalho não está disponível"</string>
     <string name="home_screen" msgid="5629429142036709174">"Início"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Definir <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> como app de início padrão nas Configurações"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Tela dividida"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Informações do app %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Configurações de uso de %1$s"</string>
diff --git a/res/values-ro/strings.xml b/res/values-ro/strings.xml
index 256fa60..d9e0413 100644
--- a/res/values-ro/strings.xml
+++ b/res/values-ro/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Widgeturile sunt dezactivate în modul de siguranță"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Comanda rapidă nu este disponibilă"</string>
     <string name="home_screen" msgid="5629429142036709174">"Pagina de pornire"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Setează <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> drept aplicație ecran de pornire prestabilită, în Setări"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Ecran împărțit"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Informații despre aplicație pentru %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Setări de utilizare pentru %1$s"</string>
diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml
index 402143d..64a21f7 100644
--- a/res/values-ru/strings.xml
+++ b/res/values-ru/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Виджеты отключены в безопасном режиме"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Ярлык недоступен"</string>
     <string name="home_screen" msgid="5629429142036709174">"Главный экран"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Установить \"<xliff:g id="LAUNCHER_NAME">%1$s</xliff:g>\" в качестве приложения главного экрана по умолчанию в настройках"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Разделить экран"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Сведения о приложении \"%1$s\""</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Настройки использования приложения \"%1$s\""</string>
diff --git a/res/values-si/strings.xml b/res/values-si/strings.xml
index 455f18b..71efc03 100644
--- a/res/values-si/strings.xml
+++ b/res/values-si/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"සුරක්ෂිත ආකාරය තුළ විජටය අබල කරන ලදි"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"කෙටි මග ලබා ගත නොහැකිය"</string>
     <string name="home_screen" msgid="5629429142036709174">"මුල් පිටුව"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"<xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> සැකසීම් තුළ පෙරනිමි මුල් පිටුව යෙදුම ලෙස සකසන්න"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"බෙදුම් තිරය"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$s සඳහා යෙදුම් තතු"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"%1$s සඳහා භාවිත සැකසීම්"</string>
diff --git a/res/values-sk/strings.xml b/res/values-sk/strings.xml
index 7771cf7..43bc29c 100644
--- a/res/values-sk/strings.xml
+++ b/res/values-sk/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Miniaplikácie sú v núdzovom režime zakázané"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Skratky nie sú k dispozícii"</string>
     <string name="home_screen" msgid="5629429142036709174">"Domov"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Nastaviť aplikáciu <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> ako predvolenú vstupnú aplikáciu v Nastaveniach"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Rozdeliť obrazovku"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Informácie o aplikácii pre %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Nastavenia používania pre %1$s"</string>
diff --git a/res/values-sl/strings.xml b/res/values-sl/strings.xml
index 2e75b17..5d423ca 100644
--- a/res/values-sl/strings.xml
+++ b/res/values-sl/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Pripomočki so onemogočeni v varnem načinu"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Bližnjica ni na voljo"</string>
     <string name="home_screen" msgid="5629429142036709174">"Začetni zaslon"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Nastavitev zaganjalnika <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> kot privzete aplikacije za začetni zaslon v nastavitvah"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Razdeljen zaslon"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Podatki o aplikaciji za: %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Nastavitve uporabe za »%1$s«"</string>
diff --git a/res/values-sq/strings.xml b/res/values-sq/strings.xml
index a00f11a..8f4133d 100644
--- a/res/values-sq/strings.xml
+++ b/res/values-sq/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Miniaplikacionet janë të çaktivizuara në modalitetin e sigurt"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Shkurtorja nuk është e disponueshme"</string>
     <string name="home_screen" msgid="5629429142036709174">"Ekrani bazë"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Cakto <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> si aplikacionin e parazgjedhur të ekranit bazë te \"Cilësimet\""</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Ekrani i ndarë"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Informacioni i aplikacionit për %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Cilësimet e përdorimit për \"%1$s\""</string>
diff --git a/res/values-sr/strings.xml b/res/values-sr/strings.xml
index 97d3ff4..cd6523c 100644
--- a/res/values-sr/strings.xml
+++ b/res/values-sr/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Виџети су онемогућени у Безбедном режиму"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Пречица није доступна"</string>
     <string name="home_screen" msgid="5629429142036709174">"Почетни екран"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Подесите <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> као подразумевану почетну апликацију у Подешавањима"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Подељени екран"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Информације о апликацији за: %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Подешавања потрошње за %1$s"</string>
diff --git a/res/values-sv/strings.xml b/res/values-sv/strings.xml
index 8a8524e..4f95c67 100644
--- a/res/values-sv/strings.xml
+++ b/res/values-sv/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Widgets är inaktiverade i felsäkert läge"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Genvägen är inte tillgänglig"</string>
     <string name="home_screen" msgid="5629429142036709174">"Startskärm"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Ställ in <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> som standardstartskärmsapp i Inställningar"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Delad skärm"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Appinformation för %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Användningsinställningar för %1$s"</string>
diff --git a/res/values-sw/strings.xml b/res/values-sw/strings.xml
index 64f6296..53a9abe 100644
--- a/res/values-sw/strings.xml
+++ b/res/values-sw/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Wijeti zimezimwa katika hali ya Usalama"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Hakuna njia ya mkato"</string>
     <string name="home_screen" msgid="5629429142036709174">"Skrini ya kwanza"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Weka <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> iwe programu chaguomsingi ya mwanzo kwenye Mipangilio"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Gawa skrini"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Maelezo ya programu ya %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Mipangilio ya matumizi ya %1$s"</string>
diff --git a/res/values-ta/strings.xml b/res/values-ta/strings.xml
index 189f4ce..738f85c 100644
--- a/res/values-ta/strings.xml
+++ b/res/values-ta/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"பாதுகாப்புப் பயன்முறையில் விட்ஜெட்கள் முடக்கப்பட்டுள்ளன"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"ஷார்ட்கட் இல்லை"</string>
     <string name="home_screen" msgid="5629429142036709174">"முகப்பு"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"அமைப்புகளில் <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> என்பதை இயல்பு முகப்பு ஆப்ஸாக அமையுங்கள்"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"திரைப் பிரிப்பு"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$sக்கான ஆப்ஸ் தகவல்கள்"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"%1$sக்கான உபயோக அமைப்புகள்"</string>
diff --git a/res/values-te/strings.xml b/res/values-te/strings.xml
index d789baa..0520ebf 100644
--- a/res/values-te/strings.xml
+++ b/res/values-te/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"సురక్షిత మోడ్‌లో విడ్జెట్‌లు నిలిపివేయబడ్డాయి"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"షార్ట్‌కట్ అందుబాటులో లేదు"</string>
     <string name="home_screen" msgid="5629429142036709174">"మొదటి ట్యాబ్"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"సెట్టింగ్‌లలో <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g>‌ను ఆటోమేటిక్ సెట్టింగ్ మొదటి స్క్రీన్ యాప్‌గా సెట్ చేయండి"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"స్ప్లిట్ స్క్రీన్"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$s కోసం యాప్ సమాచారం"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"%1$sకు సంబంధించిన వినియోగ సెట్టింగ్‌లు"</string>
diff --git a/res/values-th/strings.xml b/res/values-th/strings.xml
index f89638d..554fd94 100644
--- a/res/values-th/strings.xml
+++ b/res/values-th/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"มีการปิดใช้งานวิดเจ็ตในเซฟโหมด"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"ทางลัดไม่พร้อมใช้งาน"</string>
     <string name="home_screen" msgid="5629429142036709174">"หน้าแรก"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"ตั้ง <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> เป็นแอปหน้าแรกเริ่มต้นในการตั้งค่า"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"แยกหน้าจอ"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"ข้อมูลแอปสำหรับ %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"การตั้งค่าการใช้งานสำหรับ %1$s"</string>
diff --git a/res/values-tl/strings.xml b/res/values-tl/strings.xml
index 8754a12..cb6fe66 100644
--- a/res/values-tl/strings.xml
+++ b/res/values-tl/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Naka-disable ang mga widget sa Safe mode"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Hindi available ang shortcut"</string>
     <string name="home_screen" msgid="5629429142036709174">"Home"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Itakda ang <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> bilang default na home app sa Mga Setting"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Split screen"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Impormasyon ng app para sa %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Mga setting ng paggamit para sa %1$s"</string>
diff --git a/res/values-tr/strings.xml b/res/values-tr/strings.xml
index 761ce56..b12ec27 100644
--- a/res/values-tr/strings.xml
+++ b/res/values-tr/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Güvenli modda widget\'lar devre dışı"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Kısayol kullanılamıyor"</string>
     <string name="home_screen" msgid="5629429142036709174">"Ana ekran"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"<xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> başlatıcısını Ayarlar\'da varsayılan ana ekran uygulaması olarak ayarlayın"</string>
     <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>
diff --git a/res/values-uk/strings.xml b/res/values-uk/strings.xml
index 2c85d5a..2a01dae 100644
--- a/res/values-uk/strings.xml
+++ b/res/values-uk/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"У безпечному режимі віджети вимкнено"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Ярлик недоступний"</string>
     <string name="home_screen" msgid="5629429142036709174">"Головний екран"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Зробити <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> додатком головного екрана за умовчанням у налаштуваннях"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Розділити екран"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Інформація про додаток для %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Параметри використання (%1$s)"</string>
diff --git a/res/values-ur/strings.xml b/res/values-ur/strings.xml
index 09f4304..544357e 100644
--- a/res/values-ur/strings.xml
+++ b/res/values-ur/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"ویجیٹس کو محفوظ وضع میں غیر فعال کر دیا گیا"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"شارٹ کٹ دستیاب نہیں ہے"</string>
     <string name="home_screen" msgid="5629429142036709174">"ہوم"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"ترتیبات میں <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> کو بطور ڈیفالٹ ہوم ایپ سیٹ کریں"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"اسپلٹ اسکرین"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"‏%1$s کے لیے ایپ کی معلومات"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"‏%1$s کیلئے استعمال کی ترتیبات"</string>
diff --git a/res/values-uz/strings.xml b/res/values-uz/strings.xml
index d8dea69..5038d4f 100644
--- a/res/values-uz/strings.xml
+++ b/res/values-uz/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Xavfsiz rejimda vidjetlar o‘chirib qo‘yilgan"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Tezkor tugmadan foydalanib bo‘lmaydi"</string>
     <string name="home_screen" msgid="5629429142036709174">"Bosh ekran"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Sozlamalar orqali <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> ilovasini birlamchi bosh ekran ilovasi sifatida belgilash"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Ekranni ikkiga ajratish"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$s ilovasi axboroti"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"%1$s uchun sarf sozlamalari"</string>
diff --git a/res/values-v31/colors.xml b/res/values-v31/colors.xml
index fa87221..7270366 100644
--- a/res/values-v31/colors.xml
+++ b/res/values-v31/colors.xml
@@ -75,6 +75,8 @@
 
     <color name="widget_picker_title_color_light">
         @android:color/system_neutral1_900</color>
+    <color name="widget_picker_description_color_light">
+        @android:color/system_neutral2_700</color>
     <color name="widget_picker_header_app_title_color_light">
         @android:color/system_neutral1_900</color>
     <color name="widget_picker_header_app_subtitle_color_light">
diff --git a/res/values-v33/style.xml b/res/values-v33/style.xml
deleted file mode 100644
index 1261b23..0000000
--- a/res/values-v33/style.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
-* Copyright (C) 2022 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.
-*/
--->
-
-<resources>
-    <style name="HomeSettings.Theme" parent="@android:style/Theme.DeviceDefault.Settings">
-        <item name="android:listPreferredItemPaddingEnd">16dp</item>
-        <item name="android:listPreferredItemPaddingStart">24dp</item>
-        <item name="android:navigationBarColor">@android:color/transparent</item>
-        <item name="android:statusBarColor">@android:color/transparent</item>
-        <item name="android:switchStyle">@style/SwitchStyle</item>
-        <item name="android:textAppearanceListItem">@style/HomeSettings.PreferenceTitle</item>
-        <item name="android:windowActionBar">false</item>
-        <item name="android:windowNoTitle">true</item>
-        <item name="preferenceTheme">@style/HomeSettings.PreferenceTheme</item>
-        <item name="android:windowAnimationStyle">@style/Animation.SharedBackground</item>
-    </style>
-
-    <style name="Animation.SharedBackground" parent="@android:style/Animation.Activity">
-        <item name="android:activityOpenEnterAnimation">@anim/shared_x_axis_activity_open_enter</item>
-        <item name="android:activityOpenExitAnimation">@anim/shared_x_axis_activity_open_exit</item>
-        <item name="android:activityCloseEnterAnimation">@anim/shared_x_axis_activity_close_enter</item>
-        <item name="android:activityCloseExitAnimation">@anim/shared_x_axis_activity_close_exit</item>
-    </style>
-</resources>
\ No newline at end of file
diff --git a/res/values-vi/strings.xml b/res/values-vi/strings.xml
index 172c995..e5252b1 100644
--- a/res/values-vi/strings.xml
+++ b/res/values-vi/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Tiện ích bị vô hiệu hóa ở chế độ an toàn"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Lối tắt không khả dụng"</string>
     <string name="home_screen" msgid="5629429142036709174">"Màn hình chính"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Đặt <xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> làm trình chạy mặc định trong phần Cài đặt"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Chia đôi màn hình"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Thông tin ứng dụng cho %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Chế độ cài đặt mức sử dụng %1$s"</string>
diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml
index 9c658dc..7a76158 100644
--- a/res/values-zh-rCN/strings.xml
+++ b/res/values-zh-rCN/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"安全模式下不允许使用微件"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"无法使用快捷方式"</string>
     <string name="home_screen" msgid="5629429142036709174">"主屏幕"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"在“设置”中将“<xliff:g id="LAUNCHER_NAME">%1$s</xliff:g>”设为默认主屏幕应用"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"分屏"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$s 的应用信息"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"%1$s的使用设置"</string>
diff --git a/res/values-zh-rHK/strings.xml b/res/values-zh-rHK/strings.xml
index af32638..ed6e52f 100644
--- a/res/values-zh-rHK/strings.xml
+++ b/res/values-zh-rHK/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"在安全模式中無法使用小工具"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"沒有可用的捷徑"</string>
     <string name="home_screen" msgid="5629429142036709174">"主畫面"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"前往「設定」將「<xliff:g id="LAUNCHER_NAME">%1$s</xliff:g>」設為預設主畫面應用程式"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"分割螢幕"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$s 的應用程式資料"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"「%1$s」的用量設定"</string>
diff --git a/res/values-zh-rTW/strings.xml b/res/values-zh-rTW/strings.xml
index 54a1c5d..264d607 100644
--- a/res/values-zh-rTW/strings.xml
+++ b/res/values-zh-rTW/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"在安全模式下無法使用小工具"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"目前無法使用捷徑"</string>
     <string name="home_screen" msgid="5629429142036709174">"主畫面"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"前往「設定」將「<xliff:g id="LAUNCHER_NAME">%1$s</xliff:g>」設為預設主畫面應用程式"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"分割畫面"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"「%1$s」的應用程式資訊"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"「%1$s」的用量設定"</string>
diff --git a/res/values-zu/strings.xml b/res/values-zu/strings.xml
index 9752e18..be6cd49 100644
--- a/res/values-zu/strings.xml
+++ b/res/values-zu/strings.xml
@@ -27,8 +27,7 @@
     <string name="safemode_widget_error" msgid="4863470563535682004">"Amawijethi akhutshaziwe kwimodi yokuphepha"</string>
     <string name="shortcut_not_available" msgid="2536503539825726397">"Isinqamuleli asitholakali"</string>
     <string name="home_screen" msgid="5629429142036709174">"Ikhaya"</string>
-    <!-- no translation found for set_default_home_app (5808906607627586381) -->
-    <skip />
+    <string name="set_default_home_app" msgid="5808906607627586381">"Setha i-<xliff:g id="LAUNCHER_NAME">%1$s</xliff:g> njenge-app yasekhaya ezenzakalelayo Kumasethingi"</string>
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Hlukanisa isikrini"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"Ulwazi lwe-App ye-%1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Amasethingi okusetshenziswa ka-%1$s"</string>
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index be8b2e1..e4e047e 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -61,6 +61,7 @@
     <attr name="preloadIconAccentColor" format="color" />
     <attr name="preloadIconBackgroundColor" format="color" />
     <attr name="widgetPickerTitleColor" format="color"/>
+    <attr name="widgetPickerDescriptionColor" format="color"/>
     <attr name="widgetPickerPrimarySurfaceColor" format="color"/>
     <attr name="widgetPickerSecondarySurfaceColor" format="color"/>
     <attr name="widgetPickerHeaderAppTitleColor" format="color"/>
diff --git a/res/values/colors.xml b/res/values/colors.xml
index ce80964..8fa1992 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -104,6 +104,7 @@
     <color name="widget_picker_primary_surface_color_light">#EFEDED</color>
     <color name="widget_picker_secondary_surface_color_light">#FAF9F8</color>
     <color name="widget_picker_title_color_light">#1F1F1F</color>
+    <color name="widget_picker_description_color_light">#4C4D50</color>
     <color name="widget_picker_header_app_title_color_light">#1F1F1F</color>
     <color name="widget_picker_header_app_subtitle_color_light">#444746</color>
     <color name="widget_picker_header_background_color_light">#C2E7FF</color>
@@ -123,6 +124,7 @@
     <color name="widget_picker_primary_surface_color_dark">#1F2020</color>
     <color name="widget_picker_secondary_surface_color_dark">#393939</color>
     <color name="widget_picker_title_color_dark">#E3E3E3</color>
+    <color name="widget_picker_description_color_dark">#CCCDCF</color>
     <color name="widget_picker_header_app_title_color_dark">#E3E3E3</color>
     <color name="widget_picker_header_app_subtitle_color_dark">#C4C7C5</color>
     <color name="widget_picker_header_background_color_dark">#004A77</color>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index ae3d3b3..f7273a0 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -243,6 +243,7 @@
         <item name="widgetPickerSecondarySurfaceColor">
             @color/widget_picker_secondary_surface_color_light</item>
         <item name="widgetPickerTitleColor">@color/widget_picker_title_color_light</item>
+        <item name="widgetPickerDescriptionColor">@color/widget_picker_description_color_light</item>
         <item name="widgetPickerHeaderAppTitleColor">
             @color/widget_picker_header_app_title_color_light</item>
         <item name="widgetPickerHeaderAppSubtitleColor">
@@ -282,6 +283,7 @@
             @color/widget_picker_secondary_surface_color_dark</item>
         <item name="widgetPickerTitleColor">
             @color/widget_picker_title_color_dark</item>
+        <item name="widgetPickerDescriptionColor">@color/widget_picker_description_color_dark</item>
         <item name="widgetPickerHeaderAppTitleColor">
             @color/widget_picker_header_app_title_color_dark</item>
         <item name="widgetPickerHeaderAppSubtitleColor">
diff --git a/src/com/android/launcher3/AppWidgetResizeFrame.java b/src/com/android/launcher3/AppWidgetResizeFrame.java
index b51e850..ef56246 100644
--- a/src/com/android/launcher3/AppWidgetResizeFrame.java
+++ b/src/com/android/launcher3/AppWidgetResizeFrame.java
@@ -37,6 +37,8 @@
 import com.android.launcher3.celllayout.CellLayoutLayoutParams;
 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.dragndrop.DragLayer;
 import com.android.launcher3.keyboard.ViewGroupFocusHelper;
 import com.android.launcher3.logging.InstanceId;
@@ -221,6 +223,9 @@
         dl.addView(frame);
         frame.mIsOpen = true;
         frame.post(() -> frame.snapToWidget(false));
+        TestEventEmitter.INSTANCE.get(widget.getContext()).sendEvent(
+                TestEvent.RESIZE_FRAME_SHOWING
+        );
     }
 
     private void setCornerRadiusFromWidget() {
diff --git a/src/com/android/launcher3/FastScrollRecyclerView.java b/src/com/android/launcher3/FastScrollRecyclerView.java
index eff748a..960d77a 100644
--- a/src/com/android/launcher3/FastScrollRecyclerView.java
+++ b/src/com/android/launcher3/FastScrollRecyclerView.java
@@ -20,6 +20,7 @@
 
 import android.content.Context;
 import android.util.AttributeSet;
+import android.util.Log;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.accessibility.AccessibilityNodeInfo;
@@ -27,8 +28,8 @@
 import androidx.annotation.Nullable;
 import androidx.recyclerview.widget.RecyclerView;
 
-import com.android.app.animation.Interpolators;
 import com.android.launcher3.compat.AccessibilityManagerCompat;
+import com.android.launcher3.testing.shared.TestProtocol;
 import com.android.launcher3.views.RecyclerViewFastScroller;
 
 
@@ -188,6 +189,10 @@
      * Scrolls this recycler view to the top.
      */
     public void scrollToTop() {
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.PRIVATE_SPACE_SCROLL_FAILURE, "FastScrollRecyclerView#scrollToTop",
+                    new Exception());
+        }
         if (mScrollbar != null) {
             mScrollbar.reattachThumbToScroll();
         }
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index d905801..cb897dc 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -166,7 +166,6 @@
 import androidx.core.os.BuildCompat;
 import androidx.window.embedding.RuleController;
 
-import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.DropTarget.DragObject;
 import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
 import com.android.launcher3.allapps.ActivityAllAppsContainerView;
@@ -181,6 +180,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.dot.DotInfo;
 import com.android.launcher3.dragndrop.DragController;
 import com.android.launcher3.dragndrop.DragLayer;
@@ -595,6 +596,7 @@
             RuleController.getInstance(this).setRules(
                     RuleController.parseRules(this, R.xml.split_configuration));
         }
+        TestEventEmitter.INSTANCE.get(this).sendEvent(TestEvent.LAUNCHER_ON_CREATE);
     }
 
     protected ModelCallbacks createModelCallbacks() {
@@ -771,6 +773,7 @@
             // initialized properly.
             onSaveInstanceState(new Bundle());
             mModel.rebindCallbacks();
+            updateDisallowBack();
         } finally {
             Trace.endSection();
         }
diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java
index 239967d..85c8b57 100644
--- a/src/com/android/launcher3/LauncherAppState.java
+++ b/src/com/android/launcher3/LauncherAppState.java
@@ -24,6 +24,7 @@
 import static com.android.launcher3.LauncherPrefs.THEMED_ICONS;
 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;
 
@@ -63,6 +64,9 @@
 import com.android.launcher3.util.TraceHelper;
 import com.android.launcher3.widget.custom.CustomWidgetManager;
 
+import java.util.Locale;
+import java.util.Objects;
+
 public class LauncherAppState implements SafeCloseable {
 
     public static final String ACTION_FORCE_ROLOAD = "force-reload-launcher";
@@ -115,14 +119,25 @@
         }
 
         SimpleBroadcastReceiver modelChangeReceiver =
-                new SimpleBroadcastReceiver(mModel::onBroadcastIntent);
-        modelChangeReceiver.registerAsync(mContext, Intent.ACTION_LOCALE_CHANGED,
+                new SimpleBroadcastReceiver(UI_HELPER_EXECUTOR, mModel::onBroadcastIntent);
+        final Locale oldLocale = mContext.getResources().getConfiguration().locale;
+        modelChangeReceiver.register(
+                mContext,
+                () -> {
+                    // if local has changed before receiver is registered on bg thread,
+                    // mModel needs to reload.
+                    Locale newLocale = mContext.getResources().getConfiguration().locale;
+                    if (!Objects.equals(oldLocale, newLocale)) {
+                        mModel.forceReload();
+                    }
+                },
+                Intent.ACTION_LOCALE_CHANGED,
                 ACTION_DEVICE_POLICY_RESOURCE_UPDATED);
         if (BuildConfig.IS_STUDIO_BUILD) {
             mContext.registerReceiver(modelChangeReceiver, new IntentFilter(ACTION_FORCE_ROLOAD),
                     RECEIVER_EXPORTED);
         }
-        mOnTerminateCallback.add(() -> modelChangeReceiver.unregisterReceiverSafelyAsync(mContext));
+        mOnTerminateCallback.add(() -> modelChangeReceiver.unregisterReceiverSafely(mContext));
 
         SafeCloseable userChangeListener = UserCache.INSTANCE.get(mContext)
                 .addUserEventListener(mModel::onUserEvent);
diff --git a/src/com/android/launcher3/ModelCallbacks.kt b/src/com/android/launcher3/ModelCallbacks.kt
index 13062b6..83c34ce 100644
--- a/src/com/android/launcher3/ModelCallbacks.kt
+++ b/src/com/android/launcher3/ModelCallbacks.kt
@@ -11,6 +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.model.BgDataModel
 import com.android.launcher3.model.StringCache
 import com.android.launcher3.model.data.AppInfo
@@ -156,6 +158,7 @@
             /*pause=*/ false,
             deviceProfile.isTwoPanels
         )
+        TestEventEmitter.INSTANCE.get(launcher).sendEvent(TestEvent.WORKSPACE_FINISH_LOADING)
     }
 
     /**
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index 19a3002..a296f46 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -381,6 +381,28 @@
     }
 
     /**
+     * Scales a {@code RectF} in place about a specified pivot point.
+     *
+     * <p>This method modifies the given {@code RectF} directly to scale it proportionally
+     * by the given {@code scale}, while preserving its center at the specified
+     * {@code (pivotX, pivotY)} coordinates.
+     *
+     * @param rectF the {@code RectF} to scale, modified directly.
+     * @param pivotX the x-coordinate of the pivot point about which to scale.
+     * @param pivotY the y-coordinate of the pivot point about which to scale.
+     * @param scale the factor by which to scale the rectangle. Values less than 1 will
+     *                    shrink the rectangle, while values greater than 1 will enlarge it.
+     */
+    public static void scaleRectFAboutPivot(RectF rectF, float pivotX, float pivotY, float scale) {
+        rectF.offset(-pivotX, -pivotY);
+        rectF.left *= scale;
+        rectF.top *= scale;
+        rectF.right *= scale;
+        rectF.bottom *= scale;
+        rectF.offset(pivotX, pivotY);
+    }
+
+    /**
      * Maps t from one range to another range.
      * @param t The value to map.
      * @param fromMin The lower bound of the range that t is being mapped from.
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index e601a3e..2995e8a 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -80,6 +80,8 @@
 import com.android.launcher3.celllayout.CellPosMapper;
 import com.android.launcher3.celllayout.CellPosMapper.CellPos;
 import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.debug.TestEvent;
+import com.android.launcher3.debug.TestEventEmitter;
 import com.android.launcher3.dot.FolderDotInfo;
 import com.android.launcher3.dragndrop.DragController;
 import com.android.launcher3.dragndrop.DragLayer;
@@ -314,7 +316,6 @@
      */
     public Workspace(Context context, AttributeSet attrs, int defStyle) {
         super(context, attrs, defStyle);
-
         mLauncher = Launcher.getLauncher(context);
         mStateTransitionAnimation = new WorkspaceStateTransitionAnimation(mLauncher, this);
         mWallpaperManager = WallpaperManager.getInstance(context);
@@ -2218,6 +2219,7 @@
         if (d.stateAnnouncer != null && !droppedOnOriginalCell) {
             d.stateAnnouncer.completeAction(R.string.item_moved);
         }
+        TestEventEmitter.INSTANCE.get(getContext()).sendEvent(TestEvent.WORKSPACE_ON_DROP);
     }
 
     @Nullable
diff --git a/src/com/android/launcher3/allapps/AllAppsStore.java b/src/com/android/launcher3/allapps/AllAppsStore.java
index 9623709..89e6adc 100644
--- a/src/com/android/launcher3/allapps/AllAppsStore.java
+++ b/src/com/android/launcher3/allapps/AllAppsStore.java
@@ -42,6 +42,7 @@
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.function.Consumer;
@@ -260,8 +261,12 @@
     public void dump(String prefix, PrintWriter writer) {
         writer.println(prefix + "\tAllAppsStore Apps[] size: " + mApps.length);
         for (int i = 0; i < mApps.length; i++) {
-            writer.println(String.format("%s\tPackage index and name: %d/%s", prefix, i,
-                    mApps[i].componentName.getPackageName()));
+            writer.println(String.format(Locale.getDefault(),
+                    "%s\tPackage index, name, and class: " + "%d/%s:%s",
+                    prefix,
+                    i,
+                    mApps[i].componentName.getPackageName(),
+                    mApps[i].componentName.getClassName()));
         }
     }
 }
diff --git a/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java b/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java
index 98ca420..4b38df8 100644
--- a/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java
+++ b/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java
@@ -26,6 +26,7 @@
 import static com.android.launcher3.allapps.UserProfileManager.STATE_ENABLED;
 
 import android.content.Context;
+import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.View.OnClickListener;
@@ -278,6 +279,13 @@
                                     privateProfileManager.getReadyToAnimate())
                                 && privateProfileManager.getCurrentState() == STATE_ENABLED
                                 ? 0 : 1);
+                        Log.d(TAG, "onBindViewHolder: "
+                                + "isPrivateSpaceItem: " + isPrivateSpaceItem
+                        + " isStateTransitioning: " + privateProfileManager.isStateTransitioning()
+                        + " isScrolling: " + privateProfileManager.isScrolling()
+                        + " readyToAnimate: " + privateProfileManager.getReadyToAnimate()
+                        + " currentState: " + privateProfileManager.getCurrentState()
+                        + " currentAlpha: " + icon.getAlpha());
                     }
                     // Views can still be bounded before the app list is updated hence showing icons
                     // after collapsing.
diff --git a/src/com/android/launcher3/allapps/PrivateProfileManager.java b/src/com/android/launcher3/allapps/PrivateProfileManager.java
index 6f021ea..0f4204f 100644
--- a/src/com/android/launcher3/allapps/PrivateProfileManager.java
+++ b/src/com/android/launcher3/allapps/PrivateProfileManager.java
@@ -48,6 +48,7 @@
 import android.content.Intent;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.util.Log;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.ImageView;
@@ -89,6 +90,8 @@
  * logic in the Personal tab.
  */
 public class PrivateProfileManager extends UserProfileManager {
+
+    private static final String TAG = "PrivateProfileManager";
     private static final int EXPAND_COLLAPSE_DURATION = 800;
     private static final int SETTINGS_OPACITY_DURATION = 400;
     private static final int TEXT_UNLOCK_OPACITY_DURATION = 300;
@@ -362,6 +365,7 @@
         } else {
             // Ensure any unwanted animations to not happen.
             settingAndLockGroup.setLayoutTransition(null);
+            Log.d(TAG, "bindPrivateSpaceHeaderViewElements: removing transitions ");
         }
         updateView();
     }
@@ -597,6 +601,9 @@
         }
         attachFloatingMaskView(expand);
         ViewGroup settingsAndLockGroup = mPSHeader.findViewById(R.id.settingsAndLockGroup);
+        TextView lockText = mPSHeader.findViewById(R.id.lock_text);
+        PrivateSpaceSettingsButton privateSpaceSettingsButton =
+                mPSHeader.findViewById(R.id.ps_settings_button);
         if (settingsAndLockGroup.getLayoutTransition() == null) {
             // Set a new transition if the current ViewGroup does not already contain one as each
             // transition should only happen once when applied.
@@ -612,13 +619,15 @@
         animatorSet.addListener(new AnimatorListenerAdapter() {
             @Override
             public void onAnimationStart(Animator animation) {
+                Log.d(TAG, "updatePrivateStateAnimator: Private space animation expanding: "
+                        + expand);
                 mStatsLogManager.logger().sendToInteractionJankMonitor(
                         expand
                                 ? LAUNCHER_PRIVATE_SPACE_UNLOCK_ANIMATION_BEGIN
                                 : LAUNCHER_PRIVATE_SPACE_LOCK_ANIMATION_BEGIN,
                         mAllApps.getActiveRecyclerView());
                 // Animate the collapsing of the text at the same time while updating lock button.
-                mPSHeader.findViewById(R.id.lock_text).setVisibility(expand ? VISIBLE : GONE);
+                lockText.setVisibility(expand ? VISIBLE : GONE);
                 setAnimationRunning(true);
             }
 
@@ -636,6 +645,11 @@
                             ? LAUNCHER_PRIVATE_SPACE_UNLOCK_ANIMATION_END
                             : LAUNCHER_PRIVATE_SPACE_LOCK_ANIMATION_END,
                     mAllApps.getActiveRecyclerView());
+            Log.d(TAG, "updatePrivateStateAnimator: lockText visibility: "
+                    + lockText.getVisibility() + " lockTextAlpha: " + lockText.getAlpha());
+            Log.d(TAG, "updatePrivateStateAnimator: settingsCog visibility: "
+                    + privateSpaceSettingsButton.getVisibility()
+                    + " settingsCogAlpha: " + privateSpaceSettingsButton.getAlpha());
             if (!expand) {
                 mAllApps.mAH.get(MAIN).mRecyclerView.removeItemDecoration(
                         mPrivateAppsSectionDecorator);
@@ -717,15 +731,19 @@
             @Override
             public void startTransition(LayoutTransition transition, ViewGroup viewGroup,
                     View view, int i) {
+                Log.d(TAG, "updatePrivateStateAnimator: transition started: " + transition);
             }
             @Override
             public void endTransition(LayoutTransition transition, ViewGroup viewGroup,
                     View view, int i) {
                 settingsAndLockGroup.setLayoutTransition(null);
                 mReadyToAnimate = false;
+                Log.d(TAG, "updatePrivateStateAnimator: transition finished: " + transition);
             }
         });
         settingsAndLockGroup.setLayoutTransition(settingsAndLockTransition);
+        Log.d(TAG, "updatePrivateStateAnimator: setting transition: "
+                + settingsAndLockTransition);
     }
 
     /** Change the settings gear alpha when expanded or collapsed. */
diff --git a/src/com/android/launcher3/anim/AnimatedFloat.java b/src/com/android/launcher3/anim/AnimatedFloat.java
index b414ab6..4441164 100644
--- a/src/com/android/launcher3/anim/AnimatedFloat.java
+++ b/src/com/android/launcher3/anim/AnimatedFloat.java
@@ -20,6 +20,8 @@
 import android.animation.ObjectAnimator;
 import android.util.FloatProperty;
 
+import java.util.function.Consumer;
+
 /**
  * A mutable float which allows animating the value
  */
@@ -38,9 +40,9 @@
                 }
             };
 
-    private static final Runnable NO_OP = () -> { };
+    private static final Consumer<Float> NO_OP = t -> { };
 
-    private final Runnable mUpdateCallback;
+    private final Consumer<Float> mUpdateCallback;
     private ObjectAnimator mValueAnimator;
     // Only non-null when an animation is playing to this value.
     private Float mEndValue;
@@ -52,6 +54,10 @@
     }
 
     public AnimatedFloat(Runnable updateCallback) {
+        this(v -> updateCallback.run());
+    }
+
+    public AnimatedFloat(Consumer<Float> updateCallback) {
         mUpdateCallback = updateCallback;
     }
 
@@ -60,6 +66,11 @@
         value = initialValue;
     }
 
+    public AnimatedFloat(Consumer<Float> updateCallback, float initialValue) {
+        this(updateCallback);
+        value = initialValue;
+    }
+
     /**
      * Returns an animation from the current value to the given value.
      */
@@ -99,7 +110,7 @@
     public void updateValue(float v) {
         if (Float.compare(v, value) != 0) {
             value = v;
-            mUpdateCallback.run();
+            mUpdateCallback.accept(value);
         }
     }
 
diff --git a/src/com/android/launcher3/anim/PendingAnimation.java b/src/com/android/launcher3/anim/PendingAnimation.java
index e58890f..47a2bdd 100644
--- a/src/com/android/launcher3/anim/PendingAnimation.java
+++ b/src/com/android/launcher3/anim/PendingAnimation.java
@@ -59,6 +59,13 @@
         add(anim, springProperty);
     }
 
+    /**
+     * Utility method to sent an interpolator on an animation and add it to the list
+     */
+    public void add(Animator anim, TimeInterpolator interpolator) {
+        add(anim, interpolator, SpringProperty.DEFAULT);
+    }
+
     @Override
     public void add(Animator anim) {
         add(anim, SpringProperty.DEFAULT);
diff --git a/src/com/android/launcher3/apppairs/AppPairIcon.java b/src/com/android/launcher3/apppairs/AppPairIcon.java
index 32445ec..870c876 100644
--- a/src/com/android/launcher3/apppairs/AppPairIcon.java
+++ b/src/com/android/launcher3/apppairs/AppPairIcon.java
@@ -18,10 +18,12 @@
 
 import static com.android.launcher3.BubbleTextView.DISPLAY_FOLDER;
 
+import android.animation.ObjectAnimator;
 import android.content.Context;
 import android.graphics.Paint;
 import android.graphics.Rect;
 import android.util.AttributeSet;
+import android.util.FloatProperty;
 import android.view.LayoutInflater;
 import android.view.ViewGroup;
 import android.widget.FrameLayout;
@@ -54,6 +56,26 @@
 public class AppPairIcon extends FrameLayout implements DraggableView, Reorderable {
     private static final String TAG = "AppPairIcon";
 
+    // The duration of the scaling animation on hover enter/exit.
+    private static final int HOVER_SCALE_DURATION = 150;
+    // The default scale of the icon when not hovered.
+    private static final Float HOVER_SCALE_DEFAULT = 1f;
+    // The max scale of the icon when hovered.
+    private static final Float HOVER_SCALE_MAX = 1.1f;
+    // Animates the scale of the icon background on hover.
+    private static final FloatProperty<AppPairIcon> HOVER_SCALE_PROPERTY =
+            new FloatProperty<>("hoverScale") {
+                @Override
+                public void setValue(AppPairIcon view, float scale) {
+                    view.mIconGraphic.setHoverScale(scale);
+                }
+
+                @Override
+                public Float get(AppPairIcon view) {
+                    return view.mIconGraphic.getHoverScale();
+                }
+            };
+
     // A view that holds the app pair icon graphic.
     private AppPairIconGraphic mIconGraphic;
     // A view that holds the app pair's title.
@@ -250,4 +272,14 @@
         }
         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
     }
+
+    @Override
+    public void onHoverChanged(boolean hovered) {
+        super.onHoverChanged(hovered);
+        ObjectAnimator
+                .ofFloat(this, HOVER_SCALE_PROPERTY,
+                        hovered ? HOVER_SCALE_MAX : HOVER_SCALE_DEFAULT)
+                .setDuration(HOVER_SCALE_DURATION)
+                .start();
+    }
 }
diff --git a/src/com/android/launcher3/apppairs/AppPairIconDrawable.java b/src/com/android/launcher3/apppairs/AppPairIconDrawable.java
index db83d91..114ed2e 100644
--- a/src/com/android/launcher3/apppairs/AppPairIconDrawable.java
+++ b/src/com/android/launcher3/apppairs/AppPairIconDrawable.java
@@ -26,6 +26,7 @@
 
 import androidx.annotation.NonNull;
 
+import com.android.launcher3.Utilities;
 import com.android.launcher3.icons.FastBitmapDrawable;
 
 /**
@@ -128,6 +129,18 @@
                 height - (mP.getStandardIconPadding() + mP.getOuterPadding())
         );
 
+        // Scale each background from its center edge closest to the center channel.
+        Utilities.scaleRectFAboutPivot(
+                leftSide,
+                leftSide.left + leftSide.width(),
+                leftSide.top + leftSide.centerY(),
+                mP.getHoverScale());
+        Utilities.scaleRectFAboutPivot(
+                rightSide,
+                rightSide.left,
+                rightSide.top + rightSide.centerY(),
+                mP.getHoverScale());
+
         drawCustomRoundedRect(canvas, leftSide, new float[]{
                 mP.getBigRadius(), mP.getBigRadius(),
                 mP.getSmallRadius(), mP.getSmallRadius(),
@@ -163,6 +176,18 @@
                 height - (mP.getStandardIconPadding() + mP.getOuterPadding())
         );
 
+        // Scale each background from its center edge closest to the center channel.
+        Utilities.scaleRectFAboutPivot(
+                topSide,
+                topSide.left + topSide.centerX(),
+                topSide.top + topSide.height(),
+                mP.getHoverScale());
+        Utilities.scaleRectFAboutPivot(
+                bottomSide,
+                bottomSide.left + bottomSide.centerX(),
+                bottomSide.top,
+                mP.getHoverScale());
+
         drawCustomRoundedRect(canvas, topSide, new float[]{
                 mP.getBigRadius(), mP.getBigRadius(),
                 mP.getBigRadius(), mP.getBigRadius(),
diff --git a/src/com/android/launcher3/apppairs/AppPairIconDrawingParams.kt b/src/com/android/launcher3/apppairs/AppPairIconDrawingParams.kt
index 45dc013..5b546d6 100644
--- a/src/com/android/launcher3/apppairs/AppPairIconDrawingParams.kt
+++ b/src/com/android/launcher3/apppairs/AppPairIconDrawingParams.kt
@@ -64,6 +64,8 @@
     var isLeftRightSplit: Boolean = true
     // The background paint color (based on container).
     var bgColor: Int = 0
+    // The scale of the icon background while hovered.
+    var hoverScale: Float = 1f
 
     init {
         val activity: ActivityContext = ActivityContext.lookupContext(context)
diff --git a/src/com/android/launcher3/apppairs/AppPairIconGraphic.kt b/src/com/android/launcher3/apppairs/AppPairIconGraphic.kt
index dce97eb..034b686 100644
--- a/src/com/android/launcher3/apppairs/AppPairIconGraphic.kt
+++ b/src/com/android/launcher3/apppairs/AppPairIconGraphic.kt
@@ -139,4 +139,19 @@
         super.dispatchDraw(canvas)
         drawable.draw(canvas)
     }
+
+    /**
+     * Sets the scale of the icon background while hovered.
+     */
+    fun setHoverScale(scale: Float) {
+        drawParams.hoverScale = scale
+        redraw()
+    }
+
+    /**
+     * Gets the scale of the icon background while hovered.
+     */
+    fun getHoverScale(): Float {
+        return drawParams.hoverScale
+    }
 }
diff --git a/src/com/android/launcher3/debug/TestEventsEmitterProduction.kt b/src/com/android/launcher3/debug/TestEventsEmitterProduction.kt
new file mode 100644
index 0000000..650df5a
--- /dev/null
+++ b/src/com/android/launcher3/debug/TestEventsEmitterProduction.kt
@@ -0,0 +1,54 @@
+/*
+ * 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 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"),
+}
+
+/** 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) {}
+}
diff --git a/src/com/android/launcher3/dragndrop/DragController.java b/src/com/android/launcher3/dragndrop/DragController.java
index bc5a164..c50c008 100644
--- a/src/com/android/launcher3/dragndrop/DragController.java
+++ b/src/com/android/launcher3/dragndrop/DragController.java
@@ -27,6 +27,7 @@
 import android.view.View;
 
 import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
 
 import com.android.app.animation.Interpolators;
 import com.android.launcher3.DragSource;
@@ -69,8 +70,9 @@
      */
     protected DragDriver mDragDriver = null;
 
+    @VisibleForTesting
     /** Options controlling the drag behavior. */
-    protected DragOptions mOptions;
+    public DragOptions mOptions;
 
     /** Coordinate for motion down event */
     protected final Point mMotionDown = new Point();
@@ -79,7 +81,8 @@
 
     protected final Point mTmpPoint = new Point();
 
-    protected DropTarget.DragObject mDragObject;
+    @VisibleForTesting
+    public DropTarget.DragObject mDragObject;
 
     /** Who can receive drop events */
     private final ArrayList<DropTarget> mDropTargets = new ArrayList<>();
diff --git a/src/com/android/launcher3/dragndrop/SpringLoadedDragController.java b/src/com/android/launcher3/dragndrop/SpringLoadedDragController.java
index fbe9e33..bebef70 100644
--- a/src/com/android/launcher3/dragndrop/SpringLoadedDragController.java
+++ b/src/com/android/launcher3/dragndrop/SpringLoadedDragController.java
@@ -26,7 +26,7 @@
 public class SpringLoadedDragController implements OnAlarmListener {
     // how long the user must hover over a mini-screen before it unshrinks
     private static final long ENTER_SPRING_LOAD_HOVER_TIME = 500;
-    private static final long ENTER_SPRING_LOAD_HOVER_TIME_IN_TEST = 2000;
+    private static final long ENTER_SPRING_LOAD_HOVER_TIME_IN_TEST = 3000;
     private static final long ENTER_SPRING_LOAD_CANCEL_HOVER_TIME = 950;
 
     Alarm mAlarm;
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index dcc55e6..d3c1a02 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -65,6 +65,7 @@
 
 import androidx.annotation.IntDef;
 import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
 import androidx.core.content.res.ResourcesCompat;
 
 import com.android.launcher3.AbstractFloatingView;
@@ -1693,6 +1694,11 @@
         return windowBottomPx - folderBottomPx;
     }
 
+    @VisibleForTesting
+    public boolean getDeleteFolderOnDropCompleted() {
+        return mDeleteFolderOnDropCompleted;
+    }
+
     /**
      * Save this listener for the special case of when we update the state and concurrently
      * add another listener to {@link #mOnFolderStateChangedListeners} to avoid a
diff --git a/src/com/android/launcher3/folder/FolderAnimationManager.java b/src/com/android/launcher3/folder/FolderAnimationManager.java
index 9824992..37a8d9b 100644
--- a/src/com/android/launcher3/folder/FolderAnimationManager.java
+++ b/src/com/android/launcher3/folder/FolderAnimationManager.java
@@ -60,6 +60,7 @@
  */
 public class FolderAnimationManager {
 
+    private static final float EXTRA_FOLDER_REVEAL_RADIUS_PERCENTAGE = 0.125F;
     private static final int FOLDER_NAME_ALPHA_DURATION = 32;
     private static final int LARGE_FOLDER_FOOTER_DURATION = 128;
 
@@ -158,12 +159,9 @@
         mFolder.mFooter.setPivotX(0);
         mFolder.mFooter.setPivotY(0);
 
-        // We want to create a small X offset for the preview items, so that they follow their
-        // expected path to their final locations. ie. an icon should not move right, if it's final
-        // location is to its left. This value is arbitrarily defined.
-        int previewItemOffsetX = (int) (previewSize / 2);
+        int previewItemOffsetX = 0;
         if (Utilities.isRtl(mContext.getResources())) {
-            previewItemOffsetX = (int) (lp.width * initialScale - initialSize - previewItemOffsetX);
+            previewItemOffsetX = (int) (lp.width * initialScale - initialSize);
         }
 
         final int paddingOffsetX = (int) (mContent.getPaddingLeft() * initialScale);
@@ -239,29 +237,19 @@
         play(a, shapeDelegate.createRevealAnimator(
                 mFolder, startRect, endRect, finalRadius, !mIsOpening));
 
-        // Create reveal animator for the folder content (capture the top 4 icons 2x2)
-        int width = mDeviceProfile.folderCellLayoutBorderSpacePx.x
-                + mDeviceProfile.folderCellWidthPx * 2;
-        int rtlExtraWidth = 0;
-        int height = mDeviceProfile.folderCellLayoutBorderSpacePx.y
-                + mDeviceProfile.folderCellHeightPx * 2;
         int page = mIsOpening ? mContent.getCurrentPage() : mContent.getDestinationPage();
-        // In RTL we want to move to the last 2 columns of icons in the folder.
         if (Utilities.isRtl(mContext.getResources())) {
             page = (mContent.getPageCount() - 1) - page;
-            CellLayout clAtPage = mContent.getPageAt(page);
-            if (clAtPage != null) {
-                int numExtraRows = clAtPage.getCountX() - 2;
-                rtlExtraWidth = (int) Math.max(numExtraRows * (mDeviceProfile.folderCellWidthPx
-                        + mDeviceProfile.folderCellLayoutBorderSpacePx.x), rtlExtraWidth);
-            }
         }
-        int left = mContent.getPaddingLeft() + page * lp.width;
+        int left = page * lp.width;
+
+        int extraRadius = (int) ((mDeviceProfile.folderIconSizePx / initialScale)
+                * EXTRA_FOLDER_REVEAL_RADIUS_PERCENTAGE);
         Rect contentStart = new Rect(
-                left + rtlExtraWidth,
-                0,
-                left + width + mContent.getPaddingRight() + rtlExtraWidth,
-                height);
+                (int) (left + (startRect.left / initialScale)) - extraRadius,
+                (int) (startRect.top / initialScale) - extraRadius,
+                (int) (left + (startRect.right / initialScale)) + extraRadius,
+                (int) (startRect.bottom / initialScale) + extraRadius);
         Rect contentEnd = new Rect(left, 0, left + lp.width, lp.height);
         play(a, shapeDelegate.createRevealAnimator(
                 mFolder.getContent(), contentStart, contentEnd, finalRadius, !mIsOpening));
diff --git a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
index ae8f1d5..6088941 100644
--- a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
+++ b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
@@ -508,6 +508,7 @@
                 && !SHOULD_SHOW_FIRST_PAGE_WIDGET) {
             CellLayout firstScreen = mWorkspaceScreens.get(FIRST_SCREEN_ID);
             View qsb = mHomeElementInflater.inflate(R.layout.qsb_preview, firstScreen, false);
+            // TODO: set bgHandler on qsb when it is BaseTemplateCard, which requires API changes.
             CellLayoutLayoutParams lp = new CellLayoutLayoutParams(
                     0, 0, firstScreen.getCountX(), 1);
             lp.canReorder = false;
diff --git a/src/com/android/launcher3/logging/StatsLogManager.java b/src/com/android/launcher3/logging/StatsLogManager.java
index 861631d..fbd24d8 100644
--- a/src/com/android/launcher3/logging/StatsLogManager.java
+++ b/src/com/android/launcher3/logging/StatsLogManager.java
@@ -795,6 +795,9 @@
         @UiEvent(doc = "User launches Overview from meta+tab keyboard shortcut")
         LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_KEYBOARD_SHORTCUT(1765),
 
+        @UiEvent(doc = "User long pressed on the taskbar IME switcher button")
+        LAUNCHER_TASKBAR_IME_SWITCHER_BUTTON_LONGPRESS(1798),
+
         // ADD MORE
         ;
 
diff --git a/src/com/android/launcher3/model/data/TaskItemInfo.kt b/src/com/android/launcher3/model/data/TaskItemInfo.kt
new file mode 100644
index 0000000..fc1cd4d
--- /dev/null
+++ b/src/com/android/launcher3/model/data/TaskItemInfo.kt
@@ -0,0 +1,24 @@
+/*
+ * 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.model.data
+
+/**
+ * Temporary class holding a Task ID to allow us to reference a Task when clicking a hotseat item.
+ *
+ * TODO(b/315344726): Remove this class when we have proper Taskbar support for multi-instance apps
+ */
+class TaskItemInfo(val taskId: Int, itemInfo: WorkspaceItemInfo) : WorkspaceItemInfo(itemInfo)
diff --git a/src/com/android/launcher3/pm/UserCache.java b/src/com/android/launcher3/pm/UserCache.java
index cf03462..7339111 100644
--- a/src/com/android/launcher3/pm/UserCache.java
+++ b/src/com/android/launcher3/pm/UserCache.java
@@ -75,7 +75,7 @@
 
     private final List<BiConsumer<UserHandle, String>> mUserEventListeners = new ArrayList<>();
     private final SimpleBroadcastReceiver mUserChangeReceiver =
-            new SimpleBroadcastReceiver(this::onUsersChanged);
+            new SimpleBroadcastReceiver(MODEL_EXECUTOR, this::onUsersChanged);
 
     private final Context mContext;
 
@@ -93,12 +93,12 @@
 
     @Override
     public void close() {
-        MODEL_EXECUTOR.execute(() -> mUserChangeReceiver.unregisterReceiverSafelySync(mContext));
+        MODEL_EXECUTOR.execute(() -> mUserChangeReceiver.unregisterReceiverSafely(mContext));
     }
 
     @WorkerThread
     private void initAsync() {
-        mUserChangeReceiver.registerSync(mContext,
+        mUserChangeReceiver.register(mContext,
                 Intent.ACTION_MANAGED_PROFILE_AVAILABLE,
                 Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE,
                 Intent.ACTION_MANAGED_PROFILE_REMOVED,
diff --git a/src/com/android/launcher3/util/DisplayController.java b/src/com/android/launcher3/util/DisplayController.java
index 3dcc663..b1e82bb 100644
--- a/src/com/android/launcher3/util/DisplayController.java
+++ b/src/com/android/launcher3/util/DisplayController.java
@@ -49,6 +49,7 @@
 import android.view.Display;
 
 import androidx.annotation.AnyThread;
+import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
 import androidx.annotation.VisibleForTesting;
 
@@ -76,6 +77,7 @@
 
     private static final String TAG = "DisplayController";
     private static final boolean DEBUG = false;
+    private static boolean sTaskbarModePreferenceStatusForTests = false;
     private static boolean sTransientTaskbarStatusForTests = true;
 
     // TODO(b/254119092) remove all logs with this tag
@@ -109,7 +111,10 @@
     private DisplayInfoChangeListener mPriorityListener;
     private final ArrayList<DisplayInfoChangeListener> mListeners = new ArrayList<>();
 
-    private final SimpleBroadcastReceiver mReceiver = new SimpleBroadcastReceiver(this::onIntent);
+    // 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 Info mInfo;
     private boolean mDestroyed = false;
@@ -132,11 +137,11 @@
             mWindowContext.registerComponentCallbacks(this);
         } else {
             mWindowContext = null;
-            mReceiver.registerAsync(mContext, ACTION_CONFIGURATION_CHANGED);
+            mReceiver.register(mContext, ACTION_CONFIGURATION_CHANGED);
         }
 
         // Initialize navigation mode change listener
-        mReceiver.registerPkgActionsAsync(mContext, TARGET_OVERLAY_PACKAGE, ACTION_OVERLAY_CHANGED);
+        mReceiver.registerPkgActions(mContext, TARGET_OVERLAY_PACKAGE, ACTION_OVERLAY_CHANGED);
 
         WindowManagerProxy wmProxy = WindowManagerProxy.INSTANCE.get(context);
         Context displayInfoContext = getDisplayInfoContext(display);
@@ -203,6 +208,14 @@
     }
 
     /**
+     * Enables respecting taskbar mode preference during test.
+     */
+    @VisibleForTesting
+    public static void enableTaskbarModePreferenceForTests(boolean enable) {
+        sTaskbarModePreferenceStatusForTests = enable;
+    }
+
+    /**
      * Returns whether the taskbar is pinned in gesture navigation mode.
      */
     public static boolean isPinnedTaskbar(Context context) {
@@ -223,7 +236,7 @@
         } else {
             // TODO: unregister broadcast receiver
         }
-        mReceiver.unregisterReceiverSafelyAsync(mContext);
+        mReceiver.unregisterReceiverSafely(mContext);
     }
 
     /**
@@ -461,7 +474,7 @@
             if (navigationMode != NavigationMode.NO_BUTTON) {
                 return false;
             }
-            if (Utilities.isRunningInTestHarness()) {
+            if (Utilities.isRunningInTestHarness() && !sTaskbarModePreferenceStatusForTests) {
                 // TODO(b/258604917): Once ENABLE_TASKBAR_PINNING is enabled, remove usage of
                 //  sTransientTaskbarStatusForTests and update test to directly
                 //  toggle shared preference to switch transient taskbar on/off.
@@ -513,9 +526,8 @@
             return Collections.unmodifiableSet(mPerDisplayBounds.keySet());
         }
 
-        /**
-         * Returns all {@link WindowBounds}s for the current display.
-         */
+        /** Returns all {@link WindowBounds}s for the current display. */
+        @Nullable
         public List<WindowBounds> getCurrentBounds() {
             return mPerDisplayBounds.get(normalizedDisplayInfo);
         }
diff --git a/src/com/android/launcher3/util/LockedUserState.kt b/src/com/android/launcher3/util/LockedUserState.kt
index 2737249..10559f3 100644
--- a/src/com/android/launcher3/util/LockedUserState.kt
+++ b/src/com/android/launcher3/util/LockedUserState.kt
@@ -20,21 +20,28 @@
 import android.os.Process
 import android.os.UserManager
 import androidx.annotation.VisibleForTesting
+import com.android.launcher3.util.Executors.MAIN_EXECUTOR
+import com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR
 
 class LockedUserState(private val mContext: Context) : SafeCloseable {
     val isUserUnlockedAtLauncherStartup: Boolean
-    var isUserUnlocked: Boolean
-        private set
+    var isUserUnlocked = false
+        private set(value) {
+            field = value
+            if (value) {
+                notifyUserUnlocked()
+            }
+        }
 
     private val mUserUnlockedActions: RunnableList = RunnableList()
 
     @VisibleForTesting
-    val mUserUnlockedReceiver = SimpleBroadcastReceiver {
-        if (Intent.ACTION_USER_UNLOCKED == it.action) {
-            isUserUnlocked = true
-            notifyUserUnlocked()
+    val mUserUnlockedReceiver =
+        SimpleBroadcastReceiver(UI_HELPER_EXECUTOR) {
+            if (Intent.ACTION_USER_UNLOCKED == it.action) {
+                isUserUnlocked = true
+            }
         }
-    }
 
     init {
         // 1) when user reboots devices, launcher process starts at lock screen and both
@@ -43,26 +50,34 @@
         // yet isUserUnlockedAtLauncherStartup will remains as false.
         // 2) when launcher process restarts after user has unlocked screen, both variable are
         // init as true and will not change.
-        isUserUnlocked =
-            mContext
-                .getSystemService(UserManager::class.java)!!
-                .isUserUnlocked(Process.myUserHandle())
+        isUserUnlocked = checkIsUserUnlocked()
         isUserUnlockedAtLauncherStartup = isUserUnlocked
-        if (isUserUnlocked) {
-            notifyUserUnlocked()
-        } else {
-            mUserUnlockedReceiver.registerAsync(mContext, Intent.ACTION_USER_UNLOCKED)
+        if (!isUserUnlocked) {
+            mUserUnlockedReceiver.register(
+                mContext,
+                {
+                    // If user is unlocked while registering broadcast receiver, we should update
+                    // [isUserUnlocked], which will call [notifyUserUnlocked] in setter
+                    if (checkIsUserUnlocked()) {
+                        MAIN_EXECUTOR.execute { isUserUnlocked = true }
+                    }
+                },
+                Intent.ACTION_USER_UNLOCKED
+            )
         }
     }
 
+    private fun checkIsUserUnlocked() =
+        mContext.getSystemService(UserManager::class.java)!!.isUserUnlocked(Process.myUserHandle())
+
     private fun notifyUserUnlocked() {
         mUserUnlockedActions.executeAllAndDestroy()
-        mUserUnlockedReceiver.unregisterReceiverSafelyAsync(mContext)
+        mUserUnlockedReceiver.unregisterReceiverSafely(mContext)
     }
 
     /** Stops the receiver from listening for ACTION_USER_UNLOCK broadcasts. */
     override fun close() {
-        mUserUnlockedReceiver.unregisterReceiverSafelyAsync(mContext)
+        mUserUnlockedReceiver.unregisterReceiverSafely(mContext)
     }
 
     /**
diff --git a/src/com/android/launcher3/util/ScreenOnTracker.java b/src/com/android/launcher3/util/ScreenOnTracker.java
index c1d192c..12eff61 100644
--- a/src/com/android/launcher3/util/ScreenOnTracker.java
+++ b/src/com/android/launcher3/util/ScreenOnTracker.java
@@ -19,6 +19,8 @@
 import static android.content.Intent.ACTION_SCREEN_ON;
 import static android.content.Intent.ACTION_USER_PRESENT;
 
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+
 import android.content.Context;
 import android.content.Intent;
 
@@ -32,7 +34,8 @@
     public static final MainThreadInitializedObject<ScreenOnTracker> INSTANCE =
             new MainThreadInitializedObject<>(ScreenOnTracker::new);
 
-    private final SimpleBroadcastReceiver mReceiver = new SimpleBroadcastReceiver(this::onReceive);
+    private final SimpleBroadcastReceiver mReceiver =
+            new SimpleBroadcastReceiver(UI_HELPER_EXECUTOR, this::onReceive);
     private final CopyOnWriteArrayList<ScreenOnListener> mListeners = new CopyOnWriteArrayList<>();
 
     private final Context mContext;
@@ -42,12 +45,12 @@
         // Assume that the screen is on to begin with
         mContext = context;
         mIsScreenOn = true;
-        mReceiver.registerAsync(context, ACTION_SCREEN_ON, ACTION_SCREEN_OFF, ACTION_USER_PRESENT);
+        mReceiver.register(context, ACTION_SCREEN_ON, ACTION_SCREEN_OFF, ACTION_USER_PRESENT);
     }
 
     @Override
     public void close() {
-        mReceiver.unregisterReceiverSafelyAsync(mContext);
+        mReceiver.unregisterReceiverSafely(mContext);
     }
 
     private void onReceive(Intent intent) {
diff --git a/src/com/android/launcher3/util/SimpleBroadcastReceiver.java b/src/com/android/launcher3/util/SimpleBroadcastReceiver.java
index 5f39cce..539a7cb 100644
--- a/src/com/android/launcher3/util/SimpleBroadcastReceiver.java
+++ b/src/com/android/launcher3/util/SimpleBroadcastReceiver.java
@@ -15,21 +15,17 @@
  */
 package com.android.launcher3.util;
 
-import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
-
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.os.Handler;
 import android.os.Looper;
 import android.os.PatternMatcher;
 import android.text.TextUtils;
 
+import androidx.annotation.AnyThread;
 import androidx.annotation.Nullable;
-import androidx.annotation.UiThread;
-import androidx.annotation.WorkerThread;
-
-import com.android.launcher3.BuildConfig;
 
 import java.util.function.Consumer;
 
@@ -37,8 +33,16 @@
 
     private final Consumer<Intent> mIntentConsumer;
 
-    public SimpleBroadcastReceiver(Consumer<Intent> intentConsumer) {
+    // Handler to register/unregister broadcast receiver
+    private final Handler mHandler;
+
+    public SimpleBroadcastReceiver(LooperExecutor looperExecutor, Consumer<Intent> intentConsumer) {
+        this(looperExecutor.getHandler(), intentConsumer);
+    }
+
+    public SimpleBroadcastReceiver(Handler handler, Consumer<Intent> intentConsumer) {
         mIntentConsumer = intentConsumer;
+        mHandler = handler;
     }
 
     @Override
@@ -46,55 +50,104 @@
         mIntentConsumer.accept(intent);
     }
 
-    /** Helper method to register multiple actions. Caller should be on main thread. */
-    @UiThread
-    public void registerAsync(Context context, String... actions) {
-        assertOnMainThread();
-        UI_HELPER_EXECUTOR.execute(() -> registerSync(context, actions));
+    /** Calls {@link #register(Context, Runnable, String...)} with null completionCallback. */
+    @AnyThread
+    public void register(Context context, String... actions) {
+        register(context, null, actions);
     }
 
-    /** Helper method to register multiple actions. Caller should be on main thread. */
-    @WorkerThread
-    public void registerSync(Context context, String... actions) {
-        assertOnBgThread();
+    /**
+     * Calls {@link #register(Context, Runnable, int, String...)} with null completionCallback.
+     */
+    @AnyThread
+    public void register(Context context, int flags, String... actions) {
+        register(context, null, flags, actions);
+    }
+
+    /**
+     * Register broadcast receiver. If this method is called on the same looper with mHandler's
+     * looper, then register will be called synchronously. Otherwise asynchronously. This ensures
+     * register happens on {@link #mHandler}'s looper.
+     *
+     * @param completionCallback callback that will be triggered after registration is completed,
+     *                           caller usually pass this callback to check if states has changed
+     *                           while registerReceiver() is executed on a binder call.
+     */
+    @AnyThread
+    public void register(
+            Context context, @Nullable Runnable completionCallback, String... actions) {
+        if (Looper.myLooper() == mHandler.getLooper()) {
+            registerInternal(context, completionCallback, actions);
+        } else {
+            mHandler.post(() -> registerInternal(context, completionCallback, actions));
+        }
+    }
+
+    /** Register broadcast receiver and run completion callback if passed. */
+    @AnyThread
+    private void registerInternal(
+            Context context, @Nullable Runnable completionCallback, String... actions) {
         context.registerReceiver(this, getFilter(actions));
+        if (completionCallback != null) {
+            completionCallback.run();
+        }
     }
 
     /**
-     * Helper method to register multiple actions associated with a action. Caller should be from
-     * main thread.
+     * Same as {@link #register(Context, Runnable, String...)} above but with additional flags
+     * params.
      */
-    @UiThread
-    public void registerPkgActionsAsync(Context context, @Nullable String pkg, String... actions) {
-        assertOnMainThread();
-        UI_HELPER_EXECUTOR.execute(() -> registerPkgActionsSync(context, pkg, actions));
+    @AnyThread
+    public void register(
+            Context context, @Nullable Runnable completionCallback, int flags, String... actions) {
+        if (Looper.myLooper() == mHandler.getLooper()) {
+            registerInternal(context, completionCallback, flags, actions);
+        } else {
+            mHandler.post(() -> registerInternal(context, 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) {
+        context.registerReceiver(this, getFilter(actions), flags);
+        if (completionCallback != null) {
+            completionCallback.run();
+        }
+    }
+
+    /** Same as {@link #register(Context, Runnable, String...)} above but with pkg name. */
+    @AnyThread
+    public void registerPkgActions(Context context, @Nullable String pkg, String... actions) {
+        if (Looper.myLooper() == mHandler.getLooper()) {
+            context.registerReceiver(this, getPackageFilter(pkg, actions));
+        } else {
+            mHandler.post(() -> {
+                context.registerReceiver(this, getPackageFilter(pkg, actions));
+            });
+        }
     }
 
     /**
-     * Helper method to register multiple actions associated with a action. Caller should be from
-     * bg thread.
+     * Unregister broadcast receiver. If this method is called on the same looper with mHandler's
+     * looper, then unregister will be called synchronously. Otherwise asynchronously. This ensures
+     * unregister happens on {@link #mHandler}'s looper.
      */
-    @WorkerThread
-    public void registerPkgActionsSync(Context context, @Nullable String pkg, String... actions) {
-        assertOnBgThread();
-        context.registerReceiver(this, getPackageFilter(pkg, actions));
+    @AnyThread
+    public void unregisterReceiverSafely(Context context) {
+        if (Looper.myLooper() == mHandler.getLooper()) {
+            unregisterReceiverSafelyInternal(context);
+        } else {
+            mHandler.post(() -> {
+                unregisterReceiverSafelyInternal(context);
+            });
+        }
     }
 
-    /**
-     * Unregisters the receiver ignoring any errors on bg thread. Caller should be on main thread.
-     */
-    @UiThread
-    public void unregisterReceiverSafelyAsync(Context context) {
-        assertOnMainThread();
-        UI_HELPER_EXECUTOR.execute(() -> unregisterReceiverSafelySync(context));
-    }
-
-    /**
-     * Unregisters the receiver ignoring any errors on bg thread. Caller should be on bg thread.
-     */
-    @WorkerThread
-    public void unregisterReceiverSafelySync(Context context) {
-        assertOnBgThread();
+    /** Unregister broadcast receiver ignoring any errors. */
+    @AnyThread
+    private void unregisterReceiverSafelyInternal(Context context) {
         try {
             context.unregisterReceiver(this);
         } catch (IllegalArgumentException e) {
@@ -121,20 +174,4 @@
         }
         return filter;
     }
-
-    private static void assertOnBgThread() {
-        if (BuildConfig.IS_STUDIO_BUILD && isMainThread()) {
-            throw new IllegalStateException("Should not be called from main thread!");
-        }
-    }
-
-    private static void assertOnMainThread() {
-        if (BuildConfig.IS_STUDIO_BUILD && !isMainThread()) {
-            throw new IllegalStateException("Should not be called from bg thread!");
-        }
-    }
-
-    private static boolean isMainThread() {
-        return Thread.currentThread() == Looper.getMainLooper().getThread();
-    }
 }
diff --git a/src/com/android/launcher3/util/ViewCache.java b/src/com/android/launcher3/util/ViewCache.java
index 98e6822..b98e977 100644
--- a/src/com/android/launcher3/util/ViewCache.java
+++ b/src/com/android/launcher3/util/ViewCache.java
@@ -21,6 +21,8 @@
 import android.view.View;
 import android.view.ViewGroup;
 
+import androidx.annotation.VisibleForTesting;
+
 import com.android.launcher3.R;
 
 /**
@@ -67,7 +69,8 @@
         }
     }
 
-    private static class CacheEntry {
+    @VisibleForTesting
+    static class CacheEntry {
 
         final int mMaxSize;
         final View[] mViews;
diff --git a/src/com/android/launcher3/util/WallpaperOffsetInterpolator.java b/src/com/android/launcher3/util/WallpaperOffsetInterpolator.java
index a2277a0..f8cbe0d 100644
--- a/src/com/android/launcher3/util/WallpaperOffsetInterpolator.java
+++ b/src/com/android/launcher3/util/WallpaperOffsetInterpolator.java
@@ -32,7 +32,7 @@
     private static final int MIN_PARALLAX_PAGE_SPAN = 4;
 
     private final SimpleBroadcastReceiver mWallpaperChangeReceiver =
-            new SimpleBroadcastReceiver(i -> onWallpaperChanged());
+            new SimpleBroadcastReceiver(UI_HELPER_EXECUTOR, i -> onWallpaperChanged());
     private final Workspace<?> mWorkspace;
     private final boolean mIsRtl;
     private final Handler mHandler;
@@ -198,10 +198,10 @@
     public void setWindowToken(IBinder token) {
         mWindowToken = token;
         if (mWindowToken == null && mRegistered) {
-            mWallpaperChangeReceiver.unregisterReceiverSafelyAsync(mWorkspace.getContext());
+            mWallpaperChangeReceiver.unregisterReceiverSafely(mWorkspace.getContext());
             mRegistered = false;
         } else if (mWindowToken != null && !mRegistered) {
-            mWallpaperChangeReceiver.registerAsync(
+            mWallpaperChangeReceiver.register(
                     mWorkspace.getContext(), ACTION_WALLPAPER_CHANGED);
             onWallpaperChanged();
             mRegistered = true;
diff --git a/src/com/android/launcher3/widget/LauncherAppWidgetHost.java b/src/com/android/launcher3/widget/LauncherAppWidgetHost.java
index 40c3984..71d8503 100644
--- a/src/com/android/launcher3/widget/LauncherAppWidgetHost.java
+++ b/src/com/android/launcher3/widget/LauncherAppWidgetHost.java
@@ -21,22 +21,16 @@
 import android.appwidget.AppWidgetHost;
 import android.appwidget.AppWidgetProviderInfo;
 import android.content.Context;
-import android.view.accessibility.AccessibilityNodeInfo;
-import android.widget.RemoteViews;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.util.Executors;
-import com.android.launcher3.util.SafeCloseable;
 import com.android.launcher3.widget.LauncherWidgetHolder.ProviderChangedListener;
 
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
-import java.util.Set;
-import java.util.WeakHashMap;
 import java.util.function.IntConsumer;
 
 /**
@@ -129,37 +123,4 @@
     public void clearViews() {
         super.clearViews();
     }
-
-    public static class ListenableHostView extends LauncherAppWidgetHostView {
-
-        private Set<Runnable> mUpdateListeners = Collections.EMPTY_SET;
-
-        ListenableHostView(Context context) {
-            super(context);
-        }
-
-        @Override
-        public void updateAppWidget(RemoteViews remoteViews) {
-            super.updateAppWidget(remoteViews);
-            mUpdateListeners.forEach(Runnable::run);
-        }
-
-        @Override
-        public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
-            super.onInitializeAccessibilityNodeInfo(info);
-            info.setClassName(LauncherAppWidgetHostView.class.getName());
-        }
-
-        /**
-         * Adds a callback to be run everytime the provided app widget updates.
-         * @return a closable to remove this callback
-         */
-        public SafeCloseable addUpdateListener(Runnable callback) {
-            if (mUpdateListeners == Collections.EMPTY_SET) {
-                mUpdateListeners = Collections.newSetFromMap(new WeakHashMap<>());
-            }
-            mUpdateListeners.add(callback);
-            return () -> mUpdateListeners.remove(callback);
-        }
-    }
 }
diff --git a/src/com/android/launcher3/widget/LauncherWidgetHolder.java b/src/com/android/launcher3/widget/LauncherWidgetHolder.java
index 1fb8c83..f499fca 100644
--- a/src/com/android/launcher3/widget/LauncherWidgetHolder.java
+++ b/src/com/android/launcher3/widget/LauncherWidgetHolder.java
@@ -37,6 +37,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
 import androidx.annotation.WorkerThread;
 
 import com.android.launcher3.BaseActivity;
@@ -49,7 +50,6 @@
 import com.android.launcher3.util.LooperExecutor;
 import com.android.launcher3.util.ResourceBasedOverride;
 import com.android.launcher3.util.SafeCloseable;
-import com.android.launcher3.widget.LauncherAppWidgetHost.ListenableHostView;
 import com.android.launcher3.widget.custom.CustomWidgetManager;
 
 import java.util.ArrayList;
@@ -484,7 +484,8 @@
      * Sets or unsets a flag the can change whether the widget host should be in the listening
      * state.
      */
-    private void setShouldListenFlag(int flag, boolean on) {
+    @VisibleForTesting
+    void setShouldListenFlag(int flag, boolean on) {
         if (on) {
             mFlags.updateAndGet(old -> old | flag);
         } else {
diff --git a/src/com/android/launcher3/widget/ListenableHostView.java b/src/com/android/launcher3/widget/ListenableHostView.java
new file mode 100644
index 0000000..b809db0
--- /dev/null
+++ b/src/com/android/launcher3/widget/ListenableHostView.java
@@ -0,0 +1,60 @@
+/*
+ * 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.widget;
+
+import android.content.Context;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.widget.RemoteViews;
+
+import com.android.launcher3.util.SafeCloseable;
+
+import java.util.Collections;
+import java.util.Set;
+import java.util.WeakHashMap;
+
+public class ListenableHostView extends LauncherAppWidgetHostView {
+
+    private Set<Runnable> mUpdateListeners = Collections.EMPTY_SET;
+
+    ListenableHostView(Context context) {
+        super(context);
+    }
+
+    @Override
+    public void updateAppWidget(RemoteViews remoteViews) {
+        super.updateAppWidget(remoteViews);
+        mUpdateListeners.forEach(Runnable::run);
+    }
+
+    @Override
+    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+        super.onInitializeAccessibilityNodeInfo(info);
+        info.setClassName(LauncherAppWidgetHostView.class.getName());
+    }
+
+    /**
+     * Adds a callback to be run everytime the provided app widget updates.
+     * @return a closable to remove this callback
+     */
+    public SafeCloseable addUpdateListener(Runnable callback) {
+        if (mUpdateListeners == Collections.EMPTY_SET) {
+            mUpdateListeners = Collections.newSetFromMap(new WeakHashMap<>());
+        }
+        mUpdateListeners.add(callback);
+        return () -> mUpdateListeners.remove(callback);
+    }
+}
diff --git a/src/com/android/launcher3/widget/WidgetCell.java b/src/com/android/launcher3/widget/WidgetCell.java
index 35372d3..b7ad95e 100644
--- a/src/com/android/launcher3/widget/WidgetCell.java
+++ b/src/com/android/launcher3/widget/WidgetCell.java
@@ -40,7 +40,6 @@
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewPropertyAnimator;
-import android.view.accessibility.AccessibilityNodeInfo;
 import android.widget.Button;
 import android.widget.FrameLayout;
 import android.widget.LinearLayout;
@@ -500,12 +499,6 @@
     }
 
     @Override
-    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
-        super.onInitializeAccessibilityNodeInfo(info);
-        info.removeAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK);
-    }
-
-    @Override
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
         ViewGroup.LayoutParams containerLp = mWidgetImageContainer.getLayoutParams();
         int maxWidth = MeasureSpec.getSize(widthMeasureSpec);
diff --git a/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java b/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java
index 9260af9..d84a219 100644
--- a/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java
+++ b/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java
@@ -16,7 +16,7 @@
 
 package com.android.launcher3.widget.picker;
 
-import static com.android.launcher3.widget.util.WidgetsTableUtils.groupWidgetItemsUsingRowPxWithoutReordering;
+import static com.android.launcher3.widget.util.WidgetsTableUtils.groupWidgetItemsUsingRowPxWithReordering;
 
 import android.content.ComponentName;
 import android.content.Context;
@@ -38,6 +38,7 @@
 import com.android.launcher3.pageindicators.PageIndicatorDots;
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
@@ -57,6 +58,12 @@
     private static final String INITIALLY_DISPLAYED_WIDGETS_STATE_KEY =
             "widgetRecommendationsView:mDisplayedWidgets";
     private static final int MAX_CATEGORIES = 3;
+
+    // Whether to show all widgets in a full page without any limitation on height
+    private boolean mShowFullPageViewIfLowDensity = false;
+    // Number of items below which a category is considered low density.
+    private static final int IDEAL_ITEMS_PER_CATEGORY = 2;
+
     private TextView mRecommendationPageTitle;
     private final List<String> mCategoryTitles = new ArrayList<>();
 
@@ -88,6 +95,14 @@
     }
 
     /**
+     * When there are less than 3 categories or when at least one category has less than 2 widgets,
+     * all widgets will be shown in a single page without being limited by the available height.
+     */
+    public void enableFullPageViewIfLowDensity() {
+        mShowFullPageViewIfLowDensity = true;
+    }
+
+    /**
      * Saves the necessary state in the provided bundle. To be called in case of orientation /
      * other config changes.
      */
@@ -170,6 +185,22 @@
         return displayedWidgets.size();
     }
 
+    private boolean shouldShowFullPageView(
+            Map<WidgetRecommendationCategory, List<WidgetItem>> recommendations) {
+        if (mShowFullPageViewIfLowDensity) {
+            boolean hasLessCategories = recommendations.size() < MAX_CATEGORIES;
+            long lowDensityCategoriesCount = recommendations.values()
+                    .stream()
+                    .limit(MAX_CATEGORIES)
+                    .filter(items -> items.size() < IDEAL_ITEMS_PER_CATEGORY).count();
+
+            // If there less number of categories or if there are at least 2 categorizes with less
+            // widgets, prefer showing single page view.
+            return hasLessCategories || lowDensityCategoriesCount > 1;
+        }
+        return false;
+    }
+
     /**
      * Displays the recommendations grouped by categories as pages.
      * <p>In case of a single category, no title is displayed for it.</p>
@@ -188,6 +219,14 @@
             Map<WidgetRecommendationCategory, List<WidgetItem>> recommendations,
             DeviceProfile deviceProfile, final @Px float availableHeight,
             final @Px int availableWidth, final @Px int cellPadding, final int requestedPage) {
+        if (shouldShowFullPageView(recommendations)) {
+            // Show all widgets in single page with unlimited available height.
+            return setRecommendations(
+                    recommendations.values().stream().flatMap(Collection::stream).toList(),
+                    deviceProfile, /*availableHeight=*/ Float.MAX_VALUE, availableWidth,
+                    cellPadding);
+
+        }
         this.mAvailableHeight = availableHeight;
         this.mAvailableWidth = availableWidth;
         Context context = getContext();
@@ -325,7 +364,7 @@
 
         // Since we are limited by space, we don't sort recommendations - to show most relevant
         // (if possible).
-        List<ArrayList<WidgetItem>> rows = groupWidgetItemsUsingRowPxWithoutReordering(
+        List<ArrayList<WidgetItem>> rows = groupWidgetItemsUsingRowPxWithReordering(
                 filteredRecommendedWidgets,
                 context,
                 deviceProfile,
diff --git a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
index fd15677..2e36583 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
@@ -679,6 +679,18 @@
         return sheet;
     }
 
+    /**
+     * Updates the widget picker's title and description in the header to the provided values (if
+     * present).
+     */
+    public void mayUpdateTitleAndDescription(@Nullable String title,
+            @Nullable String descriptionRes) {
+        if (title != null) {
+            mHeaderTitle.setText(title);
+        }
+        // Full sheet doesn't support a description.
+    }
+
     @Override
     public void saveHierarchyState(SparseArray<Parcelable> sparseArray) {
         Bundle bundle = new Bundle();
diff --git a/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java b/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java
index 1ed3d88..0bcab60 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java
@@ -18,7 +18,7 @@
 import static com.android.launcher3.Flags.enableCategorizedWidgetSuggestions;
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION;
 import static com.android.launcher3.widget.util.WidgetSizes.getWidgetSizePx;
-import static com.android.launcher3.widget.util.WidgetsTableUtils.WIDGETS_TABLE_ROW_SIZE_COMPARATOR;
+import static com.android.launcher3.widget.util.WidgetsTableUtils.WIDGETS_TABLE_ROW_COUNT_COMPARATOR;
 
 import static java.lang.Math.max;
 
@@ -163,6 +163,6 @@
         }
 
         // Perform re-ordering once we have filtered out recommendations that fit.
-        return filteredRows.stream().sorted(WIDGETS_TABLE_ROW_SIZE_COMPARATOR).toList();
+        return filteredRows.stream().sorted(WIDGETS_TABLE_ROW_COUNT_COMPARATOR).toList();
     }
 }
diff --git a/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java b/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
index 840d98a..c84680d 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
@@ -36,6 +36,7 @@
 import android.widget.FrameLayout;
 import android.widget.LinearLayout;
 import android.widget.ScrollView;
+import android.widget.TextView;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -60,9 +61,6 @@
  * Popup for showing the full list of available widgets with a two-pane layout.
  */
 public class WidgetsTwoPaneSheet extends WidgetsFullSheet {
-
-    private static final int PERSONAL_TAB = 0;
-    private static final int WORK_TAB = 1;
     private static final int MINIMUM_WIDTH_LEFT_PANE_FOLDABLE_DP = 268;
     private static final int MAXIMUM_WIDTH_LEFT_PANE_FOLDABLE_DP = 395;
     private static final String SUGGESTIONS_PACKAGE_NAME = "widgets_list_suggestions_entry";
@@ -83,6 +81,7 @@
     private int mActivePage = -1;
     @Nullable
     private PackageUserKey mSelectedHeader;
+    private TextView mHeaderDescription;
 
     public WidgetsTwoPaneSheet(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
@@ -120,12 +119,17 @@
         mWidgetRecommendationsView.initParentViews(mWidgetRecommendationsContainer);
         mWidgetRecommendationsView.setWidgetCellLongClickListener(this);
         mWidgetRecommendationsView.setWidgetCellOnClickListener(this);
+        if (!mDeviceProfile.isTwoPanels) {
+            mWidgetRecommendationsView.enableFullPageViewIfLowDensity();
+        }
         // To save the currently displayed page, so that, it can be requested when rebinding
         // recommendations with different size constraints.
         mWidgetRecommendationsView.addPageSwitchListener(
                 newPage -> mRecommendationsCurrentPage = newPage);
 
         mHeaderTitle = mContent.findViewById(R.id.title);
+        mHeaderDescription = mContent.findViewById(R.id.widget_picker_description);
+
         mRightPane = mContent.findViewById(R.id.right_pane);
         mRightPaneScrollView = mContent.findViewById(R.id.right_pane_scroll_view);
         mRightPaneScrollView.setOverScrollMode(View.OVER_SCROLL_NEVER);
@@ -141,6 +145,17 @@
     }
 
     @Override
+    public void mayUpdateTitleAndDescription(@Nullable String title, @Nullable String description) {
+        if (title != null) {
+            mHeaderTitle.setText(title);
+        }
+        if (description != null) {
+            mHeaderDescription.setText(description);
+            mHeaderDescription.setVisibility(VISIBLE);
+        }
+    }
+
+    @Override
     protected int getTabletHorizontalMargin(DeviceProfile deviceProfile) {
         if (enableCategorizedWidgetSuggestions()) {
             // two pane picker is full width for fold as well as tablet.
@@ -371,9 +386,10 @@
     protected void updateRecyclerViewVisibility(AdapterHolder adapterHolder) {
         // The first item is always an empty space entry. Look for any more items.
         boolean isWidgetAvailable = adapterHolder.mWidgetsListAdapter.hasVisibleEntries();
-
-        mRightPane.setVisibility(isWidgetAvailable ? VISIBLE : GONE);
-
+        if (!isWidgetAvailable) {
+            mRightPane.removeAllViews();
+            mRightPane.addView(mNoWidgetsView);
+        }
         super.updateRecyclerViewVisibility(adapterHolder);
     }
 
diff --git a/src/com/android/launcher3/widget/util/WidgetsTableUtils.java b/src/com/android/launcher3/widget/util/WidgetsTableUtils.java
index edaf474..df72f07 100644
--- a/src/com/android/launcher3/widget/util/WidgetsTableUtils.java
+++ b/src/com/android/launcher3/widget/util/WidgetsTableUtils.java
@@ -69,6 +69,21 @@
             });
 
     /**
+     * Comparator that enables displaying rows with more number of items at the top, and then
+     * rest of widgets shown in increasing order of their size (totalW * H).
+     */
+    public static final Comparator<ArrayList<WidgetItem>> WIDGETS_TABLE_ROW_COUNT_COMPARATOR =
+            Comparator.comparingInt(row -> {
+                if (row.size() > 1) {
+                    return -row.size();
+                } else {
+                    int rowWidth = row.stream().mapToInt(w -> w.spanX).sum();
+                    int rowHeight = row.get(0).spanY;
+                    return (rowWidth * rowHeight);
+                }
+            });
+
+    /**
      * Groups {@code widgetItems} items into a 2D array which matches their appearance in a UI
      * table. This takes liberty to rearrange widgets to make the table visually appealing.
      */
diff --git a/tests/assets/ReorderWidgets/full_reorder_case b/tests/assets/ReorderWidgets/full_reorder_case
index 850e4fd..2890b79 100644
--- a/tests/assets/ReorderWidgets/full_reorder_case
+++ b/tests/assets/ReorderWidgets/full_reorder_case
@@ -17,12 +17,12 @@
 # Test 4x4
 board: 4x4
 xxxx
-bbmm
+bbaa
 iimm
-iiaa
+iimm
 arguments: 0 2
 board: 4x4
 xxxx
-bbii
+bbaa
 mmii
-mmaa
\ No newline at end of file
+mmii
\ No newline at end of file
diff --git a/tests/assets/ReorderWidgets/push_reorder_case b/tests/assets/ReorderWidgets/push_reorder_case
index 8e845a2..1eacfae 100644
--- a/tests/assets/ReorderWidgets/push_reorder_case
+++ b/tests/assets/ReorderWidgets/push_reorder_case
@@ -17,28 +17,28 @@
 #Test 5x5
 board: 5x5
 xxxxx
-bbbm-
+bbb--
 --ccc
 --ddd
------
-arguments: 2 1
+----m
+arguments: 2 2
 board: 5x5
 xxxxx
---m--
 bbb--
+--m--
 --ccc
 --ddd
 #6x5 Test
 board: 6x5
 xxxxxx
-bbbbm-
+bbbb--
 --aaa-
 --ddd-
-------
-arguments: 2 1
+-----m
+arguments: 2 2
 board: 6x5
 xxxxxx
---m---
 bbbb--
+--m---
 --aaa-
 --ddd-
\ No newline at end of file
diff --git a/tests/assets/ReorderWidgets/simple_reorder_case b/tests/assets/ReorderWidgets/simple_reorder_case
index 2c50ce4..991ccb5 100644
--- a/tests/assets/ReorderWidgets/simple_reorder_case
+++ b/tests/assets/ReorderWidgets/simple_reorder_case
@@ -21,7 +21,7 @@
 --mm-
 -----
 -----
-arguments: 0 4
+arguments: 0 3
 board: 5x5
 xxxxx
 -----
diff --git a/tests/multivalentTests/shared/com/android/launcher3/testing/shared/TestProtocol.java b/tests/multivalentTests/shared/com/android/launcher3/testing/shared/TestProtocol.java
index 59d0de6..fab3015 100644
--- a/tests/multivalentTests/shared/com/android/launcher3/testing/shared/TestProtocol.java
+++ b/tests/multivalentTests/shared/com/android/launcher3/testing/shared/TestProtocol.java
@@ -174,6 +174,7 @@
     public static final String TEST_DRAG_APP_ICON_TO_MULTIPLE_WORKSPACES_FAILURE = "b/326908466";
     public static final String WIDGET_CONFIG_NULL_EXTRA_INTENT = "b/324419890";
     public static final String OVERVIEW_SELECT_TOOLTIP_MISALIGNED = "b/332485341";
+    public static final String PRIVATE_SPACE_SCROLL_FAILURE = "b/339737008";
 
     public static final String REQUEST_FLAG_ENABLE_GRID_ONLY_OVERVIEW = "enable-grid-only-overview";
     public static final String REQUEST_FLAG_ENABLE_APP_PAIRS = "enable-app-pairs";
diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/CellLayoutTestCaseReader.java b/tests/multivalentTests/src/com/android/launcher3/celllayout/CellLayoutTestCaseReader.java
index 419cb3d..f1403e5 100644
--- a/tests/multivalentTests/src/com/android/launcher3/celllayout/CellLayoutTestCaseReader.java
+++ b/tests/multivalentTests/src/com/android/launcher3/celllayout/CellLayoutTestCaseReader.java
@@ -54,7 +54,7 @@
     }
 
     public static class Arguments extends TestSection {
-        String[] arguments;
+        public String[] arguments;
 
         public Arguments(String[] arguments) {
             super(State.ARGUMENTS);
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/LauncherWidgetHolderTest.kt b/tests/multivalentTests/src/com/android/launcher3/widget/LauncherWidgetHolderTest.kt
new file mode 100644
index 0000000..1a659e2
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/LauncherWidgetHolderTest.kt
@@ -0,0 +1,149 @@
+/*
+ * 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.widget
+
+import androidx.test.annotation.UiThreadTest
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.android.launcher3.BuildConfig.WIDGETS_ENABLED
+import com.android.launcher3.util.ActivityContextWrapper
+import com.android.launcher3.util.LauncherMultivalentJUnit
+import com.android.launcher3.widget.LauncherWidgetHolder.FLAG_ACTIVITY_RESUMED
+import com.android.launcher3.widget.LauncherWidgetHolder.FLAG_ACTIVITY_STARTED
+import com.android.launcher3.widget.LauncherWidgetHolder.FLAG_STATE_IS_NORMAL
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(LauncherMultivalentJUnit::class)
+class LauncherWidgetHolderTest {
+    private lateinit var widgetHolder: LauncherWidgetHolder
+
+    @Before
+    fun setUp() {
+        assertTrue(WIDGETS_ENABLED)
+        widgetHolder =
+            LauncherWidgetHolder(ActivityContextWrapper(getInstrumentation().targetContext)) {}
+    }
+
+    @After
+    fun tearDown() {
+        widgetHolder.destroy()
+    }
+
+    @Test
+    fun widget_holder_start_listening() {
+        val testView = mock(PendingAppWidgetHostView::class.java)
+        widgetHolder.mViews[0] = testView
+        widgetHolder.setListeningFlag(false)
+        assertFalse(widgetHolder.isListening)
+        widgetHolder.startListening()
+        widgetHolder.widgetHolderExecutor.submit {}.get()
+        getInstrumentation().waitForIdleSync()
+        assertTrue(widgetHolder.isListening)
+        verify(testView, times(1)).reInflate()
+        widgetHolder.clearWidgetViews()
+    }
+
+    @Test
+    fun holder_start_listening_after_activity_start() {
+        widgetHolder.setShouldListenFlag(FLAG_STATE_IS_NORMAL or FLAG_ACTIVITY_RESUMED, true)
+        widgetHolder.setActivityStarted(false)
+        widgetHolder.widgetHolderExecutor.submit {}.get()
+        assertFalse(widgetHolder.shouldListen(widgetHolder.mFlags.get()))
+        widgetHolder.setActivityStarted(true)
+        widgetHolder.widgetHolderExecutor.submit {}.get()
+        assertTrue(widgetHolder.shouldListen(widgetHolder.mFlags.get()))
+    }
+
+    @Test
+    fun holder_start_listening_after_activity_resume() {
+        widgetHolder.setShouldListenFlag(FLAG_STATE_IS_NORMAL or FLAG_ACTIVITY_STARTED, true)
+        widgetHolder.setActivityResumed(false)
+        widgetHolder.widgetHolderExecutor.submit {}.get()
+        assertFalse(widgetHolder.shouldListen(widgetHolder.mFlags.get()))
+        widgetHolder.setActivityResumed(true)
+        widgetHolder.widgetHolderExecutor.submit {}.get()
+        assertTrue(widgetHolder.shouldListen(widgetHolder.mFlags.get()))
+    }
+
+    @Test
+    fun holder_start_listening_after_state_normal() {
+        widgetHolder.setShouldListenFlag(FLAG_ACTIVITY_RESUMED or FLAG_ACTIVITY_STARTED, true)
+        widgetHolder.setStateIsNormal(false)
+        widgetHolder.widgetHolderExecutor.submit {}.get()
+        assertFalse(widgetHolder.shouldListen(widgetHolder.mFlags.get()))
+        widgetHolder.setStateIsNormal(true)
+        widgetHolder.widgetHolderExecutor.submit {}.get()
+        assertTrue(widgetHolder.shouldListen(widgetHolder.mFlags.get()))
+    }
+
+    @Test
+    @UiThreadTest
+    fun widget_holder_create_view() {
+        val mockProviderInfo = mock(LauncherAppWidgetProviderInfo::class.java)
+        doReturn(false).whenever(mockProviderInfo).isCustomWidget
+        assertEquals(0, widgetHolder.mViews.size())
+        widgetHolder.createView(APP_WIDGET_ID, mockProviderInfo)
+        assertEquals(1, widgetHolder.mViews.size())
+        assertEquals(APP_WIDGET_ID, widgetHolder.mViews.get(0).appWidgetId)
+        widgetHolder.deleteAppWidgetId(APP_WIDGET_ID)
+        assertEquals(0, widgetHolder.mViews.size())
+    }
+
+    @Test
+    fun holder_add_provider_change_listener() {
+        val listener = LauncherWidgetHolder.ProviderChangedListener {}
+        widgetHolder.addProviderChangeListener(listener)
+        getInstrumentation().waitForIdleSync()
+        assertEquals(1, widgetHolder.mProviderChangedListeners.size)
+        assertSame(widgetHolder.mProviderChangedListeners.first(), listener)
+        widgetHolder.removeProviderChangeListener(listener)
+    }
+
+    @Test
+    fun holder_remove_provider_change_listener() {
+        val listener = LauncherWidgetHolder.ProviderChangedListener {}
+        widgetHolder.addProviderChangeListener(listener)
+        widgetHolder.removeProviderChangeListener(listener)
+        getInstrumentation().waitForIdleSync()
+        assertEquals(0, widgetHolder.mProviderChangedListeners.size)
+    }
+
+    @Test
+    fun widget_holder_stop_listening() {
+        widgetHolder.setListeningFlag(true)
+        assertTrue(widgetHolder.isListening)
+        widgetHolder.stopListening()
+        widgetHolder.widgetHolderExecutor.submit {}.get()
+        assertFalse(widgetHolder.isListening)
+    }
+
+    companion object {
+        private const val APP_WIDGET_ID = 0
+    }
+}
diff --git a/tests/src/com/android/launcher3/allapps/PrivateProfileManagerTest.java b/tests/src/com/android/launcher3/allapps/PrivateProfileManagerTest.java
index 57117cb..430e496 100644
--- a/tests/src/com/android/launcher3/allapps/PrivateProfileManagerTest.java
+++ b/tests/src/com/android/launcher3/allapps/PrivateProfileManagerTest.java
@@ -22,8 +22,6 @@
 import static com.android.launcher3.allapps.UserProfileManager.STATE_ENABLED;
 import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_PRIVATE_PROFILE_QUIET_MODE_ENABLED;
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
-import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL;
-import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT;
 
 import static org.junit.Assert.assertEquals;
 import static org.mockito.ArgumentMatchers.any;
@@ -202,7 +200,6 @@
     }
 
     @Test
-    @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/339109319
     public void openPrivateSpaceSettings_triggersCorrectIntent() {
         Intent expectedIntent = ApiWrapper.INSTANCE.get(mContext).getPrivateSpaceSettingsIntent();
         ArgumentCaptor<Intent> acIntent = ArgumentCaptor.forClass(Intent.class);
diff --git a/tests/src/com/android/launcher3/celllayout/TaplReorderWidgetsTest.java b/tests/src/com/android/launcher3/celllayout/TaplReorderWidgetsTest.java
deleted file mode 100644
index 28a1325..0000000
--- a/tests/src/com/android/launcher3/celllayout/TaplReorderWidgetsTest.java
+++ /dev/null
@@ -1,312 +0,0 @@
-/*
- * Copyright (C) 2022 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;
-
-import static android.platform.uiautomator_helpers.DeviceHelpers.getContext;
-
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-
-import android.content.ContentResolver;
-import android.content.ContentValues;
-import android.graphics.Point;
-import android.net.Uri;
-import android.util.Log;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
-import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.MultipageCellLayout;
-import com.android.launcher3.celllayout.board.CellLayoutBoard;
-import com.android.launcher3.celllayout.board.TestWorkspaceBuilder;
-import com.android.launcher3.celllayout.board.WidgetRect;
-import com.android.launcher3.tapl.Widget;
-import com.android.launcher3.tapl.WidgetResizeFrame;
-import com.android.launcher3.ui.AbstractLauncherUiTest;
-import com.android.launcher3.util.ModelTestExtensions;
-import com.android.launcher3.util.rule.ShellCommandRule;
-
-import org.junit.After;
-import org.junit.Assert;
-import org.junit.Assume;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class TaplReorderWidgetsTest extends AbstractLauncherUiTest<Launcher> {
-
-    @Rule
-    public ShellCommandRule mGrantWidgetRule = ShellCommandRule.grantWidgetBind();
-
-    private static final String TAG = TaplReorderWidgetsTest.class.getSimpleName();
-
-    private static final List<String> FOLDABLE_GRIDS = List.of("normal", "practical", "reasonable");
-
-    TestWorkspaceBuilder mWorkspaceBuilder;
-
-    @Before
-    public void setup() throws Throwable {
-        mWorkspaceBuilder = new TestWorkspaceBuilder(mTargetContext);
-        super.setUp();
-    }
-
-    @After
-    public void tearDown() {
-        ModelTestExtensions.INSTANCE.clearModelDb(
-                LauncherAppState.getInstance(getContext()).getModel()
-        );
-    }
-
-    /**
-     * Validate if the given board represent the current CellLayout
-     **/
-    private boolean validateBoard(List<CellLayoutBoard> testBoards) {
-        ArrayList<CellLayoutBoard> workspaceBoards = workspaceToBoards();
-        if (workspaceBoards.size() < testBoards.size()) {
-            return false;
-        }
-        for (int i = 0; i < testBoards.size(); i++) {
-            if (testBoards.get(i).compareTo(workspaceBoards.get(i)) != 0) {
-                return false;
-            }
-        }
-        return true;
-    }
-
-    private FavoriteItemsTransaction buildWorkspaceFromBoards(List<CellLayoutBoard> boards,
-            FavoriteItemsTransaction transaction) {
-        for (int i = 0; i < boards.size(); i++) {
-            CellLayoutBoard board = boards.get(i);
-            mWorkspaceBuilder.buildFromBoard(board, transaction, i);
-        }
-        return transaction;
-    }
-
-    private void printCurrentWorkspace() {
-        InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(mTargetContext);
-        ArrayList<CellLayoutBoard> boards = workspaceToBoards();
-        for (int i = 0; i < boards.size(); i++) {
-            Log.d(TAG, "Screen number " + i);
-            Log.d(TAG, ".\n" + boards.get(i).toString(idp.numColumns, idp.numRows));
-        }
-    }
-
-    private ArrayList<CellLayoutBoard> workspaceToBoards() {
-        return getFromLauncher(CellLayoutTestUtils::workspaceToBoards);
-    }
-
-    private WidgetRect getWidgetClosestTo(Point point) {
-        ArrayList<CellLayoutBoard> workspaceBoards = workspaceToBoards();
-        int maxDistance = 9999;
-        WidgetRect bestRect = null;
-        for (int i = 0; i < workspaceBoards.get(0).getWidgets().size(); i++) {
-            WidgetRect widget = workspaceBoards.get(0).getWidgets().get(i);
-            if (widget.getCellX() == 0 && widget.getCellY() == 0) {
-                continue;
-            }
-            int distance = Math.abs(point.x - widget.getCellX())
-                    + Math.abs(point.y - widget.getCellY());
-            if (distance == 0) {
-                break;
-            }
-            if (distance < maxDistance) {
-                maxDistance = distance;
-                bestRect = widget;
-            }
-        }
-        return bestRect;
-    }
-
-    /**
-     * This function might be odd, its function is to select a widget and leave it in its place.
-     * The idea is to make the test broader and also test after a widgets resized because the
-     * underlying code does different things in that case
-     */
-    private void triggerWidgetResize(ReorderTestCase testCase) {
-        WidgetRect widgetRect = getWidgetClosestTo(testCase.moveMainTo);
-        if (widgetRect == null) {
-            // Some test doesn't have a widget in the final position, in those cases we will ignore
-            // them
-            return;
-        }
-        Widget widget = mLauncher.getWorkspace().getWidgetAtCell(widgetRect.getCellX(),
-                widgetRect.getCellY());
-        WidgetResizeFrame resizeFrame = widget.dragWidgetToWorkspace(widgetRect.getCellX(),
-                widgetRect.getCellY(), widgetRect.getSpanX(), widgetRect.getSpanY());
-        resizeFrame.dismiss();
-    }
-
-    private void runTestCase(ReorderTestCase testCase) {
-        WidgetRect mainWidgetCellPos = CellLayoutBoard.getMainFromList(
-                testCase.mStart);
-
-        FavoriteItemsTransaction transaction =
-                new FavoriteItemsTransaction(mTargetContext);
-        transaction = buildWorkspaceFromBoards(testCase.mStart, transaction);
-        transaction.commit();
-        mLauncher.waitForLauncherInitialized();
-        // resetLoaderState triggers the launcher to start loading the workspace which allows
-        // waitForLauncherCondition to wait for that condition, otherwise the condition would
-        // always be true and it wouldn't wait for the changes to be applied.
-        waitForLauncherCondition("Workspace didn't finish loading", l -> !l.isWorkspaceLoading());
-
-        triggerWidgetResize(testCase);
-
-        Widget widget = mLauncher.getWorkspace().getWidgetAtCell(mainWidgetCellPos.getCellX(),
-                mainWidgetCellPos.getCellY());
-        assertNotNull(widget);
-        WidgetResizeFrame resizeFrame = widget.dragWidgetToWorkspace(testCase.moveMainTo.x,
-                testCase.moveMainTo.y, mainWidgetCellPos.getSpanX(), mainWidgetCellPos.getSpanY());
-        resizeFrame.dismiss();
-
-        boolean isValid = false;
-        for (List<CellLayoutBoard> boards : testCase.mEnd) {
-            isValid |= validateBoard(boards);
-            if (isValid) break;
-        }
-        printCurrentWorkspace();
-        assertTrue("Non of the valid boards match with the current state", isValid);
-    }
-
-    /**
-     * Run only the test define for the current grid size if such test exist
-     *
-     * @param testCaseMap map containing all the tests per grid size (Point)
-     */
-    private boolean runTestCaseMap(Map<Point, ReorderTestCase> testCaseMap, String testName) {
-        Point iconGridDimensions = mLauncher.getWorkspace().getIconGridDimensions();
-        Log.d(TAG, "Running test " + testName + " for grid " + iconGridDimensions);
-        if (!testCaseMap.containsKey(iconGridDimensions)) {
-            Log.d(TAG, "The test " + testName + " doesn't support " + iconGridDimensions
-                    + " grid layout");
-            return false;
-        }
-        runTestCase(testCaseMap.get(iconGridDimensions));
-
-        return true;
-    }
-
-    private void runTestCaseMapForAllGrids(Map<Point, ReorderTestCase> testCaseMap,
-            String testName) {
-        boolean runAtLeastOnce = false;
-        for (String grid : FOLDABLE_GRIDS) {
-            applyGridOption(grid);
-            mLauncher.waitForLauncherInitialized();
-            runAtLeastOnce |= runTestCaseMap(testCaseMap, testName);
-        }
-        Assume.assumeTrue("None of the grids are supported", runAtLeastOnce);
-    }
-
-    private void applyGridOption(Object argValue) {
-        String testProviderAuthority = mTargetContext.getPackageName() + ".grid_control";
-        Uri gridUri = new Uri.Builder()
-                .scheme(ContentResolver.SCHEME_CONTENT)
-                .authority(testProviderAuthority)
-                .appendPath("default_grid")
-                .build();
-        ContentValues values = new ContentValues();
-        values.putObject("name", argValue);
-        Assert.assertEquals(1,
-                mTargetContext.getContentResolver().update(gridUri, values, null, null));
-    }
-
-    @Test
-    public void simpleReorder() throws Exception {
-        runTestCaseMap(getTestMap("ReorderWidgets/simple_reorder_case"),
-                "push_reorder_case");
-    }
-
-    @Test
-    public void pushTest() throws Exception {
-        runTestCaseMap(getTestMap("ReorderWidgets/push_reorder_case"),
-                "push_reorder_case");
-    }
-
-    @Test
-    public void fullReorder() throws Exception {
-        runTestCaseMap(getTestMap("ReorderWidgets/full_reorder_case"),
-                "full_reorder_case");
-    }
-
-    @Test
-    public void moveOutReorder() throws Exception {
-        runTestCaseMap(getTestMap("ReorderWidgets/move_out_reorder_case"),
-                "move_out_reorder_case");
-    }
-
-    @Test
-    public void multipleCellLayoutsSimpleReorder() throws Exception {
-        Assume.assumeTrue("Test doesn't support foldables", getFromLauncher(
-                l -> l.getWorkspace().getScreenWithId(0) instanceof MultipageCellLayout));
-        runTestCaseMapForAllGrids(getTestMap("ReorderWidgets/multiple_cell_layouts_simple_reorder"),
-                "multiple_cell_layouts_simple_reorder");
-    }
-
-    @Test
-    public void multipleCellLayoutsNoSpaceReorder() throws Exception {
-        Assume.assumeTrue("Test doesn't support foldables", getFromLauncher(
-                l -> l.getWorkspace().getScreenWithId(0) instanceof MultipageCellLayout));
-        runTestCaseMapForAllGrids(
-                getTestMap("ReorderWidgets/multiple_cell_layouts_no_space_reorder"),
-                "multiple_cell_layouts_no_space_reorder");
-    }
-
-    @Test
-    public void multipleCellLayoutsReorderToOtherSide() throws Exception {
-        Assume.assumeTrue("Test doesn't support foldables", getFromLauncher(
-                l -> l.getWorkspace().getScreenWithId(0) instanceof MultipageCellLayout));
-        runTestCaseMapForAllGrids(
-                getTestMap("ReorderWidgets/multiple_cell_layouts_reorder_other_side"),
-                "multiple_cell_layouts_reorder_other_side");
-    }
-
-    private void addTestCase(Iterator<CellLayoutTestCaseReader.TestSection> sections,
-            Map<Point, ReorderTestCase> testCaseMap) {
-        CellLayoutTestCaseReader.Board startBoard =
-                ((CellLayoutTestCaseReader.Board) sections.next());
-        CellLayoutTestCaseReader.Arguments point =
-                ((CellLayoutTestCaseReader.Arguments) sections.next());
-        CellLayoutTestCaseReader.Board endBoard =
-                ((CellLayoutTestCaseReader.Board) sections.next());
-        Point moveTo = new Point(Integer.parseInt(point.arguments[0]),
-                Integer.parseInt(point.arguments[1]));
-        testCaseMap.put(endBoard.gridSize,
-                new ReorderTestCase(startBoard.board, moveTo, endBoard.board));
-    }
-
-    private Map<Point, ReorderTestCase> getTestMap(String testPath) throws IOException {
-        Map<Point, ReorderTestCase> testCaseMap = new HashMap<>();
-        Iterator<CellLayoutTestCaseReader.TestSection> iterableSection =
-                CellLayoutTestCaseReader.readFromFile(testPath).parse().iterator();
-        while (iterableSection.hasNext()) {
-            addTestCase(iterableSection, testCaseMap);
-        }
-        return testCaseMap;
-    }
-}
diff --git a/tests/src/com/android/launcher3/celllayout/integrationtest/TestUtils.kt b/tests/src/com/android/launcher3/celllayout/integrationtest/TestUtils.kt
new file mode 100644
index 0000000..4cecb5a
--- /dev/null
+++ b/tests/src/com/android/launcher3/celllayout/integrationtest/TestUtils.kt
@@ -0,0 +1,72 @@
+/*
+ * 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
+
+import android.graphics.Point
+import android.graphics.Rect
+import android.view.View
+import android.view.ViewGroup
+import com.android.launcher3.CellLayout
+import com.android.launcher3.Workspace
+import com.android.launcher3.util.CellAndSpan
+import com.android.launcher3.widget.LauncherAppWidgetHostView
+
+object TestUtils {
+    fun <T> searchChildren(viewGroup: ViewGroup, type: Class<T>): T? where T : View {
+        for (i in 0..<viewGroup.childCount) {
+            val child = viewGroup.getChildAt(i)
+            if (type.isInstance(child)) {
+                return type.cast(child)
+            }
+            if (child is ViewGroup) {
+                val result = searchChildren(child, type)
+                if (result != null) {
+                    return result
+                }
+            }
+        }
+        return null
+    }
+
+    fun getWidgetAtCell(
+        workspace: Workspace<*>,
+        cellX: Int,
+        cellY: Int
+    ): LauncherAppWidgetHostView {
+        val view =
+            (workspace.getPageAt(workspace.currentPage) as CellLayout).getChildAt(cellX, cellY)
+        assert(view != null) { "There is no view at $cellX , $cellY" }
+        assert(view is LauncherAppWidgetHostView) { "The view at $cellX , $cellY is not a widget" }
+        return view as LauncherAppWidgetHostView
+    }
+
+    fun getCellTopLeftRelativeToCellLayout(
+        workspace: Workspace<*>,
+        cellAndSpan: CellAndSpan
+    ): Point {
+        val target = Rect()
+        val cellLayout = workspace.getPageAt(workspace.currentPage) as CellLayout
+        cellLayout.cellToRect(
+            cellAndSpan.cellX,
+            cellAndSpan.cellY,
+            cellAndSpan.spanX,
+            cellAndSpan.spanY,
+            target
+        )
+        return Point(target.left, target.top)
+    }
+}
diff --git a/tests/src/com/android/launcher3/celllayout/integrationtest/events/EventsRule.kt b/tests/src/com/android/launcher3/celllayout/integrationtest/events/EventsRule.kt
new file mode 100644
index 0000000..fb61ced
--- /dev/null
+++ b/tests/src/com/android/launcher3/celllayout/integrationtest/events/EventsRule.kt
@@ -0,0 +1,64 @@
+/*
+ * 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.content.Context
+import com.android.launcher3.debug.TestEvent
+import com.android.launcher3.debug.TestEventEmitter
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+/**
+ * Rule to create EventWaiters to wait for events that happens on the Launcher. For reference look
+ * at [TestEvent] for existing events.
+ *
+ * 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.
+ */
+class EventsRule(val context: Context) : TestRule {
+
+    private var prevEventEmitter: TestEventEmitter? = null
+
+    private val eventEmitter = TestEventsEmitterImplementation()
+
+    override fun apply(base: Statement, description: Description?): Statement {
+        return object : Statement() {
+            override fun evaluate() {
+                beforeTest()
+                base.evaluate()
+                afterTest()
+            }
+        }
+    }
+
+    fun createEventWaiter(expectedEvent: TestEvent): EventWaiter {
+        return eventEmitter.createEventWaiter(expectedEvent)
+    }
+
+    private fun beforeTest() {
+        prevEventEmitter = TestEventEmitter.INSTANCE.get(context)
+        TestEventEmitter.INSTANCE.initializeForTesting(eventEmitter)
+    }
+
+    private fun afterTest() {
+        TestEventEmitter.INSTANCE.initializeForTesting(prevEventEmitter)
+    }
+}
diff --git a/tests/src/com/android/launcher3/celllayout/integrationtest/events/TestEventsEmitterImplementation.kt b/tests/src/com/android/launcher3/celllayout/integrationtest/events/TestEventsEmitterImplementation.kt
new file mode 100644
index 0000000..365ad4b
--- /dev/null
+++ b/tests/src/com/android/launcher3/celllayout/integrationtest/events/TestEventsEmitterImplementation.kt
@@ -0,0 +1,89 @@
+/*
+ * 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"
+    }
+
+    fun waitForSignal(timeout: Long = TimeUnit.SECONDS.toMillis(10)) = runBlocking {
+        var status = withTimeoutOrNull(timeout) { deferrable.await() }
+        if (status == null) {
+            status = EventStatus.TIMEOUT
+        }
+        if (status != EventStatus.SUCCESS) {
+            throw Exception("Failure waiting for event $eventToWait, failure = $status")
+        }
+    }
+
+    fun terminate() {
+        deferrable.complete(EventStatus.SUCCESS)
+    }
+}
+
+class TestEventsEmitterImplementation() : TestEventEmitter {
+    companion object {
+        private const val TAG = "TestEvents"
+    }
+
+    private val expectedEvents: ArrayDeque<EventWaiter> = ArrayDeque()
+
+    fun createEventWaiter(expectedEvent: TestEvent): EventWaiter {
+        val eventWaiter = EventWaiter(expectedEvent)
+        expectedEvents.add(eventWaiter)
+        return eventWaiter
+    }
+
+    private fun clearQueue() {
+        expectedEvents.clear()
+    }
+
+    override fun sendEvent(event: TestEvent) {
+        Log.d(TAG, "Signal received $event")
+        Log.d(TAG, "Total expected events ${expectedEvents.size}")
+        if (expectedEvents.isEmpty()) return
+        val eventWaiter = expectedEvents.last()
+        if (eventWaiter.eventToWait == event) {
+            Log.d(TAG, "Removing $event")
+            expectedEvents.removeLast()
+            eventWaiter.terminate()
+        } else {
+            Log.d(TAG, "Not matching $event")
+        }
+    }
+
+    override fun close() {
+        clearQueue()
+    }
+}
diff --git a/tests/src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java b/tests/src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java
index 405dae7..46cafa7 100644
--- a/tests/src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java
+++ b/tests/src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java
@@ -177,8 +177,6 @@
      */
     @Test
     @PortraitLandscape
-    @ScreenRecordRule.ScreenRecord // b/338869019
-    @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/338869019
     public void testAddDeleteShortcutOnHotseat() {
         mLauncher.getWorkspace()
                 .deleteAppIcon(mLauncher.getWorkspace().getHotseatAppIcon(0))
diff --git a/tests/src/com/android/launcher3/folder/FolderTest.kt b/tests/src/com/android/launcher3/folder/FolderTest.kt
new file mode 100644
index 0000000..e1daa74
--- /dev/null
+++ b/tests/src/com/android/launcher3/folder/FolderTest.kt
@@ -0,0 +1,86 @@
+/*
+ * 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.folder
+
+import android.content.Context
+import android.graphics.Point
+import android.view.View
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.DropTarget.DragObject
+import com.android.launcher3.LauncherAppState
+import com.android.launcher3.celllayout.board.FolderPoint
+import com.android.launcher3.celllayout.board.TestWorkspaceBuilder
+import com.android.launcher3.util.ActivityContextWrapper
+import com.android.launcher3.util.ModelTestExtensions.clearModelDb
+import junit.framework.TestCase.assertEquals
+import org.junit.After
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+/** Tests for [Folder] */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class FolderTest {
+
+    private val context: Context =
+        ActivityContextWrapper(ApplicationProvider.getApplicationContext())
+    private val workspaceBuilder = TestWorkspaceBuilder(context)
+    private val folder: Folder = Mockito.spy(Folder(context, null))
+
+    @After
+    fun tearDown() {
+        LauncherAppState.getInstance(context).model.clearModelDb()
+    }
+
+    @Test
+    fun `Undo a folder with 1 icon when onDropCompleted is called`() {
+        val folderInfo =
+            workspaceBuilder.createFolderInCell(FolderPoint(Point(1, 0), TWO_ICON_FOLDER_TYPE), 0)
+        folder.mInfo = folderInfo
+        folder.mInfo.getContents().removeAt(0)
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+        val dragLayout = Mockito.mock(View::class.java)
+        val dragObject = Mockito.mock(DragObject::class.java)
+        assertEquals(folder.deleteFolderOnDropCompleted, false)
+        folder.onDropCompleted(dragLayout, dragObject, true)
+        verify(folder, times(1)).replaceFolderWithFinalItem()
+        assertEquals(folder.deleteFolderOnDropCompleted, false)
+    }
+
+    @Test
+    fun `Do not undo a folder with 2 icons when onDropCompleted is called`() {
+        val folderInfo =
+            workspaceBuilder.createFolderInCell(FolderPoint(Point(1, 0), TWO_ICON_FOLDER_TYPE), 0)
+        folder.mInfo = folderInfo
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+        val dragLayout = Mockito.mock(View::class.java)
+        val dragObject = Mockito.mock(DragObject::class.java)
+        assertEquals(folder.deleteFolderOnDropCompleted, false)
+        folder.onDropCompleted(dragLayout, dragObject, true)
+        verify(folder, times(0)).replaceFolderWithFinalItem()
+        assertEquals(folder.deleteFolderOnDropCompleted, false)
+    }
+
+    companion object {
+        const val TWO_ICON_FOLDER_TYPE = 'A'
+    }
+}
diff --git a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
index 6e01f9e..3d253b4 100644
--- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
+++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
@@ -22,6 +22,7 @@
 import static com.android.launcher3.testing.shared.TestProtocol.ICON_MISSING;
 import static com.android.launcher3.testing.shared.TestProtocol.WIDGET_CONFIG_NULL_EXTRA_INTENT;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
@@ -238,9 +239,9 @@
     protected void clearPackageData(String pkg) throws IOException, InterruptedException {
         final CountDownLatch count = new CountDownLatch(2);
         final SimpleBroadcastReceiver broadcastReceiver =
-                new SimpleBroadcastReceiver(i -> count.countDown());
+                new SimpleBroadcastReceiver(UI_HELPER_EXECUTOR, i -> count.countDown());
         // We OK to make binder calls on main thread in test.
-        broadcastReceiver.registerPkgActionsSync(mTargetContext, pkg,
+        broadcastReceiver.registerPkgActions(mTargetContext, pkg,
                 Intent.ACTION_PACKAGE_RESTARTED, Intent.ACTION_PACKAGE_DATA_CLEARED);
 
         mDevice.executeShellCommand("pm clear " + pkg);
diff --git a/tests/src/com/android/launcher3/ui/widget/TaplBindWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/TaplBindWidgetTest.java
index 28d1faa..d40d3bc 100644
--- a/tests/src/com/android/launcher3/ui/widget/TaplBindWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/TaplBindWidgetTest.java
@@ -23,8 +23,6 @@
 import static com.android.launcher3.provider.LauncherDbUtils.itemIdMatch;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 import static com.android.launcher3.util.WidgetUtils.createWidgetInfo;
-import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL;
-import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
@@ -56,7 +54,6 @@
 import com.android.launcher3.ui.AbstractLauncherUiTest;
 import com.android.launcher3.ui.TestViewHelpers;
 import com.android.launcher3.util.rule.ShellCommandRule;
-import com.android.launcher3.util.rule.TestStabilityRule;
 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.WidgetManagerHelper;
 
@@ -143,7 +140,6 @@
     }
 
     @Test
-    @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/310242894
     public void testPendingWidget_withConfigScreen() {
         // A non-restored widget with config screen get bound and shows a 'Click to setup' UI.
         // Do not bind the widget
@@ -193,7 +189,6 @@
     }
 
     @Test
-    @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/310242894
     public void testPendingWidget_notRestored_brokenInstall() {
         // A widget which is was being installed once, even if its not being
         // installed at the moment is not removed.
diff --git a/tests/src/com/android/launcher3/ui/workspace/TaplThemeIconsTest.java b/tests/src/com/android/launcher3/ui/workspace/TaplThemeIconsTest.java
index d653317..a148744 100644
--- a/tests/src/com/android/launcher3/ui/workspace/TaplThemeIconsTest.java
+++ b/tests/src/com/android/launcher3/ui/workspace/TaplThemeIconsTest.java
@@ -16,6 +16,8 @@
 package com.android.launcher3.ui.workspace;
 
 import static com.android.launcher3.util.TestConstants.AppNames.TEST_APP_NAME;
+import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL;
+import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
@@ -38,6 +40,8 @@
 import com.android.launcher3.tapl.HomeAppIconMenuItem;
 import com.android.launcher3.ui.AbstractLauncherUiTest;
 import com.android.launcher3.util.Executors;
+import com.android.launcher3.util.rule.ScreenRecordRule;
+import com.android.launcher3.util.rule.TestStabilityRule;
 
 import org.junit.Test;
 
@@ -111,6 +115,8 @@
     }
 
     @Test
+    @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/350557998
+    @ScreenRecordRule.ScreenRecord // b/350557998
     public void testShortcutIconWithTheme() throws Exception {
         setThemeEnabled(true);
         initialize(this);
diff --git a/tests/src/com/android/launcher3/ui/workspace/TaplTwoPanelWorkspaceTest.java b/tests/src/com/android/launcher3/ui/workspace/TaplTwoPanelWorkspaceTest.java
index e92d641..eb05000 100644
--- a/tests/src/com/android/launcher3/ui/workspace/TaplTwoPanelWorkspaceTest.java
+++ b/tests/src/com/android/launcher3/ui/workspace/TaplTwoPanelWorkspaceTest.java
@@ -113,8 +113,6 @@
 
     @Test
     @PortraitLandscape
-    @ScreenRecordRule.ScreenRecord // b/329935119
-    @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/329935119
     public void testSinglePageDragIconWhenMultiplePageScrollingIsPossible() {
         Workspace workspace = mLauncher.getWorkspace();
 
@@ -169,6 +167,7 @@
 
     @Test
     @PortraitLandscape
+    @ScreenRecordRule.ScreenRecord // b/352130094
     public void testDragIconToPage2() {
         Workspace workspace = mLauncher.getWorkspace();
 
@@ -247,7 +246,6 @@
     }
 
     @ScreenRecordRule.ScreenRecord // b/329935119
-    @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/329935119
     @Test
     @PortraitLandscape
     public void testEmptyPageDoesNotGetRemovedIfPagePairIsNotEmpty() {
diff --git a/tests/src/com/android/launcher3/util/SimpleBroadcastReceiverTest.kt b/tests/src/com/android/launcher3/util/SimpleBroadcastReceiverTest.kt
new file mode 100644
index 0000000..1de99c5
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/SimpleBroadcastReceiverTest.kt
@@ -0,0 +1,158 @@
+/*
+ * 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.util
+
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.os.Handler
+import android.os.Looper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR
+import com.google.common.truth.Truth.assertThat
+import java.util.function.Consumer
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.ArgumentMatchers.same
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.verify
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class SimpleBroadcastReceiverTest {
+
+    private lateinit var underTest: SimpleBroadcastReceiver
+
+    @Mock private lateinit var intentConsumer: Consumer<Intent>
+    @Mock private lateinit var context: Context
+    @Mock private lateinit var completionRunnable: Runnable
+    @Captor private lateinit var intentFilterCaptor: ArgumentCaptor<IntentFilter>
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        underTest = SimpleBroadcastReceiver(UI_HELPER_EXECUTOR, intentConsumer)
+        if (Looper.getMainLooper() == null) {
+            Looper.prepareMainLooper()
+        }
+    }
+
+    @Test
+    fun async_register() {
+        underTest.register(context, "test_action_1", "test_action_2")
+        awaitTasksCompleted()
+
+        verify(context).registerReceiver(same(underTest), intentFilterCaptor.capture())
+        val intentFilter = intentFilterCaptor.value
+        assertThat(intentFilter.countActions()).isEqualTo(2)
+        assertThat(intentFilter.getAction(0)).isEqualTo("test_action_1")
+        assertThat(intentFilter.getAction(1)).isEqualTo("test_action_2")
+    }
+
+    @Test
+    fun async_register_withCompletionRunnable() {
+        underTest.register(context, completionRunnable, "test_action_1", "test_action_2")
+        awaitTasksCompleted()
+
+        verify(context).registerReceiver(same(underTest), intentFilterCaptor.capture())
+        verify(completionRunnable).run()
+        val intentFilter = intentFilterCaptor.value
+        assertThat(intentFilter.countActions()).isEqualTo(2)
+        assertThat(intentFilter.getAction(0)).isEqualTo("test_action_1")
+        assertThat(intentFilter.getAction(1)).isEqualTo("test_action_2")
+    }
+
+    @Test
+    fun async_register_withCompletionRunnable_and_flag() {
+        underTest.register(context, completionRunnable, 1, "test_action_1", "test_action_2")
+        awaitTasksCompleted()
+
+        verify(context).registerReceiver(same(underTest), intentFilterCaptor.capture(), eq(1))
+        verify(completionRunnable).run()
+        val intentFilter = intentFilterCaptor.value
+        assertThat(intentFilter.countActions()).isEqualTo(2)
+        assertThat(intentFilter.getAction(0)).isEqualTo("test_action_1")
+        assertThat(intentFilter.getAction(1)).isEqualTo("test_action_2")
+    }
+
+    @Test
+    fun async_register_with_package() {
+        underTest.registerPkgActions(context, "pkg", "test_action_1", "test_action_2")
+
+        awaitTasksCompleted()
+        verify(context).registerReceiver(same(underTest), intentFilterCaptor.capture())
+        val intentFilter = intentFilterCaptor.value
+        assertThat(intentFilter.getDataScheme(0)).isEqualTo("package")
+        assertThat(intentFilter.getDataSchemeSpecificPart(0).path).isEqualTo("pkg")
+        assertThat(intentFilter.countActions()).isEqualTo(2)
+        assertThat(intentFilter.getAction(0)).isEqualTo("test_action_1")
+        assertThat(intentFilter.getAction(1)).isEqualTo("test_action_2")
+    }
+
+    @Test
+    fun sync_register_withCompletionRunnable_and_flag() {
+        underTest = SimpleBroadcastReceiver(Handler(Looper.getMainLooper()), intentConsumer)
+
+        underTest.register(context, completionRunnable, 1, "test_action_1", "test_action_2")
+
+        verify(context).registerReceiver(same(underTest), intentFilterCaptor.capture(), eq(1))
+        verify(completionRunnable).run()
+        val intentFilter = intentFilterCaptor.value
+        assertThat(intentFilter.countActions()).isEqualTo(2)
+        assertThat(intentFilter.getAction(0)).isEqualTo("test_action_1")
+        assertThat(intentFilter.getAction(1)).isEqualTo("test_action_2")
+    }
+
+    @Test
+    fun async_unregister() {
+        underTest.unregisterReceiverSafely(context)
+
+        awaitTasksCompleted()
+        verify(context).unregisterReceiver(same(underTest))
+    }
+
+    @Test
+    fun sync_unregister() {
+        underTest = SimpleBroadcastReceiver(Handler(Looper.getMainLooper()), intentConsumer)
+
+        underTest.unregisterReceiverSafely(context)
+
+        verify(context).unregisterReceiver(same(underTest))
+    }
+
+    @Test
+    fun getPackageFilter() {
+        val intentFilter =
+            SimpleBroadcastReceiver.getPackageFilter("pkg", "test_action_1", "test_action_2")
+
+        assertThat(intentFilter.getDataScheme(0)).isEqualTo("package")
+        assertThat(intentFilter.getDataSchemeSpecificPart(0).path).isEqualTo("pkg")
+        assertThat(intentFilter.countActions()).isEqualTo(2)
+        assertThat(intentFilter.getAction(0)).isEqualTo("test_action_1")
+        assertThat(intentFilter.getAction(1)).isEqualTo("test_action_2")
+    }
+
+    private fun awaitTasksCompleted() {
+        UI_HELPER_EXECUTOR.submit<Any> { null }.get()
+    }
+}
diff --git a/tests/src/com/android/launcher3/util/ViewCacheTest.kt b/tests/src/com/android/launcher3/util/ViewCacheTest.kt
new file mode 100644
index 0000000..bad21c9
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/ViewCacheTest.kt
@@ -0,0 +1,89 @@
+/*
+ * 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.util
+
+import android.view.View
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.launcher3.R
+import com.android.launcher3.util.ViewCache.CacheEntry
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class ViewCacheTest {
+
+    private lateinit var underTest: ViewCache
+
+    private val context = InstrumentationRegistry.getInstrumentation().context
+    private val layoutId =
+        context.run { resources.getIdentifier("test_layout_appwidget_blue", "layout", packageName) }
+
+    @Before
+    fun setUp() {
+        underTest = ViewCache()
+        underTest.setCacheSize(layoutId, 5)
+    }
+
+    @Test
+    fun get_view_from_empty_cache() {
+        val view: View = underTest.getView(layoutId, context, null)
+
+        val cacheEntry = view.getTag(R.id.cache_entry_tag_id) as ViewCache.CacheEntry
+        assertThat(cacheEntry).isNotNull()
+        assertThat(cacheEntry.mMaxSize).isEqualTo(5)
+        assertThat(cacheEntry.mCurrentSize).isEqualTo(0)
+        assertThat(cacheEntry.mViews[0]).isNull()
+    }
+
+    @Test
+    fun recyclerView() {
+        val view: View = underTest.getView(layoutId, context, null)
+        val cacheEntry = view.getTag(R.id.cache_entry_tag_id) as ViewCache.CacheEntry
+
+        underTest.recycleView(layoutId, view)
+
+        assertThat(cacheEntry.mMaxSize).isEqualTo(5)
+        assertThat(cacheEntry.mCurrentSize).isEqualTo(1)
+        assertThat(cacheEntry.mViews[0]).isSameInstanceAs(view)
+    }
+
+    @Test
+    fun get_view_from_cache() {
+        val view: View = underTest.getView(layoutId, context, null)
+        underTest.recycleView(layoutId, view)
+
+        val newView = underTest.getView<View>(layoutId, context, null)
+
+        assertThat(view).isSameInstanceAs(newView)
+    }
+
+    @Test
+    fun change_tag_id_recyclerView_noOp() {
+        val view: View = underTest.getView(layoutId, context, null)
+        val cacheEntry = view.getTag(R.id.cache_entry_tag_id) as ViewCache.CacheEntry
+
+        view.setTag(R.id.cache_entry_tag_id, CacheEntry(3))
+        underTest.recycleView(layoutId, view)
+
+        assertThat(cacheEntry.mMaxSize).isEqualTo(5)
+        assertThat(cacheEntry.mCurrentSize).isEqualTo(0)
+        assertThat(cacheEntry.mViews[0]).isNull()
+    }
+}