[automerger skipped] Merge 24Q4 (ab/12406339) into aosp-main-future am: 9a9e10609d -s ours

am skip reason: Merged-In Id70ceed141e2106f746d4a0e68a09675ba45dd28 with SHA-1 9c09196dfd is already in history

Original change: https://googleplex-android-review.googlesource.com/c/platform/packages/apps/Launcher3/+/30283672

Change-Id: Ib83e9eb3b5c75912b329391a3aab99747efb6b0e
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/Android.bp b/Android.bp
index 4e1c9d3..df96156 100644
--- a/Android.bp
+++ b/Android.bp
@@ -25,6 +25,7 @@
     name: "launcher-non-platform-apis-defaults",
     static_libs: [
         "android.os.flags-aconfig-java",
+        "android.multiuser.flags-aconfig-java",
         "android.appwidget.flags-aconfig-java",
         "com.android.window.flags.window-aconfig-java",
     ],
@@ -64,9 +65,74 @@
 filegroup {
     name: "launcher-quickstep-src",
     srcs: [
-        "quickstep/src/**/*.java",
         "quickstep/src/**/*.kt",
+        "quickstep/src/**/*.java",
     ],
+    device_common_srcs: [
+        ":launcher-quickstep-processed-protolog-src",
+    ],
+}
+
+// Launcher ProtoLog support
+filegroup {
+    name: "launcher-quickstep-unprocessed-protolog-src",
+    srcs: [
+        "quickstep/src_protolog/**/*.java",
+    ],
+}
+
+java_library {
+    name: "launcher-quickstep_protolog-groups",
+    srcs: [
+        "quickstep/src_protolog/**/*.java",
+    ],
+    static_libs: [
+        "protolog-group",
+        "androidx.annotation_annotation",
+        "com_android_launcher3_flags_lib",
+    ],
+}
+
+java_genrule {
+    name: "launcher-quickstep-processed-protolog-src",
+    srcs: [
+        ":protolog-impl",
+        ":launcher-quickstep-unprocessed-protolog-src",
+        ":launcher-quickstep_protolog-groups",
+    ],
+    tools: ["protologtool"],
+    cmd: "$(location protologtool) transform-protolog-calls " +
+        "--protolog-class com.android.internal.protolog.common.ProtoLog " +
+        "--loggroups-class com.android.quickstep.util.QuickstepProtoLogGroup " +
+        "--loggroups-jar $(location :launcher-quickstep_protolog-groups) " +
+        "--viewer-config-file-path /system_ext/etc/launcher.quickstep.protolog.pb " +
+        "--output-srcjar $(out) " +
+        "$(locations :launcher-quickstep-unprocessed-protolog-src)",
+    out: ["launcher.quickstep.protolog.srcjar"],
+}
+
+java_genrule {
+    name: "gen-launcher.quickstep.protolog.pb",
+    srcs: [
+        ":launcher-quickstep-unprocessed-protolog-src",
+        ":launcher-quickstep_protolog-groups",
+    ],
+    tools: ["protologtool"],
+    cmd: "$(location protologtool) generate-viewer-config " +
+        "--protolog-class com.android.internal.protolog.common.ProtoLog " +
+        "--loggroups-class com.android.quickstep.util.QuickstepProtoLogGroup " +
+        "--loggroups-jar $(location :launcher-quickstep_protolog-groups) " +
+        "--viewer-config-type proto " +
+        "--viewer-config $(out) " +
+        "$(locations :launcher-quickstep-unprocessed-protolog-src)",
+    out: ["launcher.quickstep.protolog.pb"],
+}
+
+prebuilt_etc {
+    name: "launcher.quickstep.protolog.pb",
+    system_ext_specific: true,
+    src: ":gen-launcher.quickstep.protolog.pb",
+    filename_from_src: true,
 }
 
 // Source code for quickstep dagger
@@ -322,6 +388,7 @@
         "//frameworks/libs/systemui:view_capture",
         "//frameworks/libs/systemui:animationlib",
         "//frameworks/libs/systemui:contextualeducationlib",
+        "//frameworks/libs/systemui:msdl",
         "SystemUI-statsd",
         "launcher-testing-shared",
         "androidx.lifecycle_lifecycle-common-java8",
@@ -405,6 +472,7 @@
         "SystemUISharedLib",
         "SettingsLibSettingsTheme",
         "dagger2",
+        "protolog-group",
     ],
     manifest: "quickstep/AndroidManifest.xml",
     min_sdk_version: "current",
@@ -503,7 +571,10 @@
         "Launcher2",
         "Launcher3",
     ],
-    required: ["privapp_whitelist_com.android.launcher3"],
+    required: [
+        "privapp_whitelist_com.android.launcher3",
+        "launcher.quickstep.protolog.pb",
+    ],
 
     resource_dirs: ["quickstep/res"],
 
diff --git a/aconfig/launcher.aconfig b/aconfig/launcher.aconfig
index 4d6c7ab..878aa6e 100644
--- a/aconfig/launcher.aconfig
+++ b/aconfig/launcher.aconfig
@@ -65,6 +65,13 @@
 }
 
 flag {
+    name: "enable_taskbar_connected_displays"
+    namespace: "launcher"
+    description: "Enables connected displays in taskbar."
+    bug: "362720616"
+}
+
+flag {
     name: "enable_taskbar_customization"
     namespace: "launcher"
     description: "Enables taskbar customization framework."
@@ -304,6 +311,13 @@
 }
 
 flag {
+    name: "all_apps_sheet_for_handheld"
+    namespace: "launcher"
+    description: "All Apps will be presented on a bottom sheet in handheld mode"
+    bug: "374186088"
+}
+
+flag {
     name: "multiline_search_bar"
     namespace: "launcher"
     description: "Search bar can wrap to multi-line"
@@ -362,6 +376,13 @@
 }
 
 flag {
+    name: "work_scheduler_in_work_profile"
+    namespace: "launcher"
+    description: "Enables work scheduler view above the work pause button in work profile."
+    bug: "361589193"
+}
+
+flag {
     name: "one_grid_specs"
     namespace: "launcher"
     description: "Defines the new specs for grids based on OneGrid"
@@ -426,3 +447,83 @@
     description: "Show recent apps in the taskbar overflow."
     bug: "368119679"
 }
+
+flag {
+    name: "enable_active_gesture_proto_log"
+    namespace: "launcher"
+    description: "Enables tracking active gesture logs in ProtoLog"
+    bug: "293182501"
+}
+
+flag {
+    name: "enable_recents_window_proto_log"
+    namespace: "launcher"
+    description: "Enables tracking recents window logs in ProtoLog"
+    bug: "292269949"
+}
+
+flag {
+    name: "enable_state_manager_proto_log"
+    namespace: "launcher"
+    description: "Enables tracking state manager logs in ProtoLog"
+    bug: "292269949"
+}
+
+flag {
+    name: "coordinate_workspace_scale"
+    namespace: "launcher"
+    description: "Ensure that the workspace and hotseat scale doesn't conflict and transitions smoothly between launching and closing apps"
+    bug: "366403487"
+    metadata {
+      purpose: PURPOSE_BUGFIX
+    }
+}
+
+flag {
+    name: "enable_tiered_widgets_by_default_in_picker"
+    namespace: "launcher"
+    description: "Shows filtered set of widgets by default and an option to show all widgets in the widget picker"
+    bug: "356127021"
+}
+
+flag {
+    name: "show_taskbar_pinning_popup_from_anywhere"
+    namespace: "launcher"
+    description: "Shows the pinning popup view after long-pressing or right-clicking anywhere on the pinned taskbar"
+    bug: "297325541"
+}
+
+flag {
+    name: "enable_launcher_overview_in_window"
+    namespace: "launcher"
+    description: "Enables launcher recents opening inside of a window instead of being hosted in launcher activity."
+    bug: "292269949"
+}
+
+flag {
+   name: "enforce_system_radius_for_app_widgets"
+   namespace: "launcher"
+   description: "Enforce system radius for widget corners instead of a separate 16.dp value"
+   bug: "370950552"
+}
+
+flag {
+    name: "enable_contrast_tiles"
+    namespace: "launcher"
+    description: "Enable launcher app contrast tiles."
+    bug: "341217082"
+}
+
+flag {
+  name: "msdl_feedback"
+  namespace: "launcher"
+  description: "Enable MSDL feedback for Launcher interactions"
+  bug: "377496684"
+}
+
+flag {
+    name: "taskbar_recents_layout_transition"
+    namespace: "launcher"
+    description: "Enable Taskbar LayoutTransition for Recent Apps"
+    bug: "343521765"
+}
\ No newline at end of file
diff --git a/aconfig/launcher_overview.aconfig b/aconfig/launcher_overview.aconfig
index 853faf8..93d8d54 100644
--- a/aconfig/launcher_overview.aconfig
+++ b/aconfig/launcher_overview.aconfig
@@ -36,7 +36,7 @@
     name: "enable_large_desktop_windowing_tile"
     namespace: "launcher_overview"
     description: "Makes the desktop tiles larger and moves them to the front of the list in Overview."
-    bug: "353947137"
+    bug: "357860832"
 }
 
 flag {
@@ -47,4 +47,18 @@
     metadata {
       purpose: PURPOSE_BUGFIX
     }
+}
+
+flag {
+    name: "enable_desktop_windowing_carousel_detach"
+    namespace: "launcher_overview"
+    description: "Makes the desktop windowing task carousel detaches from fullscreen task carousel during quickswitch."
+    bug: "353947917"
+}
+
+flag {
+    name: "enable_desktop_exploded_view"
+    namespace: "launcher_overview"
+    description: "Enables the non-overlapping layout for desktop windows in Overview mode."
+    bug: "378011776"
 }
\ No newline at end of file
diff --git a/go/quickstep/res/values-ne/strings.xml b/go/quickstep/res/values-ne/strings.xml
index e66f063..4f771c3 100644
--- a/go/quickstep/res/values-ne/strings.xml
+++ b/go/quickstep/res/values-ne/strings.xml
@@ -9,11 +9,11 @@
     <string name="dialog_cancel" msgid="6464336969134856366">"रद्द गर्नुहोस्"</string>
     <string name="dialog_settings" msgid="6564397136021186148">"सेटिङ"</string>
     <string name="niu_actions_confirmation_title" msgid="3863451714863526143">"स्क्रिनमा देखिने पाठ अनुवाद गर्नुहोस् वा पढेर सुनाउनुहोस्"</string>
-    <string name="niu_actions_confirmation_text" msgid="2105271481950866089">"तपाईंको स्क्रिनमा देखिने पाठ, वेब ठेगाना र स्क्रिनसटलगायतका जानकारी Google सँग सेयर गर्न सकिन्छ।\n\nकुन कुन जानकारी सेयर गर्न दिने भन्ने सेटिङ बदल्न "<b>"सेटिङ &gt; एप &gt; डिफल्ट एप &gt; डिजिटल सहायक एप"</b>" मा जानुहोस्।"</string>
+    <string name="niu_actions_confirmation_text" msgid="2105271481950866089">"तपाईंको स्क्रिनमा देखिने पाठ, वेब ठेगाना र स्क्रिनसटलगायतका जानकारी Google सँग सेयर गर्न सकिन्छ।\n\nकुन कुन जानकारी सेयर गर्न दिने भन्ने सेटिङ बदल्न "<b>"सेटिङ &gt; एप &gt; डिफल्ट एप &gt; डिजिटल एसिस्टेन्ट एप"</b>" मा जानुहोस्।"</string>
     <string name="assistant_not_selected_title" msgid="5017072974603345228">"तपाईं यो सुविधा चलाउन चाहनुहुन्छ भने कुनै सहायक छनौट गर्नुहोस्"</string>
-    <string name="assistant_not_selected_text" msgid="3244613673884359276">"तपाईं आफ्नो स्क्रिनमा देखिने पाठ सुन्न वा अनुवाद गर्न चाहनुहुन्छ भने सेटिङमा गई कुनै डिजिटल सहायक एप छनौट गर्नुहोस्"</string>
+    <string name="assistant_not_selected_text" msgid="3244613673884359276">"तपाईं आफ्नो स्क्रिनमा देखिने पाठ सुन्न वा अनुवाद गर्न चाहनुहुन्छ भने सेटिङमा गई कुनै डिजिटल एसिस्टेन्ट एप छनौट गर्नुहोस्"</string>
     <string name="assistant_not_supported_title" msgid="1675788067597484142">"तपाईं यो सुविधा चलाउन चाहनुहुन्छ भने आफ्नो सहायक परिवर्तन गर्नुहोस्"</string>
-    <string name="assistant_not_supported_text" msgid="1708031078549268884">"तपाईं आफ्नो स्क्रिनमा देखिने पाठ सुन्न वा अनुवाद गर्न चाहनुहुन्छ भने सेटिङमा गई कुनै डिजिटल सहायक एप परिर्वर्तन गर्नुहोस्"</string>
+    <string name="assistant_not_supported_text" msgid="1708031078549268884">"तपाईं आफ्नो स्क्रिनमा देखिने पाठ सुन्न वा अनुवाद गर्न चाहनुहुन्छ भने सेटिङमा गई कुनै डिजिटल एसिस्टेन्ट एप परिर्वर्तन गर्नुहोस्"</string>
     <string name="tooltip_listen" msgid="7634466447860989102">"तपाईं यो स्क्रिनमा देखिने पाठ सुन्न चाहनुहुन्छ यहाँ ट्याप गर्नुहोस्"</string>
     <string name="tooltip_translate" msgid="4184845868901542567">"तपाईं यो स्क्रिनमा देखिने पाठ अनुवाद गर्न चाहनुहुन्छ यहाँ ट्याप गर्नुहोस्"</string>
     <string name="toast_p2p_app_not_shareable" msgid="7229739094132131536">"यो एप अरूलाई चलाउन दिन मिल्दैन"</string>
diff --git a/go/quickstep/src/com/android/launcher3/AppSharing.java b/go/quickstep/src/com/android/launcher3/AppSharing.java
index e15b132..a97fecc 100644
--- a/go/quickstep/src/com/android/launcher3/AppSharing.java
+++ b/go/quickstep/src/com/android/launcher3/AppSharing.java
@@ -37,12 +37,13 @@
 
 import androidx.core.content.FileProvider;
 
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.model.AppShareabilityChecker;
 import com.android.launcher3.model.AppShareabilityJobService;
 import com.android.launcher3.model.AppShareabilityManager;
 import com.android.launcher3.model.AppShareabilityManager.ShareabilityStatus;
 import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.popup.PopupDataProvider;
 import com.android.launcher3.popup.SystemShortcut;
 import com.android.launcher3.views.ActivityContext;
 
@@ -114,17 +115,17 @@
      * The Share App system shortcut, used to initiate p2p sharing of a given app
      */
     public final class Share extends SystemShortcut<Launcher> {
-        private final PopupDataProvider mPopupDataProvider;
         private final boolean mSharingEnabledForUser;
 
         private final Set<View> mBoundViews = Collections.newSetFromMap(new WeakHashMap<>());
         private boolean mIsEnabled = true;
+        private StatsLogManager mStatsLogManager;
 
         public Share(Launcher target, ItemInfo itemInfo, View originalView) {
             super(R.drawable.ic_share, R.string.app_share_drop_target_label, target, itemInfo,
                     originalView);
-            mPopupDataProvider = target.getPopupDataProvider();
-
+            mStatsLogManager = ActivityContext.lookupContext(originalView.getContext())
+                    .getStatsLogManager();
             mSharingEnabledForUser = bluetoothSharingEnabled(target);
             if (!mSharingEnabledForUser) {
                 setEnabled(false);
@@ -150,8 +151,7 @@
 
         @Override
         public void onClick(View view) {
-            ActivityContext.lookupContext(view.getContext())
-                    .getStatsLogManager().logger().log(LAUNCHER_SYSTEM_SHORTCUT_APP_SHARE_TAP);
+            mStatsLogManager.logger().log(LAUNCHER_SYSTEM_SHORTCUT_APP_SHARE_TAP);
             if (!mIsEnabled) {
                 showCannotShareToast(view.getContext());
                 return;
@@ -240,6 +240,11 @@
         public boolean isEnabled() {
             return mIsEnabled;
         }
+
+        @VisibleForTesting
+        void setStatsLogManager(StatsLogManager statsLogManager) {
+            mStatsLogManager = statsLogManager;
+        }
     }
 
     /**
diff --git a/go/quickstep/src/com/android/quickstep/TaskOverlayFactoryGo.java b/go/quickstep/src/com/android/quickstep/TaskOverlayFactoryGo.java
index 0fb9718..8c2f5d5 100644
--- a/go/quickstep/src/com/android/quickstep/TaskOverlayFactoryGo.java
+++ b/go/quickstep/src/com/android/quickstep/TaskOverlayFactoryGo.java
@@ -149,8 +149,7 @@
             // Disable Overview Actions for Work Profile apps
             boolean isManagedProfileTask =
                     UserManager.get(mApplicationContext).isManagedProfile(task.key.userId);
-            boolean isAllowedByPolicy = mTaskContainer.getThumbnailViewDeprecated().isRealSnapshot()
-                    && !isManagedProfileTask;
+            boolean isAllowedByPolicy = isRealSnapshot() && !isManagedProfileTask;
             getActionsView().setCallbacks(new OverlayUICallbacksGoImpl(isAllowedByPolicy, task));
             mTaskPackageName = task.key.getPackageName();
             mSharedPreferences = LauncherPrefs.getPrefs(mApplicationContext);
diff --git a/proguard.flags b/proguard.flags
index 31edd8d..da00c00 100644
--- a/proguard.flags
+++ b/proguard.flags
@@ -57,3 +57,7 @@
 -keep class com.android.quickstep.** {
   *;
 }
+
+-keep class com.android.internal.protolog.** {
+  *;
+}
diff --git a/quickstep/AndroidManifest.xml b/quickstep/AndroidManifest.xml
index bf198b6..57bfb4a 100644
--- a/quickstep/AndroidManifest.xml
+++ b/quickstep/AndroidManifest.xml
@@ -54,6 +54,7 @@
         android:protectionLevel="signature|privileged" />
 
     <application android:backupAgent="com.android.launcher3.LauncherBackupAgent"
+         android:enableOnBackInvokedCallback="true"
          android:fullBackupOnly="true"
          android:fullBackupContent="@xml/backupscheme"
          android:hardwareAccelerated="true"
diff --git a/quickstep/res/color/taskbar_minimized_app_indicator_color.xml b/quickstep/res/color/taskbar_minimized_app_indicator_color.xml
new file mode 100644
index 0000000..1596fe1
--- /dev/null
+++ b/quickstep/res/color/taskbar_minimized_app_indicator_color.xml
@@ -0,0 +1,19 @@
+<?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.
+  -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:color="?attr/materialColorOutline"/>
+</selector>
diff --git a/quickstep/res/color/taskbar_running_app_indicator_color.xml b/quickstep/res/color/taskbar_running_app_indicator_color.xml
new file mode 100644
index 0000000..5dc9781
--- /dev/null
+++ b/quickstep/res/color/taskbar_running_app_indicator_color.xml
@@ -0,0 +1,19 @@
+<?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.
+  -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:color="?attr/materialColorTertiary"/>
+</selector>
diff --git a/quickstep/res/drawable/ic_external_display.xml b/quickstep/res/drawable/ic_external_display.xml
new file mode 100644
index 0000000..64c183e
--- /dev/null
+++ b/quickstep/res/drawable/ic_external_display.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="960"
+    android:viewportHeight="960">
+    <path
+        android:pathData="M320,840v-80L160,760q-33,0 -56.5,-23.5T80,680v-480q0,-33 23.5,-56.5T160,120h640q33,0 56.5,23.5T880,200v480q0,33 -23.5,56.5T800,760L640,760v80L320,840ZM160,680h640v-480L160,200v480ZM160,680v-480,480Z"
+        android:fillColor="#e8eaed"/>
+</vector>
diff --git a/quickstep/res/layout/bubblebar_flyout.xml b/quickstep/res/layout/bubblebar_flyout.xml
index fc1e914..e3338bf 100644
--- a/quickstep/res/layout/bubblebar_flyout.xml
+++ b/quickstep/res/layout/bubblebar_flyout.xml
@@ -19,7 +19,7 @@
     xmlns:tools="http://schemas.android.com/tools">
 
     <ImageView
-        android:id="@+id/bubble_flyout_avatar"
+        android:id="@+id/bubble_flyout_icon"
         android:layout_width="50dp"
         android:layout_height="36dp"
         android:paddingEnd="@dimen/bubblebar_flyout_avatar_message_space"
@@ -30,14 +30,14 @@
         tools:src="#ff0000"/>
 
     <TextView
-        android:id="@+id/bubble_flyout_name"
+        android:id="@+id/bubble_flyout_title"
         android:layout_width="0dp"
         android:layout_height="wrap_content"
         android:fontFamily="@*android:string/config_bodyFontFamilyMedium"
         android:maxLines="1"
         android:ellipsize="end"
         app:layout_constraintTop_toTopOf="parent"
-        app:layout_constraintStart_toEndOf="@id/bubble_flyout_avatar"
+        app:layout_constraintStart_toEndOf="@id/bubble_flyout_icon"
         tools:text="Sender"/>
 
     <TextView
@@ -47,8 +47,8 @@
         android:fontFamily="@*android:string/config_bodyFontFamily"
         android:maxLines="2"
         android:ellipsize="end"
-        app:layout_constraintTop_toBottomOf="@id/bubble_flyout_name"
-        app:layout_constraintStart_toEndOf="@id/bubble_flyout_avatar"
+        app:layout_constraintTop_toBottomOf="@id/bubble_flyout_title"
+        app:layout_constraintStart_toEndOf="@id/bubble_flyout_icon"
         tools:text="This is a message"/>
 
 </merge>
diff --git a/quickstep/res/layout/customizable_taskbar.xml b/quickstep/res/layout/customizable_taskbar.xml
index e1a80ae..d988cbc 100644
--- a/quickstep/res/layout/customizable_taskbar.xml
+++ b/quickstep/res/layout/customizable_taskbar.xml
@@ -51,20 +51,26 @@
         android:layout_width="match_parent"
         android:layout_height="match_parent"/>
 
-    <com.android.launcher3.taskbar.bubbles.BubbleBarView
-        android:id="@+id/taskbar_bubbles"
-        android:layout_width="wrap_content"
-        android:layout_height="@dimen/bubblebar_size_with_pointer"
-        android:layout_gravity="bottom|end"
-        android:layout_marginHorizontal="@dimen/transient_taskbar_bottom_margin"
-        android:paddingTop="@dimen/bubblebar_pointer_visible_size"
-        android:paddingEnd="@dimen/taskbar_icon_spacing"
-        android:paddingStart="@dimen/taskbar_icon_spacing"
-        android:visibility="gone"
-        android:gravity="center"
-        android:clipChildren="false"
-        android:elevation="@dimen/bubblebar_elevation"
-        />
+    <FrameLayout
+        android:id="@+id/taskbar_bubbles_container"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:clipChildren="false">
+
+        <com.android.launcher3.taskbar.bubbles.BubbleBarView
+            android:id="@+id/taskbar_bubbles"
+            android:layout_width="wrap_content"
+            android:layout_height="@dimen/bubblebar_size_with_pointer"
+            android:layout_gravity="bottom|end"
+            android:layout_marginHorizontal="@dimen/transient_taskbar_bottom_margin"
+            android:paddingTop="@dimen/bubblebar_pointer_visible_size"
+            android:paddingEnd="@dimen/taskbar_icon_spacing"
+            android:paddingStart="@dimen/taskbar_icon_spacing"
+            android:visibility="gone"
+            android:gravity="center"
+            android:clipChildren="false"
+            android:elevation="@dimen/bubblebar_elevation" />
+    </FrameLayout>
 
     <com.android.launcher3.taskbar.navbutton.NearestTouchFrame
         android:id="@+id/navbuttons_view"
diff --git a/quickstep/res/layout/task_desktop.xml b/quickstep/res/layout/task_desktop.xml
index 1564653..0472007 100644
--- a/quickstep/res/layout/task_desktop.xml
+++ b/quickstep/res/layout/task_desktop.xml
@@ -19,16 +19,11 @@
     android:id="@+id/task_view_desktop"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:clipChildren="true"
-    android:clipToPadding="true"
     android:contentDescription="@string/recent_task_desktop"
     android:defaultFocusHighlightEnabled="false"
     android:focusable="true"
-    android:padding="0.1dp"
     launcher:focusBorderColor="?attr/materialColorOutline"
     launcher:hoverBorderColor="?attr/materialColorPrimary">
-    <!-- Setting a padding of 0.1 dp since android:clipToPadding needs a non-zero value for
-    padding to work-->
     <View
         android:id="@+id/background"
         android:layout_width="match_parent"
@@ -40,4 +35,9 @@
         android:layout_height="wrap_content"
         android:inflatedId="@id/icon" />
 
+    <com.android.quickstep.views.DesktopTaskContentView
+        android:id="@+id/desktop_content"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
+
 </com.android.quickstep.views.DesktopTaskView>
diff --git a/quickstep/res/layout/task_thumbnail.xml b/quickstep/res/layout/task_thumbnail.xml
index d90d916..afbcdb5 100644
--- a/quickstep/res/layout/task_thumbnail.xml
+++ b/quickstep/res/layout/task_thumbnail.xml
@@ -16,6 +16,7 @@
 <com.android.quickstep.task.thumbnail.TaskThumbnailView
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/snapshot"
     android:layout_width="match_parent"
     android:layout_height="match_parent" >
 
diff --git a/quickstep/res/layout/taskbar.xml b/quickstep/res/layout/taskbar.xml
index e8f3d9d..54f9ae8 100644
--- a/quickstep/res/layout/taskbar.xml
+++ b/quickstep/res/layout/taskbar.xml
@@ -35,17 +35,24 @@
         android:layout_width="match_parent"
         android:layout_height="match_parent"/>
 
-    <com.android.launcher3.taskbar.bubbles.BubbleBarView
-        android:id="@+id/taskbar_bubbles"
-        android:layout_width="wrap_content"
-        android:layout_height="@dimen/bubblebar_size_with_pointer"
-        android:layout_marginHorizontal="@dimen/transient_taskbar_bottom_margin"
-        android:paddingTop="@dimen/bubblebar_pointer_visible_size"
-        android:visibility="gone"
-        android:gravity="center"
-        android:layout_gravity="bottom"
-        android:clipChildren="false"
-        android:elevation="@dimen/bubblebar_elevation" />
+    <FrameLayout
+        android:id="@+id/taskbar_bubbles_container"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:clipChildren="false">
+
+        <com.android.launcher3.taskbar.bubbles.BubbleBarView
+            android:id="@+id/taskbar_bubbles"
+            android:layout_width="wrap_content"
+            android:layout_height="@dimen/bubblebar_size_with_pointer"
+            android:layout_marginHorizontal="@dimen/transient_taskbar_bottom_margin"
+            android:paddingTop="@dimen/bubblebar_pointer_visible_size"
+            android:visibility="gone"
+            android:gravity="center"
+            android:layout_gravity="bottom"
+            android:clipChildren="false"
+            android:elevation="@dimen/bubblebar_elevation" />
+    </FrameLayout>
 
     <com.android.launcher3.taskbar.navbutton.NearestTouchFrame
         android:id="@+id/navbuttons_view"
diff --git a/quickstep/res/layout/taskbar_overflow_button.xml b/quickstep/res/layout/taskbar_overflow_view.xml
similarity index 86%
rename from quickstep/res/layout/taskbar_overflow_button.xml
rename to quickstep/res/layout/taskbar_overflow_view.xml
index 20104f2..7444e59 100644
--- a/quickstep/res/layout/taskbar_overflow_button.xml
+++ b/quickstep/res/layout/taskbar_overflow_view.xml
@@ -15,8 +15,7 @@
 -->
 
 <!-- Note: The actual size will match the taskbar icon sizes in TaskbarView#onLayout(). -->
-<com.android.launcher3.views.IconButtonView xmlns:android="http://schemas.android.com/apk/res/android"
-    style="@style/BaseIcon.Workspace.Taskbar"
+<com.android.launcher3.taskbar.TaskbarOverflowView xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="@dimen/taskbar_icon_min_touch_size"
     android:layout_height="@dimen/taskbar_icon_min_touch_size"
     android:backgroundTint="@android:color/transparent"
diff --git a/quickstep/res/layout/transient_taskbar.xml b/quickstep/res/layout/transient_taskbar.xml
index f3c3383..3ec8046 100644
--- a/quickstep/res/layout/transient_taskbar.xml
+++ b/quickstep/res/layout/transient_taskbar.xml
@@ -38,18 +38,24 @@
         android:layout_width="match_parent"
         android:layout_height="match_parent"/>
 
-    <com.android.launcher3.taskbar.bubbles.BubbleBarView
-        android:id="@+id/taskbar_bubbles"
-        android:layout_width="wrap_content"
-        android:layout_height="@dimen/bubblebar_size_with_pointer"
-        android:layout_gravity="bottom|end"
-        android:layout_marginHorizontal="@dimen/transient_taskbar_bottom_margin"
-        android:paddingTop="@dimen/bubblebar_pointer_visible_size"
-        android:visibility="gone"
-        android:gravity="center"
-        android:clipChildren="false"
-        android:elevation="@dimen/bubblebar_elevation"
-        />
+    <FrameLayout
+        android:id="@+id/taskbar_bubbles_container"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:clipChildren="false">
+
+        <com.android.launcher3.taskbar.bubbles.BubbleBarView
+            android:id="@+id/taskbar_bubbles"
+            android:layout_width="wrap_content"
+            android:layout_height="@dimen/bubblebar_size_with_pointer"
+            android:layout_gravity="bottom|end"
+            android:layout_marginHorizontal="@dimen/transient_taskbar_bottom_margin"
+            android:paddingTop="@dimen/bubblebar_pointer_visible_size"
+            android:visibility="gone"
+            android:gravity="center"
+            android:clipChildren="false"
+            android:elevation="@dimen/bubblebar_elevation" />
+    </FrameLayout>
 
     <com.android.launcher3.taskbar.navbutton.NearestTouchFrame
         android:id="@+id/navbuttons_view"
diff --git a/quickstep/res/values-af/strings.xml b/quickstep/res/values-af/strings.xml
index 4122637..eac8043 100644
--- a/quickstep/res/values-af/strings.xml
+++ b/quickstep/res/values-af/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"Speld vas"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"Vormvry"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"Rekenaar"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"Skuif na eksterne skerm"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"Werkskerm"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"Geen onlangse items nie"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"Programgebruikinstellings"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Wys altyd Taakbalk"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Verander navigasiemodus"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Taakbalkverdeler"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Taakbalkoorloop"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Skuif na links bo"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Skuif na regs onder"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{meer app}other{meer apps}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"Maak almal toe"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"vou <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g> uit"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"vou <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g> in"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"Omkring en Soek"</string>
 </resources>
diff --git a/quickstep/res/values-am/strings.xml b/quickstep/res/values-am/strings.xml
index 956767ee..b9ee381 100644
--- a/quickstep/res/values-am/strings.xml
+++ b/quickstep/res/values-am/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"ሰካ"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"ነፃ ቅጽ"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"ዴስክቶፕ"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"ወደ ውጫዊ ማሳያ አንቀሳቅስ"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"ዴስክቶፕ"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"ምንም የቅርብ ጊዜ ንጥሎች የሉም"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"የመተግበሪያ አጠቃቀም ቅንብሮች"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"ሁልጊዜ የተግባር አሞሌ ያሳዩ"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"የአሰሳ ሁነታን ይለውጡ"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"የተግባር አሞሌ አካፋይ"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"የተግባር አሞሌ ትርፍ ፍሰት"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"ወደ ላይ/ግራ ይውሰዱ"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"ወደ ታች/ቀኝ ይውሰዱ"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{ተጨማሪ መተግበሪያ}one{ተጨማሪ መተግበሪያ}other{ተጨማሪ መተግበሪያዎች}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"ሁሉንም አሰናብት"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>ን ዘርጋ"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>ን ሰብስብ"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"ለመፈለግ ክበብ"</string>
 </resources>
diff --git a/quickstep/res/values-ar/strings.xml b/quickstep/res/values-ar/strings.xml
index e691663..b699d93 100644
--- a/quickstep/res/values-ar/strings.xml
+++ b/quickstep/res/values-ar/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"تثبيت"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"شكل مجاني"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"الكمبيوتر المكتبي"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"نقل التطبيق إلى شاشة خارجية"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"كمبيوتر مكتبي"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"ما مِن عناصر تم استخدامها مؤخرًا"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"إعدادات استخدام التطبيق"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"إظهار شريط التطبيقات دائمًا"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"تغيير وضع التنقل"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"مقسِّم شريط التطبيقات"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"القائمة الكاملة لشريط التطبيقات"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"الانتقال إلى يمين الشاشة أو أعلاها"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"الانتقال إلى يسار الشاشة أو أسفلها"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{تطبيق واحد آخر}zero{تطبيق آخر}two{تطبيقان آخران}few{تطبيقات أخرى}many{تطبيقًا آخر}other{تطبيق آخر}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"إغلاق الكل"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"توسيع <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"تصغير <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"دائرة البحث"</string>
 </resources>
diff --git a/quickstep/res/values-as/strings.xml b/quickstep/res/values-as/strings.xml
index 912003b..7599530 100644
--- a/quickstep/res/values-as/strings.xml
+++ b/quickstep/res/values-as/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"পিন"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"Freeform"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"ডেস্কটপ"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"বাহ্যিক ডিছপ্লে’লৈ নিয়ক"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"ডেস্কটপ"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"কোনো শেহতীয়া বস্তু নাই"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"এপে ব্যৱহাৰ কৰা ডেটাৰ ছেটিং"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"টাস্কবাৰ সদায় দেখুৱাওক"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"নেভিগেশ্বন ম’ড সলনি কৰক"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"টাস্কবাৰ বিভাজক"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"টাস্কবাৰ অ’ভাৰফ্ল"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"ওপৰৰ বাঁওফাললৈ নিয়ক"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"তলৰ সোঁফাললৈ নিয়ক"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{অধিক এপ্‌}one{অধিক এপ্‌}other{অধিক এপ্‌}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"সকলো অগ্ৰাহ্য কৰক"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g> বিস্তাৰ কৰক"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g> সংকোচন কৰক"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"সন্ধান কৰিবৰ বাবে বৃত্ত"</string>
 </resources>
diff --git a/quickstep/res/values-az/strings.xml b/quickstep/res/values-az/strings.xml
index b89b707..6c5748d 100644
--- a/quickstep/res/values-az/strings.xml
+++ b/quickstep/res/values-az/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"Sancın"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"Sərbəst rejim"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"Masaüstü"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"Xarici displeyə köçürün"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"Masaüstü"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"Son elementlər yoxdur"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"Tətbiq istifadə ayarları"</string>
@@ -68,7 +69,7 @@
     <string name="home_gesture_intro_title" msgid="836590312858441830">"Əsas səhifəyə keçmək üçün sürüşdürün"</string>
     <string name="home_gesture_intro_subtitle" msgid="2632238748497975326">"Ekranın aşağısından yuxarısına sürüşdürün. Bu jest həmişə Əsas səhifəyə aparır."</string>
     <string name="home_gesture_spoken_intro_subtitle" msgid="1030987707382031750">"2 barmaqla ekranın aşağısından yuxarısına sürüşdürün. Bu jest həmişə Əsas səhifəyə aparır."</string>
-    <string name="home_gesture_tutorial_title" msgid="3126834347496917376">"Əsas səhifəyə qayıdın"</string>
+    <string name="home_gesture_tutorial_title" msgid="3126834347496917376">"Əsas səhifəyə keçin"</string>
     <string name="home_gesture_tutorial_subtitle" msgid="7245995490408668778">"Ekranın aşağısından yuxarı sürüşdürün"</string>
     <string name="home_gesture_tutorial_success" msgid="1736295017642244751">"Əla!"</string>
     <string name="overview_gesture_feedback_swipe_too_far_from_edge" msgid="6402349235265407385">"Ekranın aşağı kənarından yuxarı sürüşdürün"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"İşləmə paneli həmişə görünsün"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Naviqasiya rejimini dəyişin"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"İşləmə paneli ayırıcısı"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Tapşırıqlar Paneli üzrə əlavə menyu"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Yuxarı/sola köçürün"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Aşağı/sağa köçürün"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{əlavə tətbiq}other{əlavə tətbiq}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"Hamısını kənarlaşdırın"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"genişləndirin: <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"yığcamlaşdırın: <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"Dairəyə alaraq axtarın"</string>
 </resources>
diff --git a/quickstep/res/values-b+sr+Latn/strings.xml b/quickstep/res/values-b+sr+Latn/strings.xml
index 4ef487e..cbcffdf 100644
--- a/quickstep/res/values-b+sr+Latn/strings.xml
+++ b/quickstep/res/values-b+sr+Latn/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"Zakači"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"Slobodni oblik"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"Računar"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"Premestite na spoljni ekran"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"Računari"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"Nema nedavnih stavki"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"Podešavanja korišćenja aplikacije"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Uvek prikazuj traku zadataka"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Promeni režim navigacije"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Razdelnik trake zadataka"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Preklopna traka zadataka"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Premesti gore levo"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Premesti dole desno"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{dodatna aplikacija}one{dodatna aplikacija}few{dodatne aplikacije}other{dodatnih aplikacija}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"Odbaci sve"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"proširite oblačić <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"skupite oblačić <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"Pretraga zaokruživanjem"</string>
 </resources>
diff --git a/quickstep/res/values-be/strings.xml b/quickstep/res/values-be/strings.xml
index c506acb..103e243 100644
--- a/quickstep/res/values-be/strings.xml
+++ b/quickstep/res/values-be/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"Замацаваць"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"Адвольная форма"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"Працоўны стол"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"Перамясціць на знешні дысплэй"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"Працоўны стол"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"Няма новых элементаў"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"Налады выкарыстання праграмы"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Заўсёды паказваць панэль задач"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Змяніць рэжым навігацыі"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Раздзяляльнік панэлі задач"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Меню з пашырэннем панэлі задач"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Перамясціць уверх/улева"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Перамясціць уніз/управа"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{даступная праграма}one{даступная праграма}few{даступныя праграмы}many{даступных праграм}other{даступнай праграмы}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"Закрыць усе"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>: разгарнуць"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>: згарнуць"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"Абвесці для пошуку"</string>
 </resources>
diff --git a/quickstep/res/values-bg/strings.xml b/quickstep/res/values-bg/strings.xml
index d03e4f7..d624914 100644
--- a/quickstep/res/values-bg/strings.xml
+++ b/quickstep/res/values-bg/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"Фиксиране"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"Свободна форма"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"За компютър"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"Преместване към външния екран"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"Настолен компютър"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"Няма скорошни елементи"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"Настройки за използването на приложенията"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Лентата на задачите винаги да се показва"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Промяна на режима на навигация"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Разделител на лентата на задачите"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Меню при препълване на лентата на задачите"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Преместване горе/вляво"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Преместване долу/вдясно"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{допълнително приложение}other{допълнителни приложения}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"Отхвърляне на всички"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"разгъване на <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"свиване на <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"Търсене с ограждане"</string>
 </resources>
diff --git a/quickstep/res/values-bn/strings.xml b/quickstep/res/values-bn/strings.xml
index 0c568d9..c7bc2cf 100644
--- a/quickstep/res/values-bn/strings.xml
+++ b/quickstep/res/values-bn/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"পিন করুন"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"ফ্রি-ফর্ম"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"ডেস্কটপ"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"এক্সটার্নাল ডিসপ্লেতে সরিয়ে নিয়ে যান"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"ডেস্কটপ"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"কোনও সাম্প্রতিক আইটেম নেই"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"অ্যাপ ব্যবহারের সেটিংস"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"সবসময় টাস্কবার দেখুন"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"\'নেভিগেশন\' মোড পরিবর্তন করুন"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"টাস্কবার ডিভাইডার"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"টাস্কবার ওভারফ্লো"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"উপরে/বাঁদিকে সরান"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"নিচে/ডানদিকে সরান"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{আরও অ্যাপ}one{আরও অ্যাপ}other{আরও অ্যাপ}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"সব বাতিল করুন"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g> বড় করুন"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g> আড়াল করুন"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"খোঁজার জন্য সার্কেল বানান"</string>
 </resources>
diff --git a/quickstep/res/values-bs/strings.xml b/quickstep/res/values-bs/strings.xml
index 922883e..cea1921 100644
--- a/quickstep/res/values-bs/strings.xml
+++ b/quickstep/res/values-bs/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"Zakači"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"Slobodan oblik"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"Radna površina"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"Premještanje na vanjski ekran"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"Radna površina"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"Nema nedavnih stavki"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"Postavke korištenja aplikacije"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Uvijek prikaži traku zadataka"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Promijeni način navigacije"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Razdjelnik trake zadataka"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Preklopni meni trake zadataka"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Premjesti gore lijevo"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Premjesti dolje desno"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{dodatna aplikacija}one{dodatna aplikacija}few{dodatne aplikacije}other{dodatnih aplikacija}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"Odbacivanje svega"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"proširivanje oblačića <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"sužavanje oblačića <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"Pretraživanje zaokruživanjem"</string>
 </resources>
diff --git a/quickstep/res/values-ca/strings.xml b/quickstep/res/values-ca/strings.xml
index fe7933b..e2352d7 100644
--- a/quickstep/res/values-ca/strings.xml
+++ b/quickstep/res/values-ca/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"Fixa"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"Format lliure"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"Escriptori"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"Mou a la pantalla externa"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"Escriptori"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"No hi ha cap element recent"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"Configuració d\'ús d\'aplicacions"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Barra de tasques sempre visible"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Canvia el mode de navegació"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Separador de la Barra de tasques"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Menú addicional de la barra de tasques"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Mou a la part superior o a l\'esquerra"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Mou a la part inferior o a la dreta"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{aplicació més}other{aplicacions més}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"Ignora-ho tot"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"desplega <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"replega <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"Encercla per cercar"</string>
 </resources>
diff --git a/quickstep/res/values-cs/strings.xml b/quickstep/res/values-cs/strings.xml
index 3047d05..de550d1 100644
--- a/quickstep/res/values-cs/strings.xml
+++ b/quickstep/res/values-cs/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"Připnout"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"Neomezený režim"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"Počítač"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"Přesunout na externí displej"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"Počítač"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"Žádné položky z nedávné doby"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"Nastavení využití aplikací"</string>
@@ -51,8 +52,8 @@
     <string name="back_gesture_feedback_swipe_too_far_from_edge" msgid="4175100312909721217">"Přejeďte prstem z úplného pravého nebo levého okraje obrazovky"</string>
     <string name="back_gesture_feedback_cancelled" msgid="762621530959111290">"Přejeďte prstem z pravého nebo levého okraje doprostřed obrazovky a zdvihněte prst"</string>
     <string name="back_gesture_feedback_complete_with_overview_follow_up" msgid="9176400654037014471">"Naučili jste se, jak se vrátit zpět přejetím prstem zprava. Teď se naučíte přepínat mezi aplikacemi."</string>
-    <string name="back_gesture_feedback_complete_with_follow_up" msgid="8653374779579748392">"Dokončili jste gesto pro přechod zpět. Teď se naučíte přepínat aplikace."</string>
-    <string name="back_gesture_feedback_complete_without_follow_up" msgid="197189945858268342">"Dokončili jste gesto pro přechod zpět"</string>
+    <string name="back_gesture_feedback_complete_with_follow_up" msgid="8653374779579748392">"Provedli jste gesto pro přechod zpět. Teď se naučíte přepínat aplikace."</string>
+    <string name="back_gesture_feedback_complete_without_follow_up" msgid="197189945858268342">"Provedli jste gesto pro přechod zpět"</string>
     <string name="back_gesture_feedback_swipe_in_nav_bar" msgid="9157480023651452969">"Dejte pozor, abyste prstem nepřejížděli moc blízko ke spodnímu okraji obrazovky"</string>
     <string name="back_gesture_tutorial_confirm_subtitle" msgid="5181305411668713250">"Citlivost gesta pro přechod zpět můžete změnit v Nastavení"</string>
     <string name="back_gesture_intro_title" msgid="19551256430224428">"Přejetím prstem se vrátíte zpět"</string>
@@ -63,8 +64,8 @@
     <string name="home_gesture_feedback_swipe_too_far_from_edge" msgid="4816365433160895458">"Přejeďte prstem nahoru z dolního okraje obrazovky"</string>
     <string name="home_gesture_feedback_overview_detected" msgid="5177627157303895077">"Před zdvihnutím prstu nedělejte pauzu"</string>
     <string name="home_gesture_feedback_wrong_swipe_direction" msgid="8328465201424027148">"Přejeďte prstem přímo nahoru"</string>
-    <string name="home_gesture_feedback_complete_with_follow_up" msgid="8766981412895888417">"Dokončili jste gesto pro přechod na plochu. Teď se naučíte vrátit se zpět."</string>
-    <string name="home_gesture_feedback_complete_without_follow_up" msgid="2978063221383413443">"Dokončili jste gesto pro přechod na plochu"</string>
+    <string name="home_gesture_feedback_complete_with_follow_up" msgid="8766981412895888417">"Provedli jste gesto pro přechod na plochu. Teď se naučíte vrátit se zpět."</string>
+    <string name="home_gesture_feedback_complete_without_follow_up" msgid="2978063221383413443">"Provedli jste gesto pro přechod na plochu"</string>
     <string name="home_gesture_intro_title" msgid="836590312858441830">"Přechod na plochu přejetím prstem"</string>
     <string name="home_gesture_intro_subtitle" msgid="2632238748497975326">"Přejeďte prstem ze spodní části obrazovky nahoru. Tímto gestem se vždy dostanete na plochu."</string>
     <string name="home_gesture_spoken_intro_subtitle" msgid="1030987707382031750">"Přejeďte dvěma prsty z dolního okraje obrazovky nahoru. Tímto gestem se vždy dostanete na plochu."</string>
@@ -75,7 +76,7 @@
     <string name="overview_gesture_feedback_home_detected" msgid="663432226180397138">"Zkuste podržet okno delší dobu, než ho uvolníte"</string>
     <string name="overview_gesture_feedback_wrong_swipe_direction" msgid="1191055451018584958">"Přejeďte prstem přímo nahoru a pak udělejte pauzu"</string>
     <string name="overview_gesture_feedback_complete_with_follow_up" msgid="3544611727467765026">"Naučili jste se používat gesta. Vypnout je můžete v Nastavení."</string>
-    <string name="overview_gesture_feedback_complete_without_follow_up" msgid="2903050864432331629">"Dokončili jste gesto pro přepínání aplikací"</string>
+    <string name="overview_gesture_feedback_complete_without_follow_up" msgid="2903050864432331629">"Provedli jste gesto pro přepínání aplikací"</string>
     <string name="overview_gesture_intro_title" msgid="2902054412868489378">"Přepínání aplikací přejetím prstem"</string>
     <string name="overview_gesture_intro_subtitle" msgid="4968091015637850859">"Přejeďte nahoru z dolního okraje obrazovky, podržte obrazovku a uvolněte."</string>
     <string name="overview_gesture_spoken_intro_subtitle" msgid="3853371838260201751">"Přepínání mezi aplikacemi: Přejeďte dvěma prsty nahoru z dolního okraje obrazovky, podržte obrazovku a uvolněte."</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Vždy zobrazovat panel aplikací"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Změnit režim navigace"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Rozdělovač panelu aplikací"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Přetečení panelu aplikací"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Přesunout doleva nahoru"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Přesunout doprava dolů"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{další aplikace}few{další aplikace}many{další aplikace}other{dalších aplikací}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"Zavřít vše"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"rozbalit <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"sbalit <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"Zakroužkuj a hledej"</string>
 </resources>
diff --git a/quickstep/res/values-da/strings.xml b/quickstep/res/values-da/strings.xml
index 6e2130c..b022172 100644
--- a/quickstep/res/values-da/strings.xml
+++ b/quickstep/res/values-da/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"Fastgør"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"Frit format"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"Computertilstand"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"Flyt til ekstern skærm"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"Computer"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"Ingen nye elementer"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"Indstillinger for appforbrug"</string>
@@ -51,8 +52,8 @@
     <string name="back_gesture_feedback_swipe_too_far_from_edge" msgid="4175100312909721217">"Stryg fra kanten yderst til højre eller venstre"</string>
     <string name="back_gesture_feedback_cancelled" msgid="762621530959111290">"Stryg fra højre eller venstre kant mod midten af skærmen, og løft fingeren"</string>
     <string name="back_gesture_feedback_complete_with_overview_follow_up" msgid="9176400654037014471">"Du har lært, hvordan du stryger fra højre for at gå tilbage. Nu skal du se, hvordan du skifter app."</string>
-    <string name="back_gesture_feedback_complete_with_follow_up" msgid="8653374779579748392">"Du har fuldført bevægelsen for Gå tilbage. Som det næste kan du se, hvordan du skifter app."</string>
-    <string name="back_gesture_feedback_complete_without_follow_up" msgid="197189945858268342">"Du har fuldført bevægelsen for Gå tilbage"</string>
+    <string name="back_gesture_feedback_complete_with_follow_up" msgid="8653374779579748392">"Du har udført bevægelsen for Gå tilbage. Som det næste kan du se, hvordan du skifter app."</string>
+    <string name="back_gesture_feedback_complete_without_follow_up" msgid="197189945858268342">"Du har udført bevægelsen for Gå tilbage"</string>
     <string name="back_gesture_feedback_swipe_in_nav_bar" msgid="9157480023651452969">"Undgå at stryge for tæt på bunden af skærmen"</string>
     <string name="back_gesture_tutorial_confirm_subtitle" msgid="5181305411668713250">"Juster følsomheden for bevægelsen Gå tilbage i Indstillinger"</string>
     <string name="back_gesture_intro_title" msgid="19551256430224428">"Stryg for at gå tilbage"</string>
@@ -63,8 +64,8 @@
     <string name="home_gesture_feedback_swipe_too_far_from_edge" msgid="4816365433160895458">"Stryg opad fra bunden af skærmen"</string>
     <string name="home_gesture_feedback_overview_detected" msgid="5177627157303895077">"Undlad at holde fingeren stille, indtil du løfter fingeren"</string>
     <string name="home_gesture_feedback_wrong_swipe_direction" msgid="8328465201424027148">"Stryg lige opad"</string>
-    <string name="home_gesture_feedback_complete_with_follow_up" msgid="8766981412895888417">"Du har fuldført bevægelsen for Gå til startskærmen. Som det næste kan du se, hvordan du går tilbage."</string>
-    <string name="home_gesture_feedback_complete_without_follow_up" msgid="2978063221383413443">"Du har fuldført bevægelsen for Gå til startskærmen"</string>
+    <string name="home_gesture_feedback_complete_with_follow_up" msgid="8766981412895888417">"Du har udført bevægelsen for Gå til startskærmen. Som det næste kan du se, hvordan du går tilbage."</string>
+    <string name="home_gesture_feedback_complete_without_follow_up" msgid="2978063221383413443">"Du har udført bevægelsen for Gå til startskærmen"</string>
     <string name="home_gesture_intro_title" msgid="836590312858441830">"Stryg for at gå til startskærmen"</string>
     <string name="home_gesture_intro_subtitle" msgid="2632238748497975326">"Stryg opad fra bunden af skærmen. Denne bevægelse åbner altid startskærmen."</string>
     <string name="home_gesture_spoken_intro_subtitle" msgid="1030987707382031750">"Stryg opad med 2 fingre fra bunden af skærmen. Denne bevægelse åbner altid startskærmen."</string>
@@ -75,7 +76,7 @@
     <string name="overview_gesture_feedback_home_detected" msgid="663432226180397138">"Prøv at holde fingeren nede på vinduet i længere tid, inden du løfter den"</string>
     <string name="overview_gesture_feedback_wrong_swipe_direction" msgid="1191055451018584958">"Stryg lige opad, og hold derefter fingeren stille"</string>
     <string name="overview_gesture_feedback_complete_with_follow_up" msgid="3544611727467765026">"Du har lært, hvordan du bruger bevægelser. Du kan aktivere bevægelser i Indstillinger."</string>
-    <string name="overview_gesture_feedback_complete_without_follow_up" msgid="2903050864432331629">"Du har fuldført bevægelsen for at skifte mellem apps"</string>
+    <string name="overview_gesture_feedback_complete_without_follow_up" msgid="2903050864432331629">"Du har udført bevægelsen for at skifte mellem apps"</string>
     <string name="overview_gesture_intro_title" msgid="2902054412868489378">"Stryg for at skifte app"</string>
     <string name="overview_gesture_intro_subtitle" msgid="4968091015637850859">"Skift mellem apps ved at stryge opad fra bunden af skærmen, holde fingeren stille og løfte den."</string>
     <string name="overview_gesture_spoken_intro_subtitle" msgid="3853371838260201751">"Skift mellem apps ved at stryge opad fra bunden af skærmen med 2 fingre, holde dem nede og slippe."</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Vis altid proceslinjen"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Skift navigationstilstand"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Opdeling af proceslinjen"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Prikmenu på proceslinjen"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Flyt til toppen eller venstre side"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Flyt til bunden eller højre side"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{yderligere app}one{yderligere app}other{yderligere apps}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"Afvis alle"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"udvid <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"skjul <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"Circle to Search"</string>
 </resources>
diff --git a/quickstep/res/values-de/strings.xml b/quickstep/res/values-de/strings.xml
index 54961fe..f70e408 100644
--- a/quickstep/res/values-de/strings.xml
+++ b/quickstep/res/values-de/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"Fixieren"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"Freeform-Modus"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"Desktopmodus"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"Auf externes Display verschieben"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"Desktopmodus"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"Keine kürzlich verwendeten Elemente"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"Einstellungen zur App-Nutzung"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Taskleiste immer anzeigen"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Navigationsmodus ändern"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Taskleisten-Teiler"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Dreipunkt-Menü der Taskleiste"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Nach oben / Nach links verschieben"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Nach unten / Nach rechts verschieben"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{weitere App}other{weitere Apps}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"Alle schließen"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"„<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>“ maximieren"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"„<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>“ minimieren"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"Circle to Search"</string>
 </resources>
diff --git a/quickstep/res/values-el/strings.xml b/quickstep/res/values-el/strings.xml
index 6cbb833..d7ff2ad 100644
--- a/quickstep/res/values-el/strings.xml
+++ b/quickstep/res/values-el/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"Καρφίτσωμα"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"Ελεύθερη μορφή"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"Υπολογιστής"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"Μετακίνηση σε εξωτερική οθόνη"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"Υπολογιστής"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"Δεν υπάρχουν πρόσφατα στοιχεία"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"Ρυθμίσεις χρήσης εφαρμογής"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Εμφάνιση Γραμμής εργαλείων"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Αλλαγή τρόπου πλοήγησης"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Διαχωριστικό Γραμμής εργαλείων"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Υπερχείλιση γραμμής εργαλείων"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Μετακίνηση επάνω/αριστερά"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Μετακίνηση κάτω/δεξιά"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{ακόμη εφαρμογή}other{ακόμη εφαρμογές}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"Παράβλεψη όλων"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"ανάπτυξη <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"σύμπτυξη <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"Κυκλώστε για αναζήτηση"</string>
 </resources>
diff --git a/quickstep/res/values-en-rAU/strings.xml b/quickstep/res/values-en-rAU/strings.xml
index dcbaa7a..6b81b05 100644
--- a/quickstep/res/values-en-rAU/strings.xml
+++ b/quickstep/res/values-en-rAU/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"Pin"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"Freeform"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"Desktop"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"Move to external display"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"Desktop"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"No recent items"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"App usage settings"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Always show Taskbar"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Change navigation mode"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Taskbar divider"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Taskbar overflow"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Move to top/left"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Move to bottom/right"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{more app}other{more apps}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"Dismiss all"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"expand <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"collapse <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"Circle to Search"</string>
 </resources>
diff --git a/quickstep/res/values-en-rCA/strings.xml b/quickstep/res/values-en-rCA/strings.xml
index c00e6cd..da4effb 100644
--- a/quickstep/res/values-en-rCA/strings.xml
+++ b/quickstep/res/values-en-rCA/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"Pin"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"Freeform"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"Desktop"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"Move to external display"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"Desktop"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"No recent items"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"App usage settings"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Always show Taskbar"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Change navigation mode"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Taskbar Divider"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Taskbar Overflow"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Move to top/left"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Move to bottom/right"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{more app}other{more apps}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"Dismiss all"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"expand <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"collapse <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"Circle to Search"</string>
 </resources>
diff --git a/quickstep/res/values-en-rGB/strings.xml b/quickstep/res/values-en-rGB/strings.xml
index dcbaa7a..6b81b05 100644
--- a/quickstep/res/values-en-rGB/strings.xml
+++ b/quickstep/res/values-en-rGB/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"Pin"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"Freeform"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"Desktop"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"Move to external display"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"Desktop"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"No recent items"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"App usage settings"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Always show Taskbar"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Change navigation mode"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Taskbar divider"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Taskbar overflow"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Move to top/left"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Move to bottom/right"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{more app}other{more apps}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"Dismiss all"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"expand <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"collapse <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"Circle to Search"</string>
 </resources>
diff --git a/quickstep/res/values-en-rIN/strings.xml b/quickstep/res/values-en-rIN/strings.xml
index dcbaa7a..6b81b05 100644
--- a/quickstep/res/values-en-rIN/strings.xml
+++ b/quickstep/res/values-en-rIN/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"Pin"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"Freeform"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"Desktop"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"Move to external display"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"Desktop"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"No recent items"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"App usage settings"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Always show Taskbar"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Change navigation mode"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Taskbar divider"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Taskbar overflow"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Move to top/left"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Move to bottom/right"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{more app}other{more apps}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"Dismiss all"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"expand <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"collapse <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"Circle to Search"</string>
 </resources>
diff --git a/quickstep/res/values-en-rXC/strings.xml b/quickstep/res/values-en-rXC/strings.xml
index 2abef91..8fcbe2b 100644
--- a/quickstep/res/values-en-rXC/strings.xml
+++ b/quickstep/res/values-en-rXC/strings.xml
@@ -139,6 +139,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‏‎‎‏‎‎‎‎‏‎‏‎‏‎‎‎‎‏‎‎‏‏‎‏‎‎‎‏‏‏‏‏‎‏‏‏‏‎‏‏‎‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‏‏‏‎‏‎Always show Taskbar‎‏‎‎‏‎"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‏‏‏‏‎‎‎‏‎‎‎‎‎‎‏‏‏‎‏‎‏‎‏‎‏‏‎‏‎‎‏‎‎‏‏‎‏‎‎‏‏‏‎‎‏‏‏‏‎‏‏‎‎‎‎‎‎‏‎‎‎‎Change navigation mode‎‏‎‎‏‎"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‏‏‎‏‏‏‎‏‏‎‏‏‎‏‏‎‎‏‎‎‎‏‏‎‏‎‎‏‎‏‎‎‎‎‏‎‏‎‏‏‏‎‎‎‎‏‏‏‏‎‏‏‏‏‎‏‎‎‎‎‎‎Taskbar Divider‎‏‎‎‏‎"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‏‎‏‏‏‎‎‏‏‏‏‎‎‎‏‏‎‏‎‎‎‎‏‏‏‏‎‎‏‎‎‎‎‎‎‎‎‏‎‎‏‏‏‏‎‏‎‏‏‏‎‏‏‏‎‏‎‏‎‎‏‏‎Taskbar Overflow‎‏‎‎‏‎"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‎‏‎‎‏‎‏‏‏‏‎‏‎‎‎‎‎‎‎‎‎‏‏‎‎‏‏‎‏‎‏‏‎‏‏‏‏‎‏‎‏‎‎‎‎‏‎‎‏‏‏‏‏‏‎‎‏‏‏‏‏‎Move to top/left‎‏‎‎‏‎"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‎‏‎‏‏‎‏‏‎‎‎‎‎‎‎‏‎‏‏‏‏‏‎‎‎‏‎‎‏‏‎‏‎‎‎‏‎‏‏‎‏‎‎‏‏‏‎‎‏‏‎‏‏‏‏‎‎‎‎‏‎‎Move to bottom/right‎‏‎‎‏‎"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‏‎‎‏‏‎‎‎‏‎‎‎‏‎‎‏‏‎‏‎‏‎‎‎‎‎‎‎‏‎‏‏‏‎‎‏‏‎‎‎‏‏‏‏‎‏‏‎‎‎‏‎‎‏‏‏‎‎‎‎‎‎more app‎‏‎‎‏‎}other{‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‏‎‎‏‏‎‎‎‏‎‎‎‏‎‎‏‏‎‏‎‏‎‎‎‎‎‎‎‏‎‏‏‏‎‎‏‏‎‎‎‏‏‏‏‎‏‏‎‎‎‏‎‎‏‏‏‎‎‎‎‎‎more apps‎‏‎‎‏‎}}"</string>
diff --git a/quickstep/res/values-es-rUS/strings.xml b/quickstep/res/values-es-rUS/strings.xml
index f09fae6..57333f4 100644
--- a/quickstep/res/values-es-rUS/strings.xml
+++ b/quickstep/res/values-es-rUS/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"Fijar"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"Formato libre"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"Escritorio"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"Mover a pantalla externa"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"Computadoras"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"No hay elementos recientes"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"Configuración de uso de la app"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Barra de tareas visible"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Cambiar el modo de navegación"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Divisor de la Barra de tareas"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Barra de tareas ampliada"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Mover a la parte superior o izquierda"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Mover a la parte inferior o derecha"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{app más}other{apps más}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"Descartar todo"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"expandir <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"contraer <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"Busca con un círculo"</string>
 </resources>
diff --git a/quickstep/res/values-es/strings.xml b/quickstep/res/values-es/strings.xml
index 356a38b..8355a88 100644
--- a/quickstep/res/values-es/strings.xml
+++ b/quickstep/res/values-es/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"Fijar"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"Formato libre"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"Escritorio"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"Mover a pantalla externa"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"Ordenador"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"No hay nada reciente"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"Ajustes de uso de la aplicación"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Barra de tareas visible"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Cambiar el modo de navegación"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Divisor de Barra de Tareas"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Barra de tareas ampliada"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Mover arriba/a la izquierda"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Mover abajo/a la derecha"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{aplicación más}other{aplicaciones más}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"Cerrar todo"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"desplegar <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"contraer <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"Rodea para buscar"</string>
 </resources>
diff --git a/quickstep/res/values-et/strings.xml b/quickstep/res/values-et/strings.xml
index fccbeda..6192e81 100644
--- a/quickstep/res/values-et/strings.xml
+++ b/quickstep/res/values-et/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"Kinnita"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"Vabavorm"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"Lauaarvuti režiim"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"Liikuge välisele ekraanile"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"Töölaud"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"Hiljutisi üksusi pole"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"Rakenduse kasutuse seaded"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Kuva tegumiriba alati"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Navigeerimisrežiimi muutmine"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Tegumiriba jagaja"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Tegumiriba ületäide"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Teisalda üles/vasakule"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Teisalda alla/paremale"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{rakendus veel}other{rakendust veel}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"Loobu kõigist"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"Toiminguriba <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g> laiendamine"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"Toiminguriba <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g> ahendamine"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"Ring otsimiseks"</string>
 </resources>
diff --git a/quickstep/res/values-eu/strings.xml b/quickstep/res/values-eu/strings.xml
index a1d8cc3..de19f15 100644
--- a/quickstep/res/values-eu/strings.xml
+++ b/quickstep/res/values-eu/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"Ainguratu"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"Modu librea"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"Ordenagailua"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"Eraman kanpoko pantailara"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"Mahaigaina"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"Ez dago azkenaldi honetako ezer"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"Aplikazioen erabileraren ezarpenak"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Erakutsi beti zereginen barra"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Aldatu nabigazio modua"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Zereginen barraren zatitzailea"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Zereginen barraren luzapena"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Eraman gora, ezkerretara"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Eraman behera, eskuinetara"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{aplikazio gehiago}other{aplikazio gehiago}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"Baztertu guztiak"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"zabaldu <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"tolestu <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"Inguratu bilatzeko"</string>
 </resources>
diff --git a/quickstep/res/values-fa/strings.xml b/quickstep/res/values-fa/strings.xml
index 8f37686..bc14f0b 100644
--- a/quickstep/res/values-fa/strings.xml
+++ b/quickstep/res/values-fa/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"پین"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"Freeform"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"حالت رایانه"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"انتقال به نمایشگر خارجی"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"رایانه"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"چیز جدیدی اینجا نیست"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"تنظیمات استفاده از برنامه"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"نوار وظیفه همیشه نشان داده شود"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"تغییر حالت پیمایش"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"جداکننده نوار وظیفه"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"سرریز نوار وظیفه"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"انتقال به بالا/ چپ"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"انتقال به پایین/ راست"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{برنامه دیگر}one{برنامه دیگر}other{برنامه دیگر}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"رد کردن همه"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"ازهم باز کردن <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"جمع کردن <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"حلقه جستجو"</string>
 </resources>
diff --git a/quickstep/res/values-fi/strings.xml b/quickstep/res/values-fi/strings.xml
index 175a896..10e4699 100644
--- a/quickstep/res/values-fi/strings.xml
+++ b/quickstep/res/values-fi/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"Kiinnitä"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"Vapaamuotoinen"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"Tietokone"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"Siirrä ulkoiselle näytölle"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"Tietokone"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"Ei viimeaikaisia kohteita"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"Sovelluksen käyttöasetukset"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Näytä tehtäväpalkki aina"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Vaihda navigointitilaa"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Tehtäväpalkin jakaja"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Tehtäväpalkin ylivuotu"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Siirrä ylös tai vasemmalle"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Siirrä alas tai oikealle"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{muu sovellus}other{muuta sovellusta}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"Hylkää kaikki"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"laajenna <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"tiivistä <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"Circle to Search"</string>
 </resources>
diff --git a/quickstep/res/values-fr-rCA/strings.xml b/quickstep/res/values-fr-rCA/strings.xml
index 6d6c67c..746bf50 100644
--- a/quickstep/res/values-fr-rCA/strings.xml
+++ b/quickstep/res/values-fr-rCA/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"Épingler"</string>
     <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="recent_task_option_external_display" msgid="4533840664313389484">"Passer à un écran externe"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"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\'appli"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Tjrs afficher barre des tâches"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Changer de mode de navigation"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Séparateur de la barre des tâches"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Barre des tâches à développer"</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="3679780650881041632">"{count,plural, =1{autre appli}one{autre appli}other{autres applis}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"Tout ignorer"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"Développer <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"Réduire <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"Encercler et rechercher"</string>
 </resources>
diff --git a/quickstep/res/values-fr/strings.xml b/quickstep/res/values-fr/strings.xml
index a395266..6d2fba2 100644
--- a/quickstep/res/values-fr/strings.xml
+++ b/quickstep/res/values-fr/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"Épingler"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"Format libre"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"Ordinateur"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"Déplacer vers l\'écran externe"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"Ordinateur"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"Aucun élément récent"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"Paramètres de consommation de l\'application"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Barre des tâches tjs visible"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Modifier le mode de navigation"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Séparateur de barre des tâches"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Développement de la barre des tâches"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Déplacer en haut ou à gauche"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Déplacer en bas ou à droite"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{autre application}one{autre application}other{autres applications}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"Tout fermer"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"Développer <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"Réduire <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"Entourer pour chercher"</string>
 </resources>
diff --git a/quickstep/res/values-gl/strings.xml b/quickstep/res/values-gl/strings.xml
index 5b04960..d9a78ee 100644
--- a/quickstep/res/values-gl/strings.xml
+++ b/quickstep/res/values-gl/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"Fixar"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"Forma libre"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"Escritorio"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"Mover á pantalla externa"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"Ordenador"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"Non hai elementos recentes"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"Configuración do uso de aplicacións"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Ver sempre a barra de tarefas"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Cambiar modo de navegación"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Divisor da Barra de tarefas"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Menú adicional da barra de tarefas"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Mover á parte superior ou á esquerda"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Mover á parte inferior ou á dereita"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{aplicación máis}other{aplicacións máis}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"Pechar todo"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"despregar <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"contraer <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"Rodear para buscar"</string>
 </resources>
diff --git a/quickstep/res/values-gu/strings.xml b/quickstep/res/values-gu/strings.xml
index fc253bd..1bdcaa1 100644
--- a/quickstep/res/values-gu/strings.xml
+++ b/quickstep/res/values-gu/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"પિન કરો"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"ફ્રિફોર્મ"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"ડેસ્કટૉપ"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"બાહ્ય ડિસ્પ્લે પર ખસેડો"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"ડેસ્કટૉપ"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"તાજેતરની કોઈ આઇટમ નથી"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"ઍપ વપરાશનું સેટિંગ"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"હંમેશાં ટાસ્કબાર બતાવો"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"નૅવિગેશન મોડ બદલો"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"ટાસ્કબાર વિભાજક"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"ટાસ્કબાર ઓવરફ્લો"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"સૌથી ઉપર ડાબી બાજુએ ખસેડો"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"સૌથી નીચે જમણી બાજુએ ખસેડો"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{વધુ ઍપ}one{વધુ ઍપ}other{વધુ ઍપ}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"તમામ છોડી દો"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g> મોટો કરો"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g> નાનો કરો"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"શોધવા માટે વર્તુળ દોરો"</string>
 </resources>
diff --git a/quickstep/res/values-hi/strings.xml b/quickstep/res/values-hi/strings.xml
index 30a17db..e97aa78 100644
--- a/quickstep/res/values-hi/strings.xml
+++ b/quickstep/res/values-hi/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"पिन करें"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"फ़्रीफ़ॉर्म"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"डेस्कटॉप"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"बाहरी डिसप्ले पर जाएं"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"डेस्कटॉप"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"हाल ही का कोई आइटम नहीं है"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"ऐप्लिकेशन इस्तेमाल की सेटिंग"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"टास्कबार हमेशा दिखाएं"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"नेविगेशन का मोड बदलें"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"टास्कबार डिवाइडर"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"टास्कबार ओवरफ़्लो आइकॉन"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"ऊपर/बाईं तरफ़ ले जाएं"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"नीचे/दाईं तरफ़ ले जाएं"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{ज़्यादा ऐप्लिकेशन}one{ज़्यादा ऐप्लिकेशन}other{ज़्यादा ऐप्लिकेशन}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"सभी खारिज करें"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g> को बड़ा करें"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g> को छोटा करें"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"सर्कल बनाकर ढूंढें"</string>
 </resources>
diff --git a/quickstep/res/values-hr/strings.xml b/quickstep/res/values-hr/strings.xml
index 06511e9..c350bc5 100644
--- a/quickstep/res/values-hr/strings.xml
+++ b/quickstep/res/values-hr/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"Prikvači"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"Slobodni oblik"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"Računalo"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"Premještanje na vanjski zaslon"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"Radna površina"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"Nema nedavnih stavki"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"Postavke upotrebe aplikacija"</string>
@@ -91,7 +92,7 @@
     <string name="allset_title" msgid="5021126669778966707">"Sve je spremno!"</string>
     <string name="allset_hint" msgid="459504134589971527">"Prijeđite prstom prema gore da biste otvorili početni zaslon"</string>
     <string name="allset_button_hint" msgid="2395219947744706291">"Dodirnite gumb početnog zaslona da biste prešli na početni zaslon"</string>
-    <string name="allset_description_generic" msgid="5385500062202019855">"<xliff:g id="DEVICE">%1$s</xliff:g> je spreman za početak upotrebe"</string>
+    <string name="allset_description_generic" msgid="5385500062202019855">"Možete početi upotrebljavati <xliff:g id="DEVICE">%1$s</xliff:g>"</string>
     <string name="default_device_name" msgid="6660656727127422487">"Uređaj"</string>
     <string name="allset_navigation_settings" msgid="4713404605961476027"><annotation id="link">"Postavke navigacije sustavom"</annotation></string>
     <string name="action_share" msgid="2648470652637092375">"Podijeli"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Uvijek prikaži traku zadataka"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Promijeni način navigacije"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Razdjelnik trake sa zadacima"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Dodatni izbornik trake sa zadacima"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Premjesti gore/lijevo"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Premjesti dolje/desno"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{dodatna aplikacija}one{dodatna aplikacija}few{dodatne aplikacije}other{dodatnih aplikacija}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"Odbaci sve"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"proširite oblačić <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"sažmite oblačić <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"Zaokružite i potražite"</string>
 </resources>
diff --git a/quickstep/res/values-hu/strings.xml b/quickstep/res/values-hu/strings.xml
index 9bd9478..ea29620 100644
--- a/quickstep/res/values-hu/strings.xml
+++ b/quickstep/res/values-hu/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"Kitűzés"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"Szabad forma"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"Asztali"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"Áthelyezés külső kijelzőre"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"Asztali"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"Nincsenek mostanában használt elemek"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"Alkalmazáshasználati beállítások"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Mindig megjelenő Feladatsáv"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Navigációs mód módosítása"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Feladatsáv-elválasztó"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Feladatsáv túlcsordulása"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Mozgatás felülre vagy a bal oldalra"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Mozgatás alulra vagy a jobb oldalra"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{további alkalmazás}other{további alkalmazás}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"Az összes elvetése"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g> kibontása"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g> összecsukása"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"Bekarikázással keresés"</string>
 </resources>
diff --git a/quickstep/res/values-hy/strings.xml b/quickstep/res/values-hy/strings.xml
index e1481ec..14d715d 100644
--- a/quickstep/res/values-hy/strings.xml
+++ b/quickstep/res/values-hy/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"Ամրացնել"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"Կամայական ձև"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"Համակարգիչ"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"Տեղափոխել արտաքին էկրան"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"Համակարգիչ"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"Այստեղ դեռ ոչինչ չկա"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"Հավելվածի օգտագործման կարգավորումներ"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Միշտ ցույց տալ վահանակը"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Փոխել նավիգացիայի ռեժիմը"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Հավելվածների վահանակի բաժանիչ"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Հավելվածների վահանակի լրացուցիչ ընտրացանկ"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Տեղափոխել վերևի ձախ անկյուն"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Տեղափոխել ներքևի աջ անկյուն"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{լրացուցիչ հավելված}one{լրացուցիչ հավելված}other{լրացուցիչ հավելված}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"Փակել բոլորը"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>. ծավալել"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>. ծալել"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"Շրջագծել որոնելու համար"</string>
 </resources>
diff --git a/quickstep/res/values-in/strings.xml b/quickstep/res/values-in/strings.xml
index f8345b5..e1400a9 100644
--- a/quickstep/res/values-in/strings.xml
+++ b/quickstep/res/values-in/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"Sematkan"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"Format bebas"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"Desktop"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"Pindahkan ke layar eksternal"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"Desktop"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"Tidak ada item yang baru dibuka"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"Setelan penggunaan aplikasi"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Selalu tampilkan Taskbar"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Ubah mode navigasi"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Pemisah Taskbar"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Tambahan Taskbar"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Pindahkan ke atas/kiri"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Pindahkan ke bawah/kanan"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{aplikasi lainnya}other{aplikasi lainnya}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"Tutup semua"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"luaskan <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"ciutkan <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"Lingkari untuk Menelusuri"</string>
 </resources>
diff --git a/quickstep/res/values-is/strings.xml b/quickstep/res/values-is/strings.xml
index 017d108..bf2a211 100644
--- a/quickstep/res/values-is/strings.xml
+++ b/quickstep/res/values-is/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"Festa"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"Frjálst snið"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"Tölva"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"Færa í annað tæki"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"Tölva"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"Engin nýleg atriði"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"Notkunarstillingar forrits"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Alltaf sýna forritastiku"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Breyta leiðsagnarstillingu"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Skipting forritastiku"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Yfirflæði á forritastiku"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Færa efst/til vinstri"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Færa neðst/til hægri"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{forrit til viðbótar}one{forrit til viðbótar}other{forrit til viðbótar}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"Hunsa allt"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"stækka <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"minnka <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"Circle to Search"</string>
 </resources>
diff --git a/quickstep/res/values-it/strings.xml b/quickstep/res/values-it/strings.xml
index 1983ee3..af77be4 100644
--- a/quickstep/res/values-it/strings.xml
+++ b/quickstep/res/values-it/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"Blocca su schermo"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"Forma libera"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"Desktop"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"Sposta sul display esterno"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"Desktop"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"Nessun elemento recente"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"Impostazioni di utilizzo delle app"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Mostra sempre barra app"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Cambia modalità di navigazione"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Divisore barra delle app"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Overflow barra delle app"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Sposta in alto/a sinistra"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Sposta in basso/a destra"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{altra app}other{altre app}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"Ignora tutte"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"espandi <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"comprimi <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"Cerchia e Cerca"</string>
 </resources>
diff --git a/quickstep/res/values-iw/strings.xml b/quickstep/res/values-iw/strings.xml
index a7115e3..ad26421 100644
--- a/quickstep/res/values-iw/strings.xml
+++ b/quickstep/res/values-iw/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"הצמדה"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"מצב חופשי"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"במחשב"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"העברה למסך חיצוני"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"מחשב"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"אין פריטים אחרונים"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"הגדרות שימוש באפליקציה"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"סרגל האפליקציות מוצג תמיד"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"שינוי מצב הניווט"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"המחיצה בסרגל האפליקציות"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"אפשרויות נוספות בסרגל האפליקציות"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"העברה לפינה השמאלית/העליונה"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"העברה לפינה הימנית/התחתונה"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{אפליקציה נוספת}one{אפליקציות נוספות}two{אפליקציות נוספות}other{אפליקציות נוספות}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"ביטול של הכול"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"הרחבה של <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"כיווץ של <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"מקיפים ומחפשים"</string>
 </resources>
diff --git a/quickstep/res/values-ja/strings.xml b/quickstep/res/values-ja/strings.xml
index fa3c01d..b30b000 100644
--- a/quickstep/res/values-ja/strings.xml
+++ b/quickstep/res/values-ja/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"固定"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"フリーフォーム"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"デスクトップ"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"外部ディスプレイに移動する"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"パソコン"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"最近のアイテムはありません"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"アプリの使用状況の設定"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"常にタスクバーを表示する"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"ナビゲーション モードを変更"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"タスクバーの区切り"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"タスクバーのオーバフロー"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"上 / 左に移動"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"下 / 右に移動"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{個のその他のアプリ}other{個のその他のアプリ}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"すべて解除"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>を開きます"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>を閉じます"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"かこって検索"</string>
 </resources>
diff --git a/quickstep/res/values-ka/strings.xml b/quickstep/res/values-ka/strings.xml
index a032731..1f877e9 100644
--- a/quickstep/res/values-ka/strings.xml
+++ b/quickstep/res/values-ka/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"ჩამაგრება"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"თავისუფალი ფორმა"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"დესკტოპი"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"გარე ეკრანზე გადასვლა"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"დესკტოპი"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"ბოლოს გამოყენებული ერთეულები არ არის"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"აპების გამოყენების პარამეტრები"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"ამოცანათა ზოლის მუდამ ჩვენება"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"შეცვალეთ ნავიგაციის რეჟიმი"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"ამოცანათა ზოლის გამყოფი"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"ამოცანათა ზოლის გადავსება"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"ზემოთ/მარცხნივ გადატანა"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"ქვემოთ/მარჯვნივ გადატანა"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{სხვა აპი}other{სხვა აპი}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"ყველას დახურვა"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>-ის გაფართოება"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>-ის ჩაკეცვა"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"ძიება წრის მოხაზვით"</string>
 </resources>
diff --git a/quickstep/res/values-kk/strings.xml b/quickstep/res/values-kk/strings.xml
index e1a9e8e..5fd172e 100644
--- a/quickstep/res/values-kk/strings.xml
+++ b/quickstep/res/values-kk/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"Бекіту"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"Еркін форма"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"Компьютер"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"Сыртқы дисплейге ауыстыру"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"Жұмыс үстелі"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"Соңғы элементтер жоқ"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"Қолданбаны пайдалану параметрлері"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Тапсырма жолағын үнемі көрсету"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Навигация режимін өзгерту"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Тапсырмалар жолағын бөлгіш"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"\"Тапсырмалар жолағы\" қосымша мәзірі"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Жоғары/солға жылжыту"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Төмен/оңға жылжыту"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{қосымша қолданба}other{қосымша қолданба}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"Барлығын жабу"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>: жаю"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>: жию"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"Қоршау арқылы іздеу"</string>
 </resources>
diff --git a/quickstep/res/values-km/strings.xml b/quickstep/res/values-km/strings.xml
index 2eb3114..4c8227e 100644
--- a/quickstep/res/values-km/strings.xml
+++ b/quickstep/res/values-km/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"ខ្ទាស់"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"មុខងារទម្រង់សេរី"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"ដែសថប"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"ផ្លាស់ទីទៅផ្ទាំងអេក្រង់ខាងក្រៅ"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"អេក្រង់ដើម"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"មិនមានធាតុថ្មីៗទេ"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"ការកំណត់​ការប្រើប្រាស់​កម្មវិធី"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"បង្ហាញរបារកិច្ចការជានិច្ច"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"ប្ដូរ​មុខងាររុករក"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"បន្ទាត់​ខណ្ឌចែករបារកិច្ចការ"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"ម៉ឺនុយបន្ថែមរបារកិច្ចការ"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"ផ្លាស់ទីទៅខាងលើ/ឆ្វេង"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"ផ្លាស់ទីទៅខាងក្រោម/ស្ដាំ"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{កម្មវិធីច្រើនទៀត}other{កម្មវិធីច្រើនទៀត}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"ច្រានចោលទាំងអស់"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"ពង្រីក <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"បង្រួម <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"គូររង្វង់ដើម្បីស្វែងរក"</string>
 </resources>
diff --git a/quickstep/res/values-kn/strings.xml b/quickstep/res/values-kn/strings.xml
index bb60620..afd53ac 100644
--- a/quickstep/res/values-kn/strings.xml
+++ b/quickstep/res/values-kn/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"ಪಿನ್ ಮಾಡಿ"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"ಮುಕ್ತಸ್ವರೂಪ"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"ಡೆಸ್ಕ್‌ಟಾಪ್"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"ಬಾಹ್ಯ ಡಿಸ್‌ಪ್ಲೇಗೆ ಸರಿಸಿ"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"ಡೆಸ್ಕ್‌ಟಾಪ್"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"ಯಾವುದೇ ಇತ್ತೀಚಿನ ಐಟಂಗಳಿಲ್ಲ"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"ಆ್ಯಪ್‌ ಬಳಕೆಯ ಸೆಟ್ಟಿಂಗ್‌ಗಳು"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"ಯಾವಾಗಲೂ ಟಾಸ್ಕ್‌ಬಾರ್ ತೋರಿಸಿ"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"ನ್ಯಾವಿಗೇಶನ್ ಮೋಡ್ ಬದಲಾಯಿಸಿ"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"ಟಾಸ್ಕ್‌ಬಾರ್ ಡಿವೈಡರ್"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"ಟಾಸ್ಕ್ ಬಾರ್ ಓವರ್‌ಫ್ಲೋ"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"ಮೇಲಿನ/ಎಡಭಾಗಕ್ಕೆ ಸರಿಸಿ"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"ಕೆಳಗಿನ/ಬಲಭಾಗಕ್ಕೆ ಸರಿಸಿ"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{ಹೆಚ್ಚಿನ ಆ್ಯಪ್‌}one{ಹೆಚ್ಚಿನ ಆ್ಯಪ್‌ಗಳು}other{ಹೆಚ್ಚಿನ ಆ್ಯಪ್‌ಗಳು}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"ಎಲ್ಲವನ್ನು ವಜಾಗೊಳಿಸಿ"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g> ಅನ್ನು ವಿಸ್ತೃತಗೊಳಿಸಿ"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g> ಅನ್ನು ಕುಗ್ಗಿಸಿ"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"ಹುಡುಕಲು ಒಂದು ಸರ್ಕಲ್ ರಚಿಸಿ"</string>
 </resources>
diff --git a/quickstep/res/values-ko/strings.xml b/quickstep/res/values-ko/strings.xml
index e6a80c3..d602482 100644
--- a/quickstep/res/values-ko/strings.xml
+++ b/quickstep/res/values-ko/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"고정"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"자유 형식"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"데스크톱"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"외부 디스플레이로 이동"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"데스크톱"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"최근 항목이 없습니다."</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"앱 사용 설정"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"태스크 바 항상 표시"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"탐색 모드 변경"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"태스크 바 분할"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"태스크 바 오버플로"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"상단/왼쪽으로 이동"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"하단/오른쪽으로 이동"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{추가 앱}other{추가 앱}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"모두 닫기"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g> 펼치기"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g> 접기"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"서클 투 서치"</string>
 </resources>
diff --git a/quickstep/res/values-ky/strings.xml b/quickstep/res/values-ky/strings.xml
index 04e7f6e..e5fed79 100644
--- a/quickstep/res/values-ky/strings.xml
+++ b/quickstep/res/values-ky/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"Кадап коюу"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"Эркин форма режими"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"Компьютер"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"Тышкы экранга жылдыруу"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"Компьютер"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"Акыркы колдонмолор жок"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"Колдонмону пайдалануу параметрлери"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Такта ар дайым көрүнсүн"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Өтүү режимин өзгөртүү"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Тапшырмалар панелин бөлгүч"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"\"Тапшырмалар панели\" кошумча менюсу"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Жогорку/сол бурчка жылдыруу"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Төмөнкү/оң бурчка жылдыруу"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{колдонмо бар}other{колдонмо бар}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"Баарын четке кагуу"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g> жайып көрсөтүү"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g> жыйыштыруу"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"Тегеректеп издөө"</string>
 </resources>
diff --git a/quickstep/res/values-land/dimens.xml b/quickstep/res/values-land/dimens.xml
index 2239f8b..efdc7de 100644
--- a/quickstep/res/values-land/dimens.xml
+++ b/quickstep/res/values-land/dimens.xml
@@ -81,7 +81,7 @@
     <dimen name="taskbar_contextual_button_suw_margin">48dp</dimen>
     <dimen name="taskbar_contextual_button_suw_height">48dp</dimen>
     <dimen name="taskbar_suw_frame">96dp</dimen>
-    <dimen name="taskbar_suw_insets">24dp</dimen>
+    <dimen name="taskbar_suw_insets">48dp</dimen>
 
     <!-- Keyboard Quick Switch -->
     <dimen name="keyboard_quick_switch_taskview_width">217.6dp</dimen>
diff --git a/quickstep/res/values-lo/strings.xml b/quickstep/res/values-lo/strings.xml
index b3ca116..2df1a49 100644
--- a/quickstep/res/values-lo/strings.xml
+++ b/quickstep/res/values-lo/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"ປັກໝຸດ"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"ຮູບແບບອິດສະຫລະ"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"ເດັສທັອບ"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"ຍ້າຍໄປຫາຈໍສະແດງຜົນພາຍນອກ"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"ເດັສທັອບ"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"ບໍ່ມີລາຍການຫຼ້າສຸດ"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"ການຕັ້ງຄ່າການນຳໃຊ້ແອັບ"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"ສະແດງແຖບໜ້າວຽກສະເໝີ"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"ປ່ຽນໂໝດການນຳທາງ"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"ເສັ້ນແບ່ງແຖບໜ້າວຽກ"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"ສ່ວນເພີ່ມເຕີມຂອງແຖບໜ້າວຽກ"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"ຍ້າຍໄປຊ້າຍ/ເທິງ"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"ຍ້າຍໄປຂວາ/ລຸ່ມ"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{ແອັບເພີ່ມເຕີມ}other{ແອັບເພີ່ມເຕີມ}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"ປິດທັງໝົດ"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"ຂະຫຍາຍ <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"ຫຍໍ້ <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g> ລົງ"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"ແຕ້ມວົງມົນເພື່ອຊອກຫາ"</string>
 </resources>
diff --git a/quickstep/res/values-lt/strings.xml b/quickstep/res/values-lt/strings.xml
index 4f3f36e..1d29f57 100644
--- a/quickstep/res/values-lt/strings.xml
+++ b/quickstep/res/values-lt/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"Prisegti"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"Laisva forma"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"Stalinis kompiuteris"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"Perkelkite į išorinį ekraną"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"Stalinis kompiuteris"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"Nėra jokių naujausių elementų"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"Programos naudojimo nustatymai"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Visada rodyti užduočių juostą"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Keisti naršymo režimą"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Užduočių juostos daliklis"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Užduočių juostos perpildymas"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Perkelti aukštyn, kairėn"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Perkelti žemyn, dešinėn"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{papildoma programa}one{papildoma programa}few{papildomos programos}many{papildomos programos}other{papildomų programų}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"Atsisakyti visų"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"išskleisti „<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>“"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"sutraukti „<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>“"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"Paieška apibrėžiant"</string>
 </resources>
diff --git a/quickstep/res/values-lv/strings.xml b/quickstep/res/values-lv/strings.xml
index fd75fc4..805a598 100644
--- a/quickstep/res/values-lv/strings.xml
+++ b/quickstep/res/values-lv/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"Piespraust"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"Brīva forma"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"Dators"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"Pārvietošana uz ārējo displeju"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"Darbvirsma"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"Nav nesenu vienumu."</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"Lietotņu izmantošanas iestatījumi"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Vienmēr rādīt uzdevumu joslu"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Mainīt navigācijas režīmu"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Uzdevumu joslas atdalītājs"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Uzdevumu joslas pārpilde"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Pārvietot uz augšējo/kreiso stūri"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Pārvietot uz apakšējo/labo stūri"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{papildu lietotne}zero{papildu lietotņu}one{papildu lietotne}other{papildu lietotnes}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"Nerādīt nevienu"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"izvērst “<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>”"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"sakļaut “<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>”"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"Apvilkt un meklēt"</string>
 </resources>
diff --git a/quickstep/res/values-mk/strings.xml b/quickstep/res/values-mk/strings.xml
index b50277b..2634b94 100644
--- a/quickstep/res/values-mk/strings.xml
+++ b/quickstep/res/values-mk/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"Закачи"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"Freeform"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"Работна површина"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"Префрлете се на надворешниот екран"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"За компјутер"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"Нема неодамнешни ставки"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"Поставки за користење на апликациите"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Секогаш прикажувај „Лента“"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Променете режим на навигација"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Разделник на „Лента со задачи“"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Проширено балонче на „Лента со задачи“"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Премести горе лево"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Премести долу десно"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{дополнителна апликација}one{дополнителна апликација}other{дополнителни апликации}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"Отфрли ги сите"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"прошири <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"собери <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"Пребарување со заокружување"</string>
 </resources>
diff --git a/quickstep/res/values-ml/strings.xml b/quickstep/res/values-ml/strings.xml
index ef72da1..92cad89 100644
--- a/quickstep/res/values-ml/strings.xml
+++ b/quickstep/res/values-ml/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"പിൻ ചെയ്യുക"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"ഫ്രീഫോം"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"ഡെസ്‌ക്ടോപ്പ്"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"ബാഹ്യ ഡിസ്‌പ്ലേയിലേക്ക് നീക്കുക"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"ഡെസ്‌ക്ടോപ്പ്"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"സമീപകാല ഇനങ്ങൾ ഒന്നുമില്ല"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"ആപ്പ് ഉപയോഗ ക്രമീകരണം"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"ടാസ്‌ക്ബാർ എപ്പോഴും കാണിക്കൂ"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"നാവിഗേഷൻ മോഡ് മാറ്റുക"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"ടാസ്‌ക്ബാർ ഡിവൈഡർ"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"ടാസ്‌ക്ബാർ ഓവർഫ്ലോ"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"മുകളിലേക്കോ ഇടത്തേക്കോ നീക്കുക"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"താഴേക്കോ വലത്തേക്കോ നീക്കുക"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{കൂടുതൽ ആപ്പ്}other{കൂടുതൽ ആപ്പുകൾ}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"എല്ലാം ഡിസ്മിസ് ചെയ്യുക"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g> വികസിപ്പിക്കുക"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g> ചുരുക്കുക"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"തിരയാൻ വട്ടം വരയ്ക്കൽ"</string>
 </resources>
diff --git a/quickstep/res/values-mn/strings.xml b/quickstep/res/values-mn/strings.xml
index 2ca2e26..539e104 100644
--- a/quickstep/res/values-mn/strings.xml
+++ b/quickstep/res/values-mn/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"Бэхлэх"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"Чөлөөтэй хувьсах"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"Компьютер"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"Гадаад дэлгэц рүү зөөх"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"Дэлгэц"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"Сүүлийн үеийн зүйл алга"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"Апп ашиглалтын тохиргоо"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Ажлын хэсгийг үргэлж харуулах"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Навигацын горимыг өөрчлөх"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Ажлын хэсгийг хуваагч"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Ажлын хэсгийн урт цэс"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Зүүн дээд хэсэг рүү зөөх"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Баруун доод хэсэг рүү зөөх"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{бусад апп}other{бусад апп}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"Бүгдийг үл хэрэгсэх"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>-г дэлгэх"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>-г хураах"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"Тойруулж зураад хай"</string>
 </resources>
diff --git a/quickstep/res/values-mr/strings.xml b/quickstep/res/values-mr/strings.xml
index 159368d..dd2003f 100644
--- a/quickstep/res/values-mr/strings.xml
+++ b/quickstep/res/values-mr/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"पिन करा"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"फ्रीफॉर्म"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"डेस्कटॉप"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"बाह्य डिस्प्लेवर हलवा"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"डेस्कटॉप"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"कोणतेही अलीकडील आयटम नाहीत"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"अ‍ॅप वापर सेटिंग्ज"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"नेहमी टास्कबार दाखवा"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"नेव्हिगेशन मोड बदला"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"टास्कबार विभाजक"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"टास्कबार ओव्हरफ्लो"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"सर्वात वरती/डावीकडे हलवा"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"तळाशी/उजवीकडे हलवा"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{आणखी अ‍ॅप}other{आणखी अ‍ॅप्स}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"सर्व डिसमिस करा"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g> चा विस्तार करा"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g> कोलॅप्स करा"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"शोधण्यासाठी वर्तुळ करा"</string>
 </resources>
diff --git a/quickstep/res/values-ms/strings.xml b/quickstep/res/values-ms/strings.xml
index 9a27e51..af388bc 100644
--- a/quickstep/res/values-ms/strings.xml
+++ b/quickstep/res/values-ms/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"Semat"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"Bentuk bebas"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"Desktop"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"Alihkan kepada paparan luaran"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"Desktop"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"Tiada item terbaharu"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"Tetapan penggunaan apl"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Papar Bar Tugas selalu"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Tukar mod navigasi"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Pembahagi Bar Tugas"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Limpahan Bar Tugas"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Alihkan ke atas/kiri"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Alihkan ke bawah/kanan"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{apl lagi}other{apl lagi}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"Ketepikan semua"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"kembangkan <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"kuncupkan <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"Bulatkan untuk Membuat Carian"</string>
 </resources>
diff --git a/quickstep/res/values-my/strings.xml b/quickstep/res/values-my/strings.xml
index 7e298fb..55b65c8 100644
--- a/quickstep/res/values-my/strings.xml
+++ b/quickstep/res/values-my/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"ပင်ထိုးရန်"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"အလွတ်ပုံစံ"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"ဒက်စ်တော့"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"ပြင်ပဖန်သားပြင်သို့ ရွှေ့ရန်"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"ဒက်စ်တော့"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"မကြာမီကဖွင့်ထားသည်များ မရှိပါ"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"အက်ပ်အသုံးပြုမှု ဆက်တင်များ"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Taskbar အမြဲပြရန်"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"ရွှေ့ကြည့်သည့်မုဒ် ပြောင်းရန်"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"လုပ်ဆောင်စရာဘား ပိုင်းခြားစနစ်"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Taskbar မီနူးအပို"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"အပေါ်/ဘယ်ဘက်သို့ ရွှေ့ရန်"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"အောက်ခြေ/ညာဘက်သို့ ရွှေ့ရန်"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{နောက်ထပ်အက်ပ်}other{နောက်ထပ်အက်ပ်များ}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"အားလုံးကို ပယ်ရန်"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g> ကို ပိုပြပါ"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g> ကို လျှော့ပြပါ"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"ရှာရန် ကွက်၍ဝိုင်းလိုက်ပါ"</string>
 </resources>
diff --git a/quickstep/res/values-nb/strings.xml b/quickstep/res/values-nb/strings.xml
index 21b5fd4..077bc0a 100644
--- a/quickstep/res/values-nb/strings.xml
+++ b/quickstep/res/values-nb/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"Fest"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"Fritt format"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"Skrivebord"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"Flytt til ekstern skjerm"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"Skrivebord"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"Ingen nylige elementer"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"Innstillinger for appbruk"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Vis alltid oppgavelinjen"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Endre navigasjonsmodus"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Skille for oppgavelinjen"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Overflyt for oppgavelinjen"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Flytt til øverst/venstre"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Flytt til nederst/høyre"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{app til}other{apper til}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"Lukk alle"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"vis <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"skjul <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"Circle to Search"</string>
 </resources>
diff --git a/quickstep/res/values-ne/strings.xml b/quickstep/res/values-ne/strings.xml
index 326222b..0f374ba 100644
--- a/quickstep/res/values-ne/strings.xml
+++ b/quickstep/res/values-ne/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"पिन गर्नुहोस्"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"फ्रिफर्म"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"डेस्कटप"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"सारेर बाह्य डिस्प्लेमा लैजानुहोस्"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"डेस्कटप"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"हालसालैको कुनै पनि वस्तु छैन"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"एपको उपयोगका सेटिङहरू"</string>
@@ -51,8 +52,8 @@
     <string name="back_gesture_feedback_swipe_too_far_from_edge" msgid="4175100312909721217">"स्क्रिनको सबैभन्दा दायाँ किनारा वा सबैभन्दा बायाँ किनाराबाट स्वाइप गर्नुहोस्"</string>
     <string name="back_gesture_feedback_cancelled" msgid="762621530959111290">"स्क्रिनको दायाँ वा बायाँ किनाराबाट मध्य भागसम्म स्वाइप गर्नुहोस् अनि औँला उठाउनुहोस्"</string>
     <string name="back_gesture_feedback_complete_with_overview_follow_up" msgid="9176400654037014471">"तपाईंले स्क्रिनको दायाँ किनाराबाट स्वाइप गरेर अघिल्लो स्क्रिनमा फर्कने तरिका सिक्नुभयो। अब एउटा एपबाट अर्को एपमा जाने तरिका सिक्नुहोस्।"</string>
-    <string name="back_gesture_feedback_complete_with_follow_up" msgid="8653374779579748392">"तपाईंले \'पछाडि जानुहोस्\' नामक इसारा प्रयोग गर्ने तरिका सिक्नुभयो। अब एउटा एपबाट अर्को एपमा जाने तरिका सिक्नुहोस्।"</string>
-    <string name="back_gesture_feedback_complete_without_follow_up" msgid="197189945858268342">"तपाईंले \"पछाडि जानुहोस्\" नामक इसारा प्रयोग गर्ने तरिका सिक्नुभयो"</string>
+    <string name="back_gesture_feedback_complete_with_follow_up" msgid="8653374779579748392">"तपाईंले जेस्चर प्रयोग गरी पछाडि जाने तरिका सिक्नुभएको छ। अब एउटा एपबाट अर्को एपमा जाने तरिका सिक्नुहोस्।"</string>
+    <string name="back_gesture_feedback_complete_without_follow_up" msgid="197189945858268342">"तपाईंले जेस्चर प्रयोग गरी पछाडि जाने तरिका सिक्नुभएको छ"</string>
     <string name="back_gesture_feedback_swipe_in_nav_bar" msgid="9157480023651452969">"स्क्रिनको फेदको धेरै नजिकसम्म स्वाइप नगर्नुहोस्"</string>
     <string name="back_gesture_tutorial_confirm_subtitle" msgid="5181305411668713250">"\'पछाडि\' नामक इसाराको संवेदनशीलता बदल्न सेटिङमा जानुहोस्"</string>
     <string name="back_gesture_intro_title" msgid="19551256430224428">"पछाडि जान स्वाइप गर्नुहोस्"</string>
@@ -63,7 +64,7 @@
     <string name="home_gesture_feedback_swipe_too_far_from_edge" msgid="4816365433160895458">"स्क्रिनको फेदबाट माथितिर स्वाइप गर्नुहोस्"</string>
     <string name="home_gesture_feedback_overview_detected" msgid="5177627157303895077">"औँला उठाउनुअघि नरोकिनुहोस्"</string>
     <string name="home_gesture_feedback_wrong_swipe_direction" msgid="8328465201424027148">"सीधै माथितिर स्वाइप गर्नुहोस्"</string>
-    <string name="home_gesture_feedback_complete_with_follow_up" msgid="8766981412895888417">"तपाईंले \"होम स्क्रिनमा जानुहोस्\" नामक इसारा प्रयोग गर्ने तरिका सिक्नुभयो। अब पछाडि जाने तरिका सिक्नुहोस्।"</string>
+    <string name="home_gesture_feedback_complete_with_follow_up" msgid="8766981412895888417">"तपाईंले जेस्चर प्रयोग गरी होम स्क्रिनमा जाने तरिका सिक्नुभएको छ। अब पछाडि जाने तरिका सिक्नुहोस्।"</string>
     <string name="home_gesture_feedback_complete_without_follow_up" msgid="2978063221383413443">"तपाईंले \"होम स्क्रिनमा जानुहोस्\" नामक इसारा प्रयोग गर्ने तरिका सिक्नुभयो"</string>
     <string name="home_gesture_intro_title" msgid="836590312858441830">"होम स्क्रिनमा जान स्वाइप गर्नुहोस्"</string>
     <string name="home_gesture_intro_subtitle" msgid="2632238748497975326">"स्क्रिनको फेदबाट माथितिर स्वाइप गर्नुहोस्। यो इसारा प्रयोग गर्दा सधैँ होम स्क्रिन खुल्छ।"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"टास्कबार सधैँ देखाउनुहोस्"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"नेभिगेसन मोड बदल्नुहोस्"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"टास्कबार डिभाइडर"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"टास्कबार ओभरफ्लो"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"सिरान/बायाँतिर सार्नुहोस्"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"फेद/दायाँतिर सार्नुहोस्"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{थप एप}other{थप एपहरू}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"सबै हटाउनुहोस्"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g> एक्स्पान्ड गर्नुहोस्"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g> कोल्याप्स गर्नुहोस्"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"खोज्न सर्कल बनाउनुहोस्"</string>
 </resources>
diff --git a/quickstep/res/values-night/colors.xml b/quickstep/res/values-night/colors.xml
index 98e4871..a1e9c70 100644
--- a/quickstep/res/values-night/colors.xml
+++ b/quickstep/res/values-night/colors.xml
@@ -26,6 +26,4 @@
 
     <!-- Turn on work apps button -->
     <color name="work_turn_on_stroke">?attr/materialColorPrimary</color>
-    <color name="work_fab_bg_color">?attr/materialColorPrimaryFixedDim</color>
-    <color name="work_fab_icon_color">?attr/materialColorOnPrimaryFixed</color>
 </resources>
\ No newline at end of file
diff --git a/quickstep/res/values-nl/strings.xml b/quickstep/res/values-nl/strings.xml
index 8a923b5..529516c 100644
--- a/quickstep/res/values-nl/strings.xml
+++ b/quickstep/res/values-nl/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"Vastzetten"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"Vrije vorm"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"Desktop"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"Verplaatsen naar extern scherm"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"Desktop"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"Geen recente items"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"Instellingen voor app-gebruik"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Taakbalk altijd tonen"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Navigatiemodus wijzigen"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Scheiding voor Taakbalk"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Taakbalkoverloop"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Naar boven/links verplaatsen"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Naar beneden/rechts verplaatsen"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{extra app}other{extra apps}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"Alles sluiten"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g> uitvouwen"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g> samenvouwen"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"Circle to Search"</string>
 </resources>
diff --git a/quickstep/res/values-or/strings.xml b/quickstep/res/values-or/strings.xml
index 3150ded..afc909d 100644
--- a/quickstep/res/values-or/strings.xml
+++ b/quickstep/res/values-or/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"ପିନ୍‍"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"ଫ୍ରିଫର୍ମ"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"ଡେସ୍କଟପ"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"ଏକ୍ସଟର୍ନଲ ଡିସପ୍ଲେକୁ ମୁଭ କରନ୍ତୁ"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"ଡେସ୍କଟପ"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"ବର୍ତ୍ତମାନର କୌଣସି ଆଇଟମ ନାହିଁ"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"ଆପ ବ୍ୟବହାର ସେଟିଂସ"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"ସର୍ବଦା ଟାସ୍କବାର ଦେଖାନ୍ତୁ"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"ନାଭିଗେସନ ମୋଡ ପରିବର୍ତ୍ତନ କରନ୍ତୁ"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"ଟାସ୍କବାର ଡିଭାଇଡର"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"ଟାସ୍କବାର ଓଭରଫ୍ଲୋ"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"ଶୀର୍ଷ/ବାମକୁ ମୁଭ କରନ୍ତୁ"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"ନିମ୍ନ/ଡାହାଣକୁ ମୁଭ କରନ୍ତୁ"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{ଅଧିକ ଆପ}other{ଅଧିକ ଆପ୍ସ}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"ସବୁ ଖାରଜ କରନ୍ତୁ"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g> ବିସ୍ତାର କରନ୍ତୁ"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g> ସଙ୍କୁଚିତ କରନ୍ତୁ"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"ସର୍ଚ୍ଚ କରିବାକୁ ସର୍କଲ କରନ୍ତୁ"</string>
 </resources>
diff --git a/quickstep/res/values-pa/strings.xml b/quickstep/res/values-pa/strings.xml
index 7da7555..69b33f9 100644
--- a/quickstep/res/values-pa/strings.xml
+++ b/quickstep/res/values-pa/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"ਪਿੰਨ ਕਰੋ"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"ਫ੍ਰੀਫਾਰਮ"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"ਡੈਸਕਟਾਪ"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"ਬਾਹਰੀ ਡਿਸਪਲੇ \'ਤੇ ਜਾਓ"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"ਡੈਸਕਟਾਪ"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"ਕੋਈ ਹਾਲੀਆ ਆਈਟਮ ਨਹੀਂ"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"ਐਪ ਵਰਤੋਂ ਦੀਆਂ ਸੈਟਿੰਗਾਂ"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"ਹਮੇਸ਼ਾਂ ਟਾਸਕਬਾਰ ਦਿਖਾਓ"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"ਨੈਵੀਗੇਸ਼ਨ ਮੋਡ ਬਦਲੋ"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"ਟਾਸਕਬਾਰ ਵਿਭਾਜਕ"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"ਟਾਸਕਬਾਰ ਓਵਰਫ਼ਲੋ"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"ਸਿਖਰਲੇ/ਖੱਬੇ ਪਾਸੇ ਲੈ ਕੇ ਜਾਓ"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"ਹੇਠਾਂ/ਸੱਜੇ ਪਾਸੇ ਲੈ ਕੇ ਜਾਓ"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{ਹੋਰ ਐਪ}one{ਹੋਰ ਐਪ}other{ਹੋਰ ਐਪਾਂ}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"ਸਭ ਖਾਰਜ ਕਰੋ"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g> ਦਾ ਵਿਸਤਾਰ ਕਰੋ"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g> ਨੂੰ ਸਮੇਟੋ"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"ਖੋਜਣ ਲਈ ਚੱਕਰ ਬਣਾਓ"</string>
 </resources>
diff --git a/quickstep/res/values-pl/strings.xml b/quickstep/res/values-pl/strings.xml
index 0cc49e2..88b5053 100644
--- a/quickstep/res/values-pl/strings.xml
+++ b/quickstep/res/values-pl/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"Przypnij"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"Tryb dowolny"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"Pulpit"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"Przenieś na wyświetlacz zewnętrzny"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"Pulpit"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"Brak ostatnich elementów"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"Ustawienia użycia aplikacji"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Zawsze pokazuj pasek aplikacji"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Zmień tryb nawigacji"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Linia dzielenia paska aplikacji"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Rozwijany pasek aplikacji"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Przesuń w górny lewy róg"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Przesuń w dolny prawy róg"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{inna aplikacja}few{inne aplikacje}many{innych aplikacji}other{innej aplikacji}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"Zamknij wszystkie"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"rozwiń dymek: <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"zwiń dymek: <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"Zaznacz, aby wyszukać"</string>
 </resources>
diff --git a/quickstep/res/values-pt-rPT/strings.xml b/quickstep/res/values-pt-rPT/strings.xml
index 33b87df..84120a1 100644
--- a/quickstep/res/values-pt-rPT/strings.xml
+++ b/quickstep/res/values-pt-rPT/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"Fixar"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"Forma livre"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"Computador"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"Mover para o ecrã externo"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"Computador"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"Nenhum item recente"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"Definições de utilização de aplicações"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Ver sempre Barra de tarefas"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Alterar modo de navegação"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Divisor da Barra de tarefas"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Menu adicional da Barra de tarefas"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Mover para a parte superior esquerda"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Mover para a part superior direita"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{outra app}other{outras apps}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"Ignorar tudo"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"expandir <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"reduzir <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"Circundar para Pesquisar"</string>
 </resources>
diff --git a/quickstep/res/values-pt/strings.xml b/quickstep/res/values-pt/strings.xml
index 0aa6295..3238c99 100644
--- a/quickstep/res/values-pt/strings.xml
+++ b/quickstep/res/values-pt/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"Fixar"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"Forma livre"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"Modo área de trabalho"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"Mover para a tela externa"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"Computador"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"Nenhum item recente"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"Configurações de uso do app"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Sempre mostrar a Barra de tarefas"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Mudar o modo de navegação"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Separador da Barra de tarefas"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Barra de tarefas flutuante"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Mover para cima/para a esquerda"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Mover para baixo/para a direita"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{outro app}one{outro app}other{outros apps}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"Dispensar todos"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"abrir <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"fechar <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"Circule para pesquisar"</string>
 </resources>
diff --git a/quickstep/res/values-ro/strings.xml b/quickstep/res/values-ro/strings.xml
index 2f9d287..e6aad47 100644
--- a/quickstep/res/values-ro/strings.xml
+++ b/quickstep/res/values-ro/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"Fixează"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"Formă liberă"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"Desktop"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"Mută pe ecranul extern"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"Computer"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"Niciun element recent"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"Setări de utilizare a aplicației"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Afișează mereu bara"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Schimbă modul de navigare"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Separator pentru bara de activități"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Meniu suplimentar pentru bara de activități"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Mută în stânga sus"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Mută în dreapta jos"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{aplicație suplimentară}few{mai multe aplicații}other{mai multe aplicații}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"Închide-le pe toate"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"extinde <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"restrânge <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"Încercuiește și caută"</string>
 </resources>
diff --git a/quickstep/res/values-ru/strings.xml b/quickstep/res/values-ru/strings.xml
index 2722ca9..297ae02 100644
--- a/quickstep/res/values-ru/strings.xml
+++ b/quickstep/res/values-ru/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"Закрепить"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"Произвольная форма"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"Мультиоконный режим"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"Перенести на внешний дисплей"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"Мультиоконный режим"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"Здесь пока ничего нет."</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"Настройки использования приложения"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Всегда показывать панель задач"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Изменить режим навигации"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Разделитель панели задач"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Дополнительное меню панели задач"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Переместить вверх или влево"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Переместить вниз или вправо"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{дополнительное приложение}one{дополнительное приложение}few{дополнительных приложения}many{дополнительных приложений}other{дополнительного приложения}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"Закрыть все"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"Развернуто: <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"Свернуто: <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"Обвести и найти"</string>
 </resources>
diff --git a/quickstep/res/values-si/strings.xml b/quickstep/res/values-si/strings.xml
index 19b61f7..2c7c672 100644
--- a/quickstep/res/values-si/strings.xml
+++ b/quickstep/res/values-si/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"අමුණන්න"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"Freeform"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"ඩෙස්ක්ටොපය"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"බාහිර සංදර්ශකය වෙත ගෙන යන්න"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"ඩෙස්ක්ටොපය"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"මෑත අයිතම නැත"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"යෙදුම් භාවිත සැකසීම්"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"සෑම විටම කාර්ය තීරුව පෙන්වන්න"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"සංචාලන ප්‍රකාරය වෙනස් කරන්න"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"කාර්ය තීරු බෙදනය"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"කාර්ය තීරුව පිටාර යාම"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"ඉහළ/වම වෙත ගෙන යන්න"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"පහළ/දකුණ වෙත ගෙන යන්න"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{තව යෙදුම}one{තවත් යෙදුම්}other{තවත් යෙදුම්}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"සියල්ල ඉවතලන්න"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g> දිග හරින්න"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g> හකුළන්න"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"සෙවීමට කවයසෙවීමට කවය අදින්න"</string>
 </resources>
diff --git a/quickstep/res/values-sk/strings.xml b/quickstep/res/values-sk/strings.xml
index 0f0ec27..638e88a 100644
--- a/quickstep/res/values-sk/strings.xml
+++ b/quickstep/res/values-sk/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"Pripnúť"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"Voľný režim"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"Počítač"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"Presunúť na externú obrazovku"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"Počítač"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"Žiadne nedávne položky"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"Nastavenia využívania aplikácie"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Zobrazovať panel aplikácií"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Zmeniť režim navigácie"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Rozdeľovač panela aplikácií"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Rozšírená ponuka panela aplikácií"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Presunúť hore alebo doľava"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Presunúť dole alebo doprava"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{ďalšia aplikácia}few{ďalšie aplikácie}many{ďalšie aplikácie}other{ďalšie aplikácie}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"Zavrieť všetko"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"rozbaliť <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"zbaliť <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"Vyhľadávanie krúžením"</string>
 </resources>
diff --git a/quickstep/res/values-sl/strings.xml b/quickstep/res/values-sl/strings.xml
index 972ced5..30d2c03 100644
--- a/quickstep/res/values-sl/strings.xml
+++ b/quickstep/res/values-sl/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"Pripni"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"Prosta oblika"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"Namizni računalnik"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"Premik v zunanji zaslon"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"Namizni način"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"Ni nedavnih elementov"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"Nastavitve uporabe aplikacij"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Stalen prikaz oprav. vrstice"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Spreminjanje načina navigacije"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Razdelilnik opravilne vrstice"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Oblaček opravilne vrstice z dodatnimi elementi"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Premakni na vrh/levo"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Premakni na dno/desno"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{dodatna aplikacija}one{dodatna aplikacija}two{dodatni aplikaciji}few{dodatne aplikacije}other{dodatnih aplikacij}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"Opusti vse"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"razširitev oblačka <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"strnitev oblačka <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"Iskanje z obkroževanjem"</string>
 </resources>
diff --git a/quickstep/res/values-sq/strings.xml b/quickstep/res/values-sq/strings.xml
index d2a7281..b4b6711 100644
--- a/quickstep/res/values-sq/strings.xml
+++ b/quickstep/res/values-sq/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"Gozhdo"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"Formë e lirë"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"Desktopi"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"Zhvendose tek ekrani i jashtëm"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"Desktopi"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"Nuk ka asnjë artikull të fundit"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"Cilësimet e përdorimit të aplikacionit"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Shfaq gjithmonë shiritin e detyrave"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Ndrysho modalitetin e navigimit"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Ndarësi i shiritit të detyrave"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Tejkalimi i shiritit të detyrave"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Lëviz në krye/majtas"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Lëviz në fund/djathtas"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{aplikacion tjetër}other{aplikacione të tjera}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"Hiqi të gjitha"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"zgjero <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"palos <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"Qarko për të kërkuar"</string>
 </resources>
diff --git a/quickstep/res/values-sr/strings.xml b/quickstep/res/values-sr/strings.xml
index 81204af..6622217 100644
--- a/quickstep/res/values-sr/strings.xml
+++ b/quickstep/res/values-sr/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"Закачи"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"Слободни облик"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"Рачунар"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"Преместите на спољни екран"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"Рачунари"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"Нема недавних ставки"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"Подешавања коришћења апликације"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Увек приказуј траку задатака"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Промени режим навигације"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Разделник траке задатака"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Преклопна трака задатака"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Премести горе лево"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Премести доле десно"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{додатна апликација}one{додатна апликација}few{додатне апликације}other{додатних апликација}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"Одбаци све"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"проширите облачић <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"скупите облачић <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"Претрага заокруживањем"</string>
 </resources>
diff --git a/quickstep/res/values-sv/strings.xml b/quickstep/res/values-sv/strings.xml
index 5d0c7a3..089d1b5 100644
--- a/quickstep/res/values-sv/strings.xml
+++ b/quickstep/res/values-sv/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"Fäst"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"Fritt format"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"Dator"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"Flytta till extern skärm"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"Dator"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"Listan är tom"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"Inställningar för appanvändning"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Visa alltid aktivitetsfältet"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Ändra navigeringsläge"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Avdelare för aktivitetsfältet"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Fler alternativ för aktivitetsfältet"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Flytta högst upp/till vänster"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Flytta längst ned/till höger"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{app till}other{appar till}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"Stäng alla"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"utöka <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"komprimera <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"Circle to Search"</string>
 </resources>
diff --git a/quickstep/res/values-sw/strings.xml b/quickstep/res/values-sw/strings.xml
index e133ba3..821797b 100644
--- a/quickstep/res/values-sw/strings.xml
+++ b/quickstep/res/values-sw/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"Bandika"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"Muundo huru"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"Kompyuta ya mezani"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"Hamishia programu kwenye skrini ya nje"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"Kompyuta ya Mezani"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"Hakuna vipengee vya hivi karibuni"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"Mipangilio ya matumizi ya programu"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Onyesha Zana kila wakati"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Badilisha hali ya usogezaji"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Kitenganishi cha Upauzana"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Upauzana wa Vipengele vya Ziada"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Sogeza juu/kushoto"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Sogeza chini/kulia"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{programu nyingine}other{programu zingine}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"Ondoa vyote"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"panua <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"kunja <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"Chora Mviringo ili Kutafuta"</string>
 </resources>
diff --git a/quickstep/res/values-sw600dp-land/dimens.xml b/quickstep/res/values-sw600dp-land/dimens.xml
index 5e9a177..49239aa 100644
--- a/quickstep/res/values-sw600dp-land/dimens.xml
+++ b/quickstep/res/values-sw600dp-land/dimens.xml
@@ -33,4 +33,6 @@
     <!-- The bottom margin above the bottom row of tasks in grid only overview -->
     <dimen name="overview_bottom_margin_grid_only">40dp</dimen>
 
+    <dimen name="taskbar_suw_insets">24dp</dimen>
+
 </resources>
diff --git a/quickstep/res/values-ta/strings.xml b/quickstep/res/values-ta/strings.xml
index 95d0fa9..7bbfaba 100644
--- a/quickstep/res/values-ta/strings.xml
+++ b/quickstep/res/values-ta/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"பின் செய்தல்"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"குறிப்பிட்ட வடிவமில்லாத பயன்முறை"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"டெஸ்க்டாப்"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"வெளிப்புற டிஸ்ப்ளேவிற்கு நகர்த்துதல்"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"டெஸ்க்டாப்"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"சமீபத்தியவை எதுவுமில்லை"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"ஆப்ஸ் உபயோக அமைப்புகள்"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"செயல் பட்டியை எப்போதும் காட்டு"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"வழிசெலுத்தல் பயன்முறையை மாற்று"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"செயல் பட்டிப் பிரிப்பான்"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"செயல் பட்டிக்கான கூடுதல் விருப்பங்கள்"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"மேலே/இடதுபுறம் நகர்த்தும்"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"கீழே/வலதுபுறம் நகர்த்தும்"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{கூடுதல் ஆப்ஸ்}other{கூடுதல் ஆப்ஸ்}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"அனைத்தையும் மூடும்"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g> ஐ விரிவாக்கும்"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g> ஐச் சுருக்கும்"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"வட்டமிட்டுத் தேடல்"</string>
 </resources>
diff --git a/quickstep/res/values-te/strings.xml b/quickstep/res/values-te/strings.xml
index 116d388..5439e80 100644
--- a/quickstep/res/values-te/strings.xml
+++ b/quickstep/res/values-te/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"పిన్ చేయండి"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"సంప్రదాయేతర"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"డెస్క్‌టాప్"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"ఎక్స్‌టర్నల్ డిస్‌ప్లేకు తరలించండి"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"డెస్క్‌టాప్"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"ఇటీవలి ఐటెమ్‌లు ఏవీ లేవు"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"యాప్ వినియోగ సెట్టింగ్‌లు"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"టాస్క్‌బార్‌ను నిరంతరం చూపండి"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"నావిగేషన్ మోడ్‌ను మార్చండి"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"టాస్క్‌బార్ డివైడర్"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"టాస్క్‌బార్ ఓవర్‌ఫ్లో"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"ఎగువ/ఎడమ వైపునకు తరలించండి"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"దిగువ/కుడి వైపునకు తరలించండి"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{మరో యాప్‌}other{మరిన్ని యాప్‌లు}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"అన్నింటినీ విస్మరించండి"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>ను విస్తరించండి"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>ను కుదించండి"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"సెర్చ్ చేయడానికి సర్కిల్ గీయండి"</string>
 </resources>
diff --git a/quickstep/res/values-th/strings.xml b/quickstep/res/values-th/strings.xml
index ecdb3b5..30e73e6 100644
--- a/quickstep/res/values-th/strings.xml
+++ b/quickstep/res/values-th/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"ปักหมุด"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"รูปแบบอิสระ"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"เดสก์ท็อป"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"ย้ายไปยังจอแสดงผลภายนอก"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"เดสก์ท็อป"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"ไม่มีรายการล่าสุด"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"การตั้งค่าการใช้แอป"</string>
@@ -51,8 +52,8 @@
     <string name="back_gesture_feedback_swipe_too_far_from_edge" msgid="4175100312909721217">"ปัดจากขอบด้านขวาสุดหรือซ้ายสุด"</string>
     <string name="back_gesture_feedback_cancelled" msgid="762621530959111290">"ตรวจสอบว่าปัดจากขอบด้านขวาหรือซ้ายไปตรงกลางหน้าจอ แล้วยกนิ้วขึ้น"</string>
     <string name="back_gesture_feedback_complete_with_overview_follow_up" msgid="9176400654037014471">"คุณรู้วิธีปัดจากด้านขวาเพื่อย้อนกลับแล้ว ต่อไปดูวิธีสลับแอป"</string>
-    <string name="back_gesture_feedback_complete_with_follow_up" msgid="8653374779579748392">"คุณทำท่าทางสัมผัสเพื่อย้อนกลับเสร็จแล้ว ต่อไปดูวิธีสลับแอป"</string>
-    <string name="back_gesture_feedback_complete_without_follow_up" msgid="197189945858268342">"คุณทำท่าทางสัมผัสเพื่อย้อนกลับเสร็จแล้ว"</string>
+    <string name="back_gesture_feedback_complete_with_follow_up" msgid="8653374779579748392">"คุณทำท่าทางสัมผัสเพื่อย้อนกลับสำเร็จแล้ว ต่อไปดูวิธีสลับแอป"</string>
+    <string name="back_gesture_feedback_complete_without_follow_up" msgid="197189945858268342">"คุณทำท่าทางสัมผัสเพื่อย้อนกลับสำเร็จแล้ว"</string>
     <string name="back_gesture_feedback_swipe_in_nav_bar" msgid="9157480023651452969">"ไม่ปัดใกล้กับด้านล่างของหน้าจอมากเกินไป"</string>
     <string name="back_gesture_tutorial_confirm_subtitle" msgid="5181305411668713250">"เปลี่ยนความไวของท่าทางสัมผัสเพื่อย้อนกลับได้ที่การตั้งค่า"</string>
     <string name="back_gesture_intro_title" msgid="19551256430224428">"ปัดเพื่อย้อนกลับ"</string>
@@ -63,8 +64,8 @@
     <string name="home_gesture_feedback_swipe_too_far_from_edge" msgid="4816365433160895458">"ปัดขึ้นจากขอบด้านล่างของหน้าจอ"</string>
     <string name="home_gesture_feedback_overview_detected" msgid="5177627157303895077">"ไม่ต้องหยุดชั่วคราวก่อนยกนิ้วขึ้น"</string>
     <string name="home_gesture_feedback_wrong_swipe_direction" msgid="8328465201424027148">"ปัดขึ้นในแนวตรง"</string>
-    <string name="home_gesture_feedback_complete_with_follow_up" msgid="8766981412895888417">"คุณทำท่าทางสัมผัสเพื่อไปที่หน้าแรกเสร็จแล้ว ต่อไปดูวิธีย้อนกลับ"</string>
-    <string name="home_gesture_feedback_complete_without_follow_up" msgid="2978063221383413443">"คุณทำท่าทางสัมผัสเพื่อไปที่หน้าแรกเสร็จแล้ว"</string>
+    <string name="home_gesture_feedback_complete_with_follow_up" msgid="8766981412895888417">"คุณทำท่าทางสัมผัสเพื่อไปที่หน้าแรกสำเร็จแล้ว ต่อไปดูวิธีย้อนกลับ"</string>
+    <string name="home_gesture_feedback_complete_without_follow_up" msgid="2978063221383413443">"คุณทำท่าทางสัมผัสเพื่อไปที่หน้าแรกสำเร็จแล้ว"</string>
     <string name="home_gesture_intro_title" msgid="836590312858441830">"ปัดเพื่อไปที่หน้าแรก"</string>
     <string name="home_gesture_intro_subtitle" msgid="2632238748497975326">"ปัดขึ้นจากด้านล่างของหน้าจอ ท่าทางสัมผัสนี้จะนำคุณไปที่หน้าจอหลักเสมอ"</string>
     <string name="home_gesture_spoken_intro_subtitle" msgid="1030987707382031750">"ใช้ 2 นิ้วปัดขึ้นจากด้านล่างของหน้าจอ ท่าทางสัมผัสนี้จะนำคุณไปที่หน้าจอหลักเสมอ"</string>
@@ -75,7 +76,7 @@
     <string name="overview_gesture_feedback_home_detected" msgid="663432226180397138">"ลองแตะหน้าต่างค้างไว้นานขึ้นก่อนปล่อยนิ้ว"</string>
     <string name="overview_gesture_feedback_wrong_swipe_direction" msgid="1191055451018584958">"ปัดขึ้นในแนวตรง แล้วหยุดชั่วคราว"</string>
     <string name="overview_gesture_feedback_complete_with_follow_up" msgid="3544611727467765026">"คุณรู้วิธีใช้ท่าทางสัมผัสแล้ว หากต้องการปิดท่าทางสัมผัส ให้ไปที่การตั้งค่า"</string>
-    <string name="overview_gesture_feedback_complete_without_follow_up" msgid="2903050864432331629">"คุณทำท่าทางสัมผัสเพื่อสลับแอปเสร็จแล้ว"</string>
+    <string name="overview_gesture_feedback_complete_without_follow_up" msgid="2903050864432331629">"คุณทำท่าทางสัมผัสเพื่อสลับแอปสำเร็จแล้ว"</string>
     <string name="overview_gesture_intro_title" msgid="2902054412868489378">"ปัดเพื่อสลับแอป"</string>
     <string name="overview_gesture_intro_subtitle" msgid="4968091015637850859">"หากต้องการสลับระหว่างแอปต่างๆ ให้ปัดขึ้นจากด้านล่างของหน้าจอ ค้างไว้ แล้วปล่อย"</string>
     <string name="overview_gesture_spoken_intro_subtitle" msgid="3853371838260201751">"หากต้องการสลับระหว่างแอป ให้ใช้ 2 นิ้วปัดขึ้นจากด้านล่างของหน้าจอค้างไว้แล้วปล่อย"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"แสดงแถบงานเสมอ"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"เปลี่ยนโหมดการนําทาง"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"ตัวแบ่งแถบงาน"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"การดำเนินการเพิ่มเติมของแถบงาน"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"ย้ายไปที่ด้านบนหรือด้านซ้าย"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"ย้ายไปที่ด้านล่างหรือด้านขวา"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{แอปเพิ่มเติม}other{แอปเพิ่มเติม}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"ปิดทั้งหมด"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"ขยาย <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"ยุบ <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"วงเพื่อค้นหา"</string>
 </resources>
diff --git a/quickstep/res/values-tl/strings.xml b/quickstep/res/values-tl/strings.xml
index da646a4..583f419 100644
--- a/quickstep/res/values-tl/strings.xml
+++ b/quickstep/res/values-tl/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"I-pin"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"Freeform"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"Desktop"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"Ilipat sa external na display"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"Desktop"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"Walang kamakailang item"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"Mga setting ng paggamit ng app"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Ipakita lagi ang Taskbar"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Magpalit ng navigation mode"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Divider ng Taskbar"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Taskbar Overflow"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Ilipat sa itaas/kaliwa"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Ilipat sa ibaba/kanan"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{pang app}one{pang app}other{pang app}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"I-dismiss lahat"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"i-expand ang <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"i-collapse ang <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"Circle to Search"</string>
 </resources>
diff --git a/quickstep/res/values-tr/strings.xml b/quickstep/res/values-tr/strings.xml
index 7be5464..c50c1f8 100644
--- a/quickstep/res/values-tr/strings.xml
+++ b/quickstep/res/values-tr/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"Sabitle"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"Serbest çalışma"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"Masaüstü"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"Harici ekrana taşı"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"Masaüstü"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"Yeni öğe yok"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"Uygulama kullanım ayarları"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Görev çubuğunu daima göster"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Gezinme modunu değiştir"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Görev Çubuğu Ayırıcısı"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Görev Çubuğu Taşması"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Sol üste taşı"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Sağ alta taşı"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{uygulama daha}other{uygulama daha}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"Tümünü kapat"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"genişlet: <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"daralt: <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"Seçerek Arat"</string>
 </resources>
diff --git a/quickstep/res/values-uk/strings.xml b/quickstep/res/values-uk/strings.xml
index 216ae0b..d75f8b4 100644
--- a/quickstep/res/values-uk/strings.xml
+++ b/quickstep/res/values-uk/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"Закріпити"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"Довільна форма"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"Робочий стіл"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"Перемістити на зовнішній екран"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"Комп’ютер"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"Немає нещодавніх додатків"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"Налаштування використання додатка"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Завжди показув. панель завдань"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Змінити режим навігації"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Розділювач панелі завдань"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Додаткове меню панелі завдань"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Перемістити вгору або вліво"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Перемістити вниз або вправо"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{інший додаток}one{інший додаток}few{інші додатки}many{інших додатків}other{іншого додатка}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"Закрити все"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"розгорнути \"<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>\""</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"згорнути \"<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>\""</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"Обвести й знайти"</string>
 </resources>
diff --git a/quickstep/res/values-ur/strings.xml b/quickstep/res/values-ur/strings.xml
index 2307cb1..68f838f 100644
--- a/quickstep/res/values-ur/strings.xml
+++ b/quickstep/res/values-ur/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"پن کریں"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"فری فارم"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"ڈیسک ٹاپ"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"بیرونی ڈسپلے پر متقل کریں"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"ڈیسک ٹاپ"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"کوئی حالیہ آئٹم نہیں"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"ایپ کے استعمال کی ترتیبات"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"ہمیشہ ٹاسک بار دکھائیں"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"نیویگیشن موڈ تبدیل کریں"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"ٹاسک بار ڈیوائیڈر"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"ٹاسک بار اوورفلو"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"اوپر/بائیں طرف منتقل کریں"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"نیچے/دائیں طرف منتقل کریں"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{مزید ایپ}other{مزید ایپس}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"سبھی کو برخاست کریں"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g> کو پھیلائیں"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g> کو سکیڑیں"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"تلاش کرنے کیلئے دائرہ بنائیں"</string>
 </resources>
diff --git a/quickstep/res/values-uz/strings.xml b/quickstep/res/values-uz/strings.xml
index 9f98b55..294be84 100644
--- a/quickstep/res/values-uz/strings.xml
+++ b/quickstep/res/values-uz/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"Qadash"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"Erkin shakl"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"Desktop"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"Tashqi displeyga olish"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"Desktop"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"Yaqinda ishlatilgan ilovalar yo‘q"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"Ilovadan foydalanish sozlamalari"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Vazifalar paneli doim chiqarilsin"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Navigatsiya rejimini oʻzgartirish"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Vazifalar panelini ajratkich"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Vazifalar panelini kengaytirish"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Yuqoriga yoki chapga oʻtkazish"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Pastga yoki oʻngga oʻtkazish"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{boshqa ilova}other{boshqa ilovalar}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"Hammasini yopish"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>ni yoyish"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>ni yigʻish"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"Chizib qidirish"</string>
 </resources>
diff --git a/quickstep/res/values-vi/strings.xml b/quickstep/res/values-vi/strings.xml
index 50acbbc..7eeacde 100644
--- a/quickstep/res/values-vi/strings.xml
+++ b/quickstep/res/values-vi/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"Ghim"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"Dạng tự do"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"Máy tính"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"Chuyển sang màn hình ngoài"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"Máy tính"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"Không có mục gần đây nào"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"Cài đặt mức sử dụng ứng dụng"</string>
@@ -52,7 +53,7 @@
     <string name="back_gesture_feedback_cancelled" msgid="762621530959111290">"Hãy vuốt từ mép phải hoặc mép trái tới giữa màn hình rồi nhấc ngón tay ra"</string>
     <string name="back_gesture_feedback_complete_with_overview_follow_up" msgid="9176400654037014471">"Bạn đã học được cách vuốt từ mép phải để quay lại. Tiếp theo, hãy tìm hiểu cách chuyển đổi ứng dụng."</string>
     <string name="back_gesture_feedback_complete_with_follow_up" msgid="8653374779579748392">"Bạn đã thực hiện xong cử chỉ quay lại. Tiếp theo, hãy tìm hiểu cách chuyển đổi ứng dụng."</string>
-    <string name="back_gesture_feedback_complete_without_follow_up" msgid="197189945858268342">"Bạn đã thực hiện xong cử chỉ quay lại"</string>
+    <string name="back_gesture_feedback_complete_without_follow_up" msgid="197189945858268342">"Bạn đã hoàn tất cử chỉ quay lại"</string>
     <string name="back_gesture_feedback_swipe_in_nav_bar" msgid="9157480023651452969">"Hãy nhớ không được vuốt quá gần phần dưới cùng của màn hình"</string>
     <string name="back_gesture_tutorial_confirm_subtitle" msgid="5181305411668713250">"Để thay đổi độ nhạy của cử chỉ quay lại, hãy vào mục Cài đặt"</string>
     <string name="back_gesture_intro_title" msgid="19551256430224428">"Vuốt để quay lại"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Luôn hiện Thanh tác vụ"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Thay đổi chế độ điều hướng"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Đường phân chia Taskbar"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Trình đơn mục bổ sung trên thanh tác vụ"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Chuyển lên trên cùng/sang bên trái"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Chuyển xuống dưới cùng/sang bên phải"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{ứng dụng khác}other{ứng dụng khác}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"Đóng tất cả"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"mở rộng <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"thu gọn <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"Khoanh tròn để tìm kiếm"</string>
 </resources>
diff --git a/quickstep/res/values-zh-rCN/strings.xml b/quickstep/res/values-zh-rCN/strings.xml
index 1b6c4e4..dc16036 100644
--- a/quickstep/res/values-zh-rCN/strings.xml
+++ b/quickstep/res/values-zh-rCN/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"固定"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"自由窗口"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"桌面"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"移至外接显示屏"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"桌面设备"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"近期没有任何内容"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"应用使用设置"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"始终显示任务栏"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"更改导航模式"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"任务栏分隔线"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"任务栏溢出图标"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"移到顶部/左侧"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"移到底部/右侧"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{多个应用}other{多个应用}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"全部关闭"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"展开“<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>”"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"收起“<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>”"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"圈定即搜"</string>
 </resources>
diff --git a/quickstep/res/values-zh-rHK/strings.xml b/quickstep/res/values-zh-rHK/strings.xml
index 38608e5..c8d18eb 100644
--- a/quickstep/res/values-zh-rHK/strings.xml
+++ b/quickstep/res/values-zh-rHK/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"固定"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"自由形式"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"桌面"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"移至外部顯示屏"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"桌面"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"最近沒有任何項目"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"應用程式使用情況設定"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"一律顯示工作列"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"變更導覽模式"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"工作列分隔線"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"工作列溢位"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"移至上方/左側"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"移至底部/右側"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{個其他應用程式}other{個其他應用程式}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"全部關閉"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"打開<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"收埋<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"一圈即搜"</string>
 </resources>
diff --git a/quickstep/res/values-zh-rTW/strings.xml b/quickstep/res/values-zh-rTW/strings.xml
index 3e46779..fd132d2 100644
--- a/quickstep/res/values-zh-rTW/strings.xml
+++ b/quickstep/res/values-zh-rTW/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"固定"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"自由形式"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"桌面"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"移至外接螢幕"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"電腦"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"最近沒有任何項目"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"應用程式使用情況設定"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"一律顯示工作列"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"變更操作模式"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"工作列分隔線"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"工作列溢位"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"移到上方/左側"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"移到底部/右側"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{個其他應用程式}other{個其他應用程式}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"全部關閉"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"展開「<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>」"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"收合「<xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>」"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"畫圈搜尋"</string>
 </resources>
diff --git a/quickstep/res/values-zu/strings.xml b/quickstep/res/values-zu/strings.xml
index 120b387..46dbbd5 100644
--- a/quickstep/res/values-zu/strings.xml
+++ b/quickstep/res/values-zu/strings.xml
@@ -22,6 +22,7 @@
     <string name="recent_task_option_pin" msgid="7929860679018978258">"Phina"</string>
     <string name="recent_task_option_freeform" msgid="48863056265284071">"I-Freeform"</string>
     <string name="recent_task_option_desktop" msgid="8280879717125435668">"Ideskithophu"</string>
+    <string name="recent_task_option_external_display" msgid="4533840664313389484">"Hambisa esibonisini sangaphandle"</string>
     <string name="recent_task_desktop" msgid="8081113562549637334">"Ideskithophu"</string>
     <string name="recents_empty_message" msgid="7040467240571714191">"Azikho izinto zakamuva"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"Izilungiselelo zokusetshenziswa kohlelo lokusebenza"</string>
@@ -139,6 +140,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Bonisa i-Taskbar njalo."</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Shintsha imodi yokufuna"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Isihlukanisi se-Taskbar"</string>
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Ukuphuphuma Kwetaskbar"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Hamba phezulu/kwesokunxele"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Hamba phansi/kwesokudla"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{i-app eyengeziwe}one{ama-app engeziwe}other{ama-app engeziwe}}"</string>
@@ -153,4 +155,5 @@
     <string name="bubble_bar_action_dismiss_all" msgid="3290722022983403060">"Chitha konke"</string>
     <string name="bubble_bar_accessibility_announce_expand" msgid="1503192695527477102">"nweba <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
     <string name="bubble_bar_accessibility_announce_collapse" msgid="928284600086798791">"goqa <xliff:g id="BUBBLE_DESCRIPTION">%1$s</xliff:g>"</string>
+    <string name="search_gesture_feature_title" msgid="1294044108313175306">"Khethela Ukusesha"</string>
 </resources>
diff --git a/quickstep/res/values/colors.xml b/quickstep/res/values/colors.xml
index 4c48bd3..668bce7 100644
--- a/quickstep/res/values/colors.xml
+++ b/quickstep/res/values/colors.xml
@@ -31,7 +31,6 @@
     <color name="taskbar_nav_icon_dark_color_on_home">#99000000</color>
     <color name="taskbar_stashed_handle_light_color">#EBffffff</color>
     <color name="taskbar_stashed_handle_dark_color">#99000000</color>
-    <color name="taskbar_running_app_indicator_color">#646464</color>
 
     <!-- Floating rotation button -->
     <color name="floating_rotation_button_light_color">#ffffff</color>
@@ -95,6 +94,4 @@
 
     <!-- Turn on work apps button -->
     <color name="work_turn_on_stroke">?attr/materialColorPrimary</color>
-    <color name="work_fab_bg_color">?attr/materialColorPrimaryFixedDim</color>
-    <color name="work_fab_icon_color">?attr/materialColorOnPrimaryFixed</color>
 </resources>
\ No newline at end of file
diff --git a/quickstep/res/values/config.xml b/quickstep/res/values/config.xml
index e8cb5d5..f3c9467 100644
--- a/quickstep/res/values/config.xml
+++ b/quickstep/res/values/config.xml
@@ -33,13 +33,10 @@
     <string name="taskbar_model_callbacks_factory_class" translatable="false">com.android.launcher3.taskbar.TaskbarModelCallbacksFactory</string>
     <string name="taskbar_view_callbacks_factory_class" translatable="false">com.android.launcher3.taskbar.TaskbarViewCallbacksFactory</string>
     <string name="launcher_restore_event_logger_class" translatable="false">com.android.quickstep.LauncherRestoreEventLoggerImpl</string>
-    <string name="plugin_manager_wrapper_class" translatable="false">com.android.launcher3.uioverrides.plugins.PluginManagerWrapperImpl</string>
     <string name="taskbar_edu_tooltip_controller_class" translatable="false">com.android.launcher3.taskbar.TaskbarEduTooltipController</string>
-    <string name="contextual_edu_manager_class" translatable="false">com.android.quickstep.contextualeducation.SystemContextualEduStatsManager</string>
     <string name="nav_handle_long_press_handler_class" translatable="false"></string>
-    <string name="assist_utils_class" translatable="false"></string>
-    <string name="assist_state_manager_class" translatable="false"></string>
-    <string name="api_wrapper_class" translatable="false">com.android.launcher3.uioverrides.SystemApiWrapper</string>
+    <string name="contextual_search_invoker_class" translatable="false"></string>
+    <string name="contextual_search_state_manager_class" translatable="false"></string>
 
     <!-- The number of thumbnails and icons to keep in the cache. The thumbnail cache size also
          determines how many thumbnails will be fetched in the background. -->
@@ -55,6 +52,11 @@
 
     <integer name="max_depth_blur_radius">23</integer>
 
+    <!-- If predicted widgets from prediction service are less than this number, additional
+    eligible widgets may be added locally by launcher. When set to 0, no widgets will be added
+    locally. -->
+    <integer name="widget_predictions_min_count">6</integer>
+
     <!-- Accessibility actions -->
     <item type="id" name="action_move_to_top_or_left" />
     <item type="id" name="action_move_to_bottom_or_right" />
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index 8957e0d..6367a01 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -111,6 +111,7 @@
     <dimen name="motion_pause_detector_speed_very_slow">0.0285dp</dimen>
     <dimen name="motion_pause_detector_speed_slow">0.15dp</dimen>
     <dimen name="motion_pause_detector_speed_somewhat_fast">0.285dp</dimen>
+    <dimen name="motion_pause_detector_speed_trackpad_somewhat_fast">0.7dp</dimen>
     <dimen name="motion_pause_detector_speed_fast">1.4dp</dimen>
     <dimen name="motion_pause_detector_min_displacement_from_app">36dp</dimen>
     <dimen name="quickstep_fling_threshold_speed">0.5dp</dimen>
@@ -356,12 +357,15 @@
     <dimen name="taskbar_back_button_suw_start_margin">48dp</dimen>
     <dimen name="taskbar_back_button_suw_bottom_margin">1dp</dimen>
     <dimen name="taskbar_back_button_suw_height">72dp</dimen>
-    <dimen name="taskbar_running_app_indicator_height">4dp</dimen>
-    <dimen name="taskbar_running_app_indicator_width">14dp</dimen>
-    <dimen name="taskbar_running_app_indicator_top_margin">2dp</dimen>
-    <dimen name="taskbar_minimized_app_indicator_height">2dp</dimen>
-    <dimen name="taskbar_minimized_app_indicator_width">12dp</dimen>
-    <dimen name="taskbar_minimized_app_indicator_top_margin">2dp</dimen>
+    <dimen name="taskbar_running_app_indicator_height">2dp</dimen>
+    <dimen name="taskbar_running_app_indicator_width">12dp</dimen>
+    <dimen name="taskbar_running_app_indicator_top_margin">4dp</dimen>
+    <dimen name="taskbar_minimized_app_indicator_width">6dp</dimen>
+    <dimen name="taskbar_overflow_item_icon_size_default">22dp</dimen>
+    <dimen name="taskbar_overflow_item_icon_size_scaled_down">15dp</dimen>
+    <dimen name="taskbar_overflow_item_icon_stroke_width_default">2dp</dimen>
+    <dimen name="taskbar_overflow_leave_behind_size_default">18dp</dimen>
+    <dimen name="taskbar_overflow_leave_behind_size_scaled_down">15dp</dimen>
 
     <!-- Transient taskbar -->
     <dimen name="transient_taskbar_padding">12dp</dimen>
@@ -424,6 +428,7 @@
     <!--- Taskbar Pinning -->
     <dimen name="taskbar_pinning_popup_menu_width">300dp</dimen>
     <dimen name="taskbar_pinning_popup_menu_vertical_margin">16dp</dimen>
+    <dimen name="taskbar_pinning_popup_menu_min_padding_from_screen_edge">16dp</dimen>
 
     <!--- Floating Ime Inset height-->
     <dimen name="floating_ime_inset_height">60dp</dimen>
@@ -503,6 +508,7 @@
     <dimen name="keyboard_quick_switch_recents_icon_size">20dp</dimen>
     <dimen name="keyboard_quick_switch_desktop_icon_size">32dp</dimen>
     <dimen name="keyboard_quick_switch_margin_top">56dp</dimen>
+    <dimen name="keyboard_quick_switch_margin_bottom">24dp</dimen>
     <dimen name="keyboard_quick_switch_margin_ends">16dp</dimen>
     <dimen name="keyboard_quick_switch_view_spacing">16dp</dimen>
     <dimen name="keyboard_quick_switch_view_small_spacing">4dp</dimen>
diff --git a/quickstep/res/values/ids.xml b/quickstep/res/values/ids.xml
index 3091d9e..c71bb76 100644
--- a/quickstep/res/values/ids.xml
+++ b/quickstep/res/values/ids.xml
@@ -19,4 +19,6 @@
     <item type="id" name="action_move_left" />
     <item type="id" name="action_move_right" />
     <item type="id" name="action_dismiss_all" />
+
+    <item type="id" name="bubble_bar_flyout_view" />
 </resources>
\ No newline at end of file
diff --git a/quickstep/res/values/strings.xml b/quickstep/res/values/strings.xml
index 008766b..026e25c 100644
--- a/quickstep/res/values/strings.xml
+++ b/quickstep/res/values/strings.xml
@@ -28,6 +28,8 @@
     <string name="recent_task_option_freeform">Freeform</string>
     <!-- Title and content description for an option to enter desktop windowing mode for a given app -->
     <string name="recent_task_option_desktop">Desktop</string>
+    <!-- Title and content description for an option to move app to external display. -->
+    <string name="recent_task_option_external_display">Move to external display</string>
 
     <!-- Title and content description for Desktop tile in Recents screen that contains apps opened inside desktop windowing mode [CHAR LIMIT=NONE] -->
     <string name="recent_task_desktop">Desktop</string>
@@ -360,4 +362,8 @@
     <string name="bubble_bar_accessibility_announce_expand">expand <xliff:g id="bubble_description" example="some title from Messages">%1$s</xliff:g></string>
     <!-- Accessibility announcement when the bubble bar collapses. [CHAR LIMIT=NONE]-->
     <string name="bubble_bar_accessibility_announce_collapse">collapse <xliff:g id="bubble_description" example="some title from Messages">%1$s</xliff:g></string>
+
+    <!-- Name of Google's new feature to circle to search anything on your phone screen, without
+     switching apps. [CHAR_LIMIT=60] -->
+    <string name="search_gesture_feature_title">Circle to Search</string>
 </resources>
diff --git a/quickstep/src/com/android/launcher3/LauncherInitListener.java b/quickstep/src/com/android/launcher3/LauncherInitListener.java
index 523923d..4e4ffe7 100644
--- a/quickstep/src/com/android/launcher3/LauncherInitListener.java
+++ b/quickstep/src/com/android/launcher3/LauncherInitListener.java
@@ -15,11 +15,11 @@
  */
 package com.android.launcher3;
 
-import com.android.quickstep.util.ActivityInitListener;
+import com.android.quickstep.util.ContextInitListener;
 
 import java.util.function.BiPredicate;
 
-public class LauncherInitListener extends ActivityInitListener<Launcher> {
+public class LauncherInitListener extends ContextInitListener<Launcher> {
 
     /**
      * @param onInitListener a callback made when the activity is initialized. The callback should
@@ -31,8 +31,8 @@
     }
 
     @Override
-    public boolean handleInit(Launcher launcher, boolean alreadyOnHome) {
+    public boolean handleInit(Launcher launcher, boolean isHomeStarted) {
         launcher.deferOverlayCallbacksUntilNextResumeOrStop();
-        return super.handleInit(launcher, alreadyOnHome);
+        return super.handleInit(launcher, isHomeStarted);
     }
 }
diff --git a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
index a64936d..18337d3 100644
--- a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
+++ b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
@@ -116,6 +116,7 @@
 import androidx.annotation.Nullable;
 import androidx.core.graphics.ColorUtils;
 
+import com.android.app.animation.Animations;
 import com.android.internal.jank.Cuj;
 import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
 import com.android.launcher3.LauncherAnimationRunner.RemoteAnimationFactory;
@@ -574,23 +575,45 @@
         } else {
             List<View> viewsToAnimate = new ArrayList<>();
             Workspace<?> workspace = mLauncher.getWorkspace();
-            workspace.forEachVisiblePage(
-                    view -> viewsToAnimate.add(((CellLayout) view).getShortcutsAndWidgets()));
+            if (Flags.coordinateWorkspaceScale()) {
+                viewsToAnimate.add(workspace);
+            } else {
+                workspace.forEachVisiblePage(
+                        view -> viewsToAnimate.add(((CellLayout) view).getShortcutsAndWidgets()));
+            }
 
+            Hotseat hotseat = mLauncher.getHotseat();
             // Do not scale hotseat as a whole when taskbar is present, and scale QSB only if it's
             // not inline.
             if (mDeviceProfile.isTaskbarPresent) {
                 if (!mDeviceProfile.isQsbInline) {
-                    viewsToAnimate.add(mLauncher.getHotseat().getQsb());
+                    viewsToAnimate.add(hotseat.getQsb());
                 }
             } else {
-                viewsToAnimate.add(mLauncher.getHotseat());
+                viewsToAnimate.add(hotseat);
             }
 
             viewsToAnimate.forEach(view -> {
                 view.setLayerType(View.LAYER_TYPE_HARDWARE, null);
 
-                ObjectAnimator scaleAnim = ObjectAnimator.ofFloat(view, SCALE_PROPERTY, scales)
+                float[] scale = scales;
+                if (Flags.coordinateWorkspaceScale()) {
+                    // Start the animation from the current value, instead of assuming the views are
+                    // in their resting state, so interrupted animations merge seamlessly.
+                    // TODO(b/367591368): ideally these animations would be refactored to be
+                    //  controlled centrally so each instances doesn't need to care about this
+                    //  coordination.
+                    scale = new float[]{view.getScaleX(), scales[1]};
+
+                    // Cancel any ongoing animations. This is necessary to avoid a conflict between
+                    // e.g. the unfinished animation triggered when closing an app back to Home and
+                    // this animation caused by a launch.
+                    Animations.Companion.cancelOngoingAnimation(view);
+                    // Make sure to cache the current animation, so it can be properly interrupted.
+                    Animations.Companion.setOngoingAnimation(view, launcherAnimator);
+                }
+
+                ObjectAnimator scaleAnim = ObjectAnimator.ofFloat(view, SCALE_PROPERTY, scale)
                         .setDuration(CONTENT_SCALE_DURATION);
                 scaleAnim.setInterpolator(DECELERATE_1_5);
                 launcherAnimator.play(scaleAnim);
@@ -600,6 +623,11 @@
                 viewsToAnimate.forEach(view -> {
                     SCALE_PROPERTY.set(view, 1f);
                     view.setLayerType(View.LAYER_TYPE_NONE, null);
+
+                    if (Flags.coordinateWorkspaceScale()) {
+                        // Reset the cached animation.
+                        Animations.Companion.setOngoingAnimation(view, null /* animation */);
+                    }
                 });
                 mLauncher.resumeExpensiveViewUpdates();
             };
@@ -1353,8 +1381,13 @@
                             ? null
                             : mLauncher.getTaskbarUIController().findMatchingView(launcherView),
                     true /* hideOriginal */, targetRect, false /* isOpening */);
-            isInHotseat = launcherView.getTag() instanceof ItemInfo
-                    && ((ItemInfo) launcherView.getTag()).isInHotseat();
+            if (launcherView.getTag() instanceof ItemInfo itemInfo) {
+                isInHotseat = itemInfo.isInHotseat();
+                if (isInHotseat) {
+                    int dx = mLauncher.getHotseatItemTranslationX(itemInfo);
+                    targetRect.offset(dx, 0);
+                }
+            }
         } else {
             targetRect.set(getDefaultWindowTargetRect());
         }
diff --git a/quickstep/src/com/android/launcher3/WidgetPickerActivity.java b/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
index 955388d..dc0f899 100644
--- a/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
+++ b/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
@@ -23,6 +23,8 @@
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 
+import static java.util.Collections.emptyList;
+
 import android.appwidget.AppWidgetManager;
 import android.appwidget.AppWidgetProviderInfo;
 import android.content.ClipData;
@@ -44,6 +46,7 @@
 import com.android.launcher3.model.StringCache;
 import com.android.launcher3.model.WidgetItem;
 import com.android.launcher3.model.WidgetPredictionsRequester;
+import com.android.launcher3.model.WidgetsFilterDataProvider;
 import com.android.launcher3.model.WidgetsModel;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.PackageItemInfo;
@@ -52,6 +55,7 @@
 import com.android.launcher3.widget.model.WidgetsListBaseEntry;
 import com.android.launcher3.widget.picker.WidgetsFullSheet;
 import com.android.launcher3.widget.picker.model.WidgetPickerDataProvider;
+import com.android.systemui.animation.back.FlingOnBackAnimationCallback;
 
 import java.util.ArrayList;
 import java.util.HashSet;
@@ -112,6 +116,7 @@
     private WidgetPredictionsRequester mWidgetPredictionsRequester;
     private final WidgetPickerDataProvider mWidgetPickerDataProvider =
             new WidgetPickerDataProvider();
+    private WidgetsFilterDataProvider mWidgetsFilterDataProvider;
 
     private int mDesiredWidgetWidth;
     private int mDesiredWidgetHeight;
@@ -133,13 +138,13 @@
     @Nullable
     private WidgetsFullSheet mWidgetSheet;
 
-    private final Predicate<WidgetItem> mWidgetsFilter = widget -> {
+    private final Predicate<WidgetItem> mNoShortcutsFilter = widget -> {
         final WidgetAcceptabilityVerdict verdict =
                 isWidgetAcceptable(widget, /* applySizeFilter=*/ false);
         verdict.maybeLogVerdict();
         return verdict.isAcceptable;
     };
-    private final Predicate<WidgetItem> mDefaultWidgetsFilter = widget -> {
+    private final Predicate<WidgetItem> mHostSizeAndNoShortcutsFilter = widget -> {
         final WidgetAcceptabilityVerdict verdict =
                 isWidgetAcceptable(widget, /* applySizeFilter=*/ true);
         verdict.maybeLogVerdict();
@@ -157,6 +162,7 @@
         InvariantDeviceProfile idp = mApp.getInvariantDeviceProfile();
         mDeviceProfile = idp.getDeviceProfile(this);
         mModel = new WidgetsModel();
+        mWidgetsFilterDataProvider = WidgetsFilterDataProvider.Companion.newInstance(this);
 
         setContentView(R.layout.widget_picker_activity);
         mDragLayer = findViewById(R.id.drag_layer);
@@ -288,13 +294,16 @@
     private void refreshAndBindWidgets() {
         MODEL_EXECUTOR.execute(() -> {
             LauncherAppState app = LauncherAppState.getInstance(this);
+            // Don't have to setup filters - its setup when launcher loads
+            // Just refresh filters with available cached info.
+            mModel.updateWidgetFilters(mWidgetsFilterDataProvider);
             mModel.update(app, null);
 
             StringCache stringCache = new StringCache();
             stringCache.loadStrings(this);
 
             bindStringCache(stringCache);
-            bindWidgets(mModel.getWidgetsByPackageItem());
+            bindWidgets(mModel.getWidgetsByPackageItem(), mModel.getDefaultWidgetsFilter());
             // Open sheet once widgets are available, so that it doesn't interrupt the open
             // animation.
             openWidgetsSheet();
@@ -310,14 +319,23 @@
         MAIN_EXECUTOR.execute(() -> mStringCache = stringCache);
     }
 
-    private void bindWidgets(Map<PackageItemInfo, List<WidgetItem>> widgets) {
+    private void bindWidgets(Map<PackageItemInfo, List<WidgetItem>> widgets,
+            @Nullable Predicate<WidgetItem> defaultWidgetsFilter) {
         WidgetsListBaseEntriesBuilder builder = new WidgetsListBaseEntriesBuilder(
                 mApp.getContext());
 
-        final List<WidgetsListBaseEntry> allWidgets = builder.build(widgets, mWidgetsFilter);
-        final List<WidgetsListBaseEntry> defaultWidgets =
-                shouldShowDefaultWidgets() ? builder.build(widgets,
-                        mDefaultWidgetsFilter) : List.of();
+        final List<WidgetsListBaseEntry> allWidgets = builder.build(widgets, mNoShortcutsFilter);
+
+        // Default list is shown if either defaultWidgetsFilter exists or host has additionally
+        // enforced size filtering.
+        @Nullable Predicate<WidgetItem> defaultListFilter =
+                hasHostSizeFilters() ? mHostSizeAndNoShortcutsFilter : null;
+        if (defaultWidgetsFilter != null) {
+            defaultListFilter = defaultListFilter != null ? defaultListFilter.and(
+                    defaultWidgetsFilter) : defaultWidgetsFilter;
+        }
+        final List<WidgetsListBaseEntry> defaultWidgets = defaultListFilter != null ? builder.build(
+                widgets, defaultListFilter) : emptyList();
 
         MAIN_EXECUTOR.execute(
                 () -> mWidgetPickerDataProvider.setWidgets(allWidgets, defaultWidgets));
@@ -342,6 +360,7 @@
     @Override
     protected void onDestroy() {
         super.onDestroy();
+        MODEL_EXECUTOR.execute(() -> mWidgetsFilterDataProvider.destroy());
         if (mWidgetPredictionsRequester != null) {
             mWidgetPredictionsRequester.clear();
         }
@@ -356,12 +375,12 @@
     /**
      * Animation callback for different predictive back animation states for the widget picker.
      */
-    private class BackAnimationCallback implements OnBackAnimationCallback {
+    private class BackAnimationCallback extends FlingOnBackAnimationCallback {
         @Nullable
         OnBackAnimationCallback mActiveOnBackAnimationCallback;
 
         @Override
-        public void onBackStarted(@NonNull BackEvent backEvent) {
+        public void onBackStartedCompat(@NonNull BackEvent backEvent) {
             if (mActiveOnBackAnimationCallback != null) {
                 mActiveOnBackAnimationCallback.onBackCancelled();
             }
@@ -372,7 +391,7 @@
         }
 
         @Override
-        public void onBackInvoked() {
+        public void onBackInvokedCompat() {
             if (mActiveOnBackAnimationCallback == null) {
                 return;
             }
@@ -381,7 +400,7 @@
         }
 
         @Override
-        public void onBackProgressed(@NonNull BackEvent backEvent) {
+        public void onBackProgressedCompat(@NonNull BackEvent backEvent) {
             if (mActiveOnBackAnimationCallback == null) {
                 return;
             }
@@ -389,7 +408,7 @@
         }
 
         @Override
-        public void onBackCancelled() {
+        public void onBackCancelledCompat() {
             if (mActiveOnBackAnimationCallback == null) {
                 return;
             }
@@ -398,7 +417,7 @@
         }
     }
 
-    private boolean shouldShowDefaultWidgets() {
+    private boolean hasHostSizeFilters() {
         // If optional filters such as size filter are present, we display them as default widgets.
         return mDesiredWidgetWidth != 0 || mDesiredWidgetHeight != 0;
     }
diff --git a/quickstep/src/com/android/launcher3/appprediction/AppsDividerView.java b/quickstep/src/com/android/launcher3/appprediction/AppsDividerView.java
index 7a8b58e..32fda48 100644
--- a/quickstep/src/com/android/launcher3/appprediction/AppsDividerView.java
+++ b/quickstep/src/com/android/launcher3/appprediction/AppsDividerView.java
@@ -31,6 +31,7 @@
 import android.view.accessibility.AccessibilityManager;
 
 import androidx.annotation.ColorInt;
+import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
@@ -253,4 +254,9 @@
     public View getFocusedChild() {
         return null;
     }
+
+    @VisibleForTesting
+    public DividerType getDividerType() {
+        return mDividerType;
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java b/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java
index 92d9516..8e80aa5 100644
--- a/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java
+++ b/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java
@@ -57,7 +57,7 @@
     // Vertical padding of the icon that contributes to the expected cell height.
     private final int mVerticalPadding;
     // Extra padding that is used in the top app rows (prediction and search) that is not used in
-    // the regular A-Z list. This only applies to single line label.
+    // the regular A-Z list.
     private final int mTopRowExtraHeight;
 
     // Helper to drawing the focus indicator.
@@ -140,7 +140,7 @@
         // is not enabled. Otherwise, the extra height will increase by just the textHeight.
         int extraHeight = (Flags.enableTwolineToggle() &&
                 LauncherPrefs.ENABLE_TWOLINE_ALLAPPS_TOGGLE.get(getContext()))
-                ? textHeight : mTopRowExtraHeight;
+                ? (textHeight + mTopRowExtraHeight) : mTopRowExtraHeight;
         totalHeight += extraHeight;
         return getVisibility() == GONE ? 0 : totalHeight + getPaddingTop() + getPaddingBottom();
     }
diff --git a/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransition.kt b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransition.kt
new file mode 100644
index 0000000..87a82f0
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransition.kt
@@ -0,0 +1,165 @@
+/*
+ * 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.desktop
+
+import android.animation.Animator
+import android.animation.AnimatorSet
+import android.animation.ValueAnimator
+import android.content.Context
+import android.graphics.Rect
+import android.os.IBinder
+import android.view.SurfaceControl.Transaction
+import android.view.WindowManager.TRANSIT_OPEN
+import android.view.WindowManager.TRANSIT_TO_BACK
+import android.view.WindowManager.TRANSIT_TO_FRONT
+import android.window.IRemoteTransitionFinishedCallback
+import android.window.RemoteTransitionStub
+import android.window.TransitionInfo
+import android.window.TransitionInfo.Change
+import androidx.core.animation.addListener
+import com.android.app.animation.Interpolators
+import com.android.internal.policy.ScreenDecorationsUtils
+import com.android.quickstep.RemoteRunnable
+import com.android.wm.shell.shared.animation.MinimizeAnimator
+import com.android.wm.shell.shared.animation.WindowAnimator
+import java.util.concurrent.Executor
+
+/**
+ * [android.window.RemoteTransition] for Desktop app launches.
+ *
+ * This transition supports minimize-changes, i.e. in a launch-transition, if a window is moved back
+ * ([android.view.WindowManager.TRANSIT_TO_BACK]) this transition will apply a minimize animation to
+ * that window.
+ */
+class DesktopAppLaunchTransition(
+    private val context: Context,
+    private val mainExecutor: Executor,
+    private val launchType: AppLaunchType,
+) : RemoteTransitionStub() {
+
+    enum class AppLaunchType(
+        val boundsAnimationParams: WindowAnimator.BoundsAnimationParams,
+        val alphaDurationMs: Long,
+    ) {
+        LAUNCH(launchBoundsAnimationDef, /* alphaDurationMs= */ 200L),
+        UNMINIMIZE(unminimizeBoundsAnimationDef, /* alphaDurationMs= */ 100L),
+    }
+
+    override fun startAnimation(
+        token: IBinder,
+        info: TransitionInfo,
+        t: Transaction,
+        transitionFinishedCallback: IRemoteTransitionFinishedCallback,
+    ) {
+        val safeTransitionFinishedCallback = RemoteRunnable {
+            transitionFinishedCallback.onTransitionFinished(/* wct= */ null, /* sct= */ null)
+        }
+        mainExecutor.execute {
+            runAnimators(info, safeTransitionFinishedCallback)
+            t.apply()
+        }
+    }
+
+    private fun runAnimators(info: TransitionInfo, finishedCallback: RemoteRunnable) {
+        val animators = mutableListOf<Animator>()
+        val animatorFinishedCallback: (Animator) -> Unit = { animator ->
+            animators -= animator
+            if (animators.isEmpty()) finishedCallback.run()
+        }
+        animators += createAnimators(info, animatorFinishedCallback)
+        animators.forEach { it.start() }
+    }
+
+    private fun createAnimators(
+        info: TransitionInfo,
+        finishCallback: (Animator) -> Unit,
+    ): List<Animator> {
+        val transaction = Transaction()
+        val launchAnimator =
+            createLaunchAnimator(getLaunchChange(info), transaction, finishCallback)
+        val minimizeChange = getMinimizeChange(info) ?: return listOf(launchAnimator)
+        val minimizeAnimator =
+            MinimizeAnimator.create(
+                context.resources.displayMetrics,
+                minimizeChange,
+                transaction,
+                finishCallback,
+            )
+        return listOf(launchAnimator, minimizeAnimator)
+    }
+
+    private fun getLaunchChange(info: TransitionInfo): Change =
+        requireNotNull(info.changes.firstOrNull { change -> change.mode in LAUNCH_CHANGE_MODES }) {
+            "expected an app launch Change"
+        }
+
+    private fun getMinimizeChange(info: TransitionInfo): Change? =
+        info.changes.firstOrNull { change -> change.mode == TRANSIT_TO_BACK }
+
+    private fun createLaunchAnimator(
+        change: Change,
+        transaction: Transaction,
+        onAnimFinish: (Animator) -> Unit,
+    ): Animator {
+        val boundsAnimator =
+            WindowAnimator.createBoundsAnimator(
+                context.resources.displayMetrics,
+                launchType.boundsAnimationParams,
+                change,
+                transaction,
+            )
+        val alphaAnimator =
+            ValueAnimator.ofFloat(0f, 1f).apply {
+                duration = launchType.alphaDurationMs
+                interpolator = Interpolators.LINEAR
+                addUpdateListener { animation ->
+                    transaction.setAlpha(change.leash, animation.animatedValue as Float).apply()
+                }
+            }
+        val clipRect = Rect(change.endAbsBounds).apply { offsetTo(0, 0) }
+        transaction.setCrop(change.leash, clipRect)
+        transaction.setCornerRadius(
+            change.leash,
+            ScreenDecorationsUtils.getWindowCornerRadius(context),
+        )
+        return AnimatorSet().apply {
+            playTogether(boundsAnimator, alphaAnimator)
+            addListener(onEnd = { animation -> onAnimFinish(animation) })
+        }
+    }
+
+    companion object {
+        /** Change modes that represent a task becoming visible / launching in Desktop mode. */
+        val LAUNCH_CHANGE_MODES = intArrayOf(TRANSIT_OPEN, TRANSIT_TO_FRONT)
+
+        private val launchBoundsAnimationDef =
+            WindowAnimator.BoundsAnimationParams(
+                durationMs = 600,
+                startOffsetYDp = 36f,
+                startScale = 0.95f,
+                interpolator = Interpolators.STANDARD_DECELERATE,
+            )
+
+        private val unminimizeBoundsAnimationDef =
+            WindowAnimator.BoundsAnimationParams(
+                durationMs = 300,
+                startOffsetYDp = 12f,
+                startScale = 0.97f,
+                interpolator = Interpolators.STANDARD_DECELERATE,
+            )
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransitionManager.kt b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransitionManager.kt
new file mode 100644
index 0000000..e32bcd1
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransitionManager.kt
@@ -0,0 +1,88 @@
+/*
+ * 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.desktop
+
+import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
+import android.content.Context
+import android.window.DesktopModeFlags
+import android.window.RemoteTransition
+import android.window.TransitionFilter
+import android.window.TransitionFilter.CONTAINER_ORDER_TOP
+import com.android.launcher3.desktop.DesktopAppLaunchTransition.AppLaunchType
+import com.android.launcher3.util.Executors.MAIN_EXECUTOR
+import com.android.quickstep.SystemUiProxy
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
+
+/** Manages transitions related to app launches in Desktop Mode. */
+class DesktopAppLaunchTransitionManager(
+    private val context: Context,
+    private val systemUiProxy: SystemUiProxy,
+) {
+    private var remoteWindowLimitUnminimizeTransition: RemoteTransition? = null
+
+    /**
+     * Register a [RemoteTransition] supporting Desktop app launches, and window limit
+     * minimizations.
+     */
+    fun registerTransitions() {
+        if (!shouldRegisterTransitions()) {
+            return
+        }
+        remoteWindowLimitUnminimizeTransition =
+            RemoteTransition(
+                DesktopAppLaunchTransition(context, MAIN_EXECUTOR, AppLaunchType.UNMINIMIZE)
+            )
+        systemUiProxy.registerRemoteTransition(
+            remoteWindowLimitUnminimizeTransition,
+            buildAppLaunchFilter(),
+        )
+    }
+
+    /**
+     * Unregister the [RemoteTransition] supporting Desktop app launches and window limit
+     * minimizations.
+     */
+    fun unregisterTransitions() {
+        if (!shouldRegisterTransitions()) {
+            return
+        }
+        systemUiProxy.unregisterRemoteTransition(remoteWindowLimitUnminimizeTransition)
+        remoteWindowLimitUnminimizeTransition = null
+    }
+
+    private fun shouldRegisterTransitions(): Boolean =
+        DesktopModeStatus.canEnterDesktopMode(context) &&
+            DesktopModeFlags.ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS.isTrue
+
+    companion object {
+        private fun buildAppLaunchFilter(): TransitionFilter {
+            val openRequirement =
+                TransitionFilter.Requirement().apply {
+                    mActivityType = ACTIVITY_TYPE_STANDARD
+                    mWindowingMode = WINDOWING_MODE_FREEFORM
+                    mModes = DesktopAppLaunchTransition.LAUNCH_CHANGE_MODES
+                    mMustBeTask = true
+                    mOrder = CONTAINER_ORDER_TOP
+                }
+            return TransitionFilter().apply {
+                mTypeSet = DesktopAppLaunchTransition.LAUNCH_CHANGE_MODES
+                mRequirements = arrayOf(openRequirement)
+            }
+        }
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/desktop/DesktopRecentsTransitionController.kt b/quickstep/src/com/android/launcher3/desktop/DesktopRecentsTransitionController.kt
index f7da34a..ac1ffa6 100644
--- a/quickstep/src/com/android/launcher3/desktop/DesktopRecentsTransitionController.kt
+++ b/quickstep/src/com/android/launcher3/desktop/DesktopRecentsTransitionController.kt
@@ -20,6 +20,7 @@
 import android.os.RemoteException
 import android.util.Log
 import android.view.SurfaceControl
+import android.view.WindowManager.TRANSIT_TO_FRONT
 import android.window.IRemoteTransitionFinishedCallback
 import android.window.RemoteTransition
 import android.window.RemoteTransitionStub
@@ -30,6 +31,7 @@
 import com.android.quickstep.SystemUiProxy
 import com.android.quickstep.TaskViewUtils
 import com.android.quickstep.views.DesktopTaskView
+import com.android.window.flags.Flags
 import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource
 import java.util.function.Consumer
 
@@ -38,14 +40,14 @@
     private val stateManager: StateManager<*, *>,
     private val systemUiProxy: SystemUiProxy,
     private val appThread: IApplicationThread,
-    private val depthController: DepthController?
+    private val depthController: DepthController?,
 ) {
 
     /** Launch desktop tasks from recents view */
     fun launchDesktopFromRecents(
         desktopTaskView: DesktopTaskView,
         animated: Boolean,
-        callback: Consumer<Boolean>? = null
+        callback: Consumer<Boolean>? = null,
     ) {
         val animRunner =
             RemoteDesktopLaunchTransitionRunner(
@@ -53,7 +55,7 @@
                 animated,
                 stateManager,
                 depthController,
-                callback
+                callback,
             )
         val transition = RemoteTransition(animRunner, appThread, "RecentsToDesktop")
         systemUiProxy.showDesktopApps(desktopTaskView.display.displayId, transition)
@@ -64,19 +66,24 @@
         systemUiProxy.moveToDesktop(taskId, transitionSource)
     }
 
+    /** Move task to external display from recents view */
+    fun moveToExternalDisplay(taskId: Int) {
+        systemUiProxy.moveToExternalDisplay(taskId)
+    }
+
     private class RemoteDesktopLaunchTransitionRunner(
         private val desktopTaskView: DesktopTaskView,
         private val animated: Boolean,
         private val stateManager: StateManager<*, *>,
         private val depthController: DepthController?,
-        private val successCallback: Consumer<Boolean>?
+        private val successCallback: Consumer<Boolean>?,
     ) : RemoteTransitionStub() {
 
         override fun startAnimation(
             token: IBinder,
             info: TransitionInfo,
             t: SurfaceControl.Transaction,
-            finishCallback: IRemoteTransitionFinishedCallback
+            finishCallback: IRemoteTransitionFinishedCallback,
         ) {
             val errorHandlingFinishCallback = Runnable {
                 try {
@@ -86,6 +93,9 @@
                 }
             }
 
+            if (Flags.enableDesktopWindowingPersistence()) {
+                handleAnimationAfterReboot(info)
+            }
             MAIN_EXECUTOR.execute {
                 val animator =
                     TaskViewUtils.composeRecentsDesktopLaunchAnimator(
@@ -93,7 +103,7 @@
                         stateManager,
                         depthController,
                         info,
-                        t
+                        t,
                     ) {
                         errorHandlingFinishCallback.run()
                         successCallback?.accept(true)
@@ -104,6 +114,26 @@
                 animator.start()
             }
         }
+
+        /**
+         * Upon reboot the start bounds of a task is set to fullscreen with the recents transition.
+         * Check this case and set the start bounds to the end bounds so that the window doesn't
+         * jump from start bounds to end bounds during the animation. Tasks in desktop cannot
+         * normally have top bound as 0 due to status bar so this is a good indicator to identify
+         * reboot case.
+         */
+        private fun handleAnimationAfterReboot(info: TransitionInfo) {
+            info.changes.forEach { change ->
+                if (
+                    change.mode == TRANSIT_TO_FRONT &&
+                        change.taskInfo?.isFreeform == true &&
+                        change.startAbsBounds.top == 0 &&
+                        change.startAbsBounds.left == 0
+                ) {
+                    change.setStartAbsBounds(change.endAbsBounds)
+                }
+            }
+        }
     }
 
     companion object {
diff --git a/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java b/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java
index 6af5a30..2f4c6f6 100644
--- a/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java
+++ b/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java
@@ -59,7 +59,6 @@
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherPrefs;
-import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.logger.LauncherAtom;
 import com.android.launcher3.logging.InstanceId;
 import com.android.launcher3.logging.InstanceIdSequence;
@@ -77,6 +76,7 @@
 import com.android.launcher3.util.PersistedItemArray;
 import com.android.quickstep.logging.SettingsChangeLogger;
 import com.android.quickstep.logging.StatsLogCompatManager;
+import com.android.quickstep.util.ContextualSearchStateManager;
 import com.android.systemui.shared.system.SysUiStatsLog;
 
 import java.util.ArrayList;
@@ -155,9 +155,6 @@
                         state.containerId);
         FixedContainerItems fci = new FixedContainerItems(state.containerId,
                 state.storage.read(mApp.getContext(), factory, ums.allUsers::get));
-        if (FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) {
-            bindPredictionItems(callbacks, fci);
-        }
         mDataModel.extraItems.put(state.containerId, fci);
     }
 
@@ -209,6 +206,8 @@
     @Override
     public void workspaceLoadComplete() {
         super.workspaceLoadComplete();
+        // Initialize ContextualSearchStateManager.
+        ContextualSearchStateManager.INSTANCE.get(mContext);
         recreatePredictors();
     }
 
@@ -237,7 +236,7 @@
             InstanceId instanceId = new InstanceIdSequence().newInstanceId();
             for (ItemInfo info : itemsIdMap) {
                 CollectionInfo parent = getContainer(info, itemsIdMap);
-                StatsLogCompatManager.writeSnapshot(info.buildProto(parent), instanceId);
+                StatsLogCompatManager.writeSnapshot(info.buildProto(parent, mContext), instanceId);
             }
             additionalSnapshotEvents(instanceId);
             prefs.put(LAST_SNAPSHOT_TIME_MILLIS, now);
@@ -274,7 +273,7 @@
 
                         for (ItemInfo info : itemsIdMap) {
                             CollectionInfo parent = getContainer(info, itemsIdMap);
-                            LauncherAtom.ItemInfo itemInfo = info.buildProto(parent);
+                            LauncherAtom.ItemInfo itemInfo = info.buildProto(parent, mContext);
                             Log.d(TAG, itemInfo.toString());
                             StatsEvent statsEvent = StatsLogCompatManager.buildStatsEvent(itemInfo,
                                     instanceId);
diff --git a/quickstep/src/com/android/launcher3/model/WellbeingModel.java b/quickstep/src/com/android/launcher3/model/WellbeingModel.java
index fb17f15..0f3aaa6 100644
--- a/quickstep/src/com/android/launcher3/model/WellbeingModel.java
+++ b/quickstep/src/com/android/launcher3/model/WellbeingModel.java
@@ -44,23 +44,30 @@
 import androidx.annotation.WorkerThread;
 
 import com.android.launcher3.R;
+import com.android.launcher3.dagger.ApplicationContext;
+import com.android.launcher3.dagger.LauncherAppSingleton;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.popup.RemoteActionShortcut;
 import com.android.launcher3.popup.SystemShortcut;
+import com.android.launcher3.util.DaggerSingletonObject;
+import com.android.launcher3.util.DaggerSingletonTracker;
 import com.android.launcher3.util.Executors;
-import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.util.Preconditions;
 import com.android.launcher3.util.SafeCloseable;
 import com.android.launcher3.util.SimpleBroadcastReceiver;
 import com.android.launcher3.views.ActivityContext;
+import com.android.quickstep.dagger.QuickstepBaseAppComponent;
 
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.Map;
 
+import javax.inject.Inject;
+
 /**
  * Data model for digital wellbeing status of apps.
  */
+@LauncherAppSingleton
 public final class WellbeingModel implements SafeCloseable {
     private static final String TAG = "WellbeingModel";
     private static final int[] RETRY_TIMES_MS = {5000, 15000, 30000};
@@ -75,8 +82,8 @@
     private static final String EXTRA_PACKAGES = "packages";
     private static final String EXTRA_SUCCESS = "success";
 
-    public static final MainThreadInitializedObject<WellbeingModel> INSTANCE =
-            new MainThreadInitializedObject<>(WellbeingModel::new);
+    public static final DaggerSingletonObject<WellbeingModel> INSTANCE =
+            new DaggerSingletonObject<>(QuickstepBaseAppComponent::getWellbeingModel);
 
     private final Context mContext;
     private final String mWellbeingProviderPkg;
@@ -93,7 +100,9 @@
 
     private boolean mIsInTest;
 
-    private WellbeingModel(final Context context) {
+    @Inject
+    WellbeingModel(@ApplicationContext final Context context,
+            DaggerSingletonTracker tracker) {
         mContext = context;
         mWellbeingProviderPkg = mContext.getString(R.string.wellbeing_provider_pkg);
         mWorkerHandler = new Handler(TextUtils.isEmpty(mWellbeingProviderPkg)
@@ -112,6 +121,7 @@
             }
         };
         mWorkerHandler.post(this::initializeInBackground);
+        tracker.addCloseable(this);
     }
 
     @WorkerThread
diff --git a/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java b/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java
index 0395d32..9d9054e 100644
--- a/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java
+++ b/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java
@@ -16,8 +16,12 @@
 package com.android.launcher3.model;
 
 import static com.android.launcher3.Flags.enableCategorizedWidgetSuggestions;
+import static com.android.launcher3.Flags.enableTieredWidgetsByDefaultInPicker;
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION;
 
+import static java.util.stream.Collectors.groupingBy;
+import static java.util.stream.Collectors.toMap;
+
 import android.app.prediction.AppTarget;
 import android.content.ComponentName;
 import android.content.Context;
@@ -26,6 +30,7 @@
 import androidx.annotation.NonNull;
 
 import com.android.launcher3.LauncherModel.ModelUpdateTask;
+import com.android.launcher3.R;
 import com.android.launcher3.model.BgDataModel.FixedContainerItems;
 import com.android.launcher3.model.QuickstepModelDelegate.PredictorState;
 import com.android.launcher3.model.data.ItemInfo;
@@ -34,8 +39,10 @@
 import com.android.launcher3.widget.picker.WidgetRecommendationCategoryProvider;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Random;
 import java.util.Set;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
@@ -60,33 +67,72 @@
     @Override
     public void execute(@NonNull ModelTaskController taskController, @NonNull BgDataModel dataModel,
             @NonNull AllAppsList apps) {
+        Predicate<WidgetItem> predictedWidgetsFilter = enableTieredWidgetsByDefaultInPicker()
+                ? dataModel.widgetsModel.getPredictedWidgetsFilter() : null;
         Set<ComponentKey> widgetsInWorkspace = dataModel.appWidgets.stream().map(
                 widget -> new ComponentKey(widget.providerName, widget.user)).collect(
                 Collectors.toSet());
-        Predicate<WidgetItem> notOnWorkspace = w -> !widgetsInWorkspace.contains(w);
-        Map<ComponentKey, WidgetItem> allWidgets =
-                dataModel.widgetsModel.getWidgetsByComponentKey();
+
+        // Widgets (excluding shortcuts & already added widgets) that belong to apps eligible for
+        // being in predictions.
+        Map<ComponentKey, WidgetItem> allEligibleWidgets =
+                dataModel.widgetsModel.getWidgetsByComponentKey()
+                        .entrySet()
+                        .stream()
+                        .filter(entry -> entry.getValue().widgetInfo != null
+                                && !widgetsInWorkspace.contains(entry.getValue())
+                                && (predictedWidgetsFilter == null
+                                || predictedWidgetsFilter.test(entry.getValue()))
+                        ).collect(toMap(Map.Entry::getKey, Map.Entry::getValue));
+
+        Context context = taskController.getApp().getContext();
 
         List<WidgetItem> servicePredictedItems = new ArrayList<>();
+        List<String> addedWidgetApps = new ArrayList<>();
 
         for (AppTarget app : mTargets) {
             ComponentKey componentKey = new ComponentKey(
                     new ComponentName(app.getPackageName(), app.getClassName()), app.getUser());
-            WidgetItem widget = allWidgets.get(componentKey);
-            if (widget == null) {
+            WidgetItem widget = allEligibleWidgets.get(componentKey);
+            if (widget == null) { // widget not eligible.
                 continue;
             }
             String className = app.getClassName();
             if (!TextUtils.isEmpty(className)) {
-                if (notOnWorkspace.test(widget)) {
-                    servicePredictedItems.add(widget);
-                }
+                servicePredictedItems.add(widget);
+                addedWidgetApps.add(componentKey.componentName.getPackageName());
+            }
+        }
+
+        int minPredictionCount = context.getResources().getInteger(
+                R.integer.widget_predictions_min_count);
+        if (enableTieredWidgetsByDefaultInPicker()
+                && servicePredictedItems.size() < minPredictionCount) {
+            // Eligible apps that aren't already part of predictions.
+            Map<String, List<WidgetItem>> eligibleWidgetsByApp =
+                    allEligibleWidgets.values().stream()
+                            .filter(w -> !addedWidgetApps.contains(
+                                    w.componentName.getPackageName()))
+                            .collect(groupingBy(w -> w.componentName.getPackageName()));
+
+            // Randomize available apps list
+            List<String> appPackages = new ArrayList<>(eligibleWidgetsByApp.keySet());
+            Collections.shuffle(appPackages);
+
+            int widgetsToAdd = minPredictionCount - servicePredictedItems.size();
+            for (String appPackage : appPackages) {
+                if (widgetsToAdd <= 0) break;
+
+                List<WidgetItem> widgetsForApp = eligibleWidgetsByApp.get(appPackage);
+                int index = new Random().nextInt(widgetsForApp.size());
+                // Add a random widget from the app.
+                servicePredictedItems.add(widgetsForApp.get(index));
+                widgetsToAdd--;
             }
         }
 
         List<ItemInfo> items;
         if (enableCategorizedWidgetSuggestions()) {
-            Context context = taskController.getApp().getContext();
             WidgetRecommendationCategoryProvider categoryProvider =
                     WidgetRecommendationCategoryProvider.newInstance(context);
             items = servicePredictedItems.stream()
diff --git a/quickstep/src/com/android/launcher3/proxy/ProxyActivityStarter.java b/quickstep/src/com/android/launcher3/proxy/ProxyActivityStarter.java
index 212a5ff..4293ccd 100644
--- a/quickstep/src/com/android/launcher3/proxy/ProxyActivityStarter.java
+++ b/quickstep/src/com/android/launcher3/proxy/ProxyActivityStarter.java
@@ -61,6 +61,7 @@
             }
         } catch (NullPointerException | ActivityNotFoundException | SecurityException
                 | SendIntentException e) {
+            Log.w(TAG, "Proxy activity starter could not start activity: ", e);
             mParams.deliverResult(this, RESULT_CANCELED, null);
         }
         finishAndRemoveTask();
diff --git a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java
index 3dcb2ac..5744464 100644
--- a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java
+++ b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java
@@ -16,7 +16,7 @@
 package com.android.launcher3.statehandlers;
 
 import static android.view.View.VISIBLE;
-import static android.window.flags.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY;
+import static android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY;
 
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 
@@ -384,7 +384,7 @@
             Log.d(TAG, "markLauncherPaused " + Debug.getCaller());
         }
         StatefulActivity<LauncherState> activity =
-                QuickstepLauncher.ACTIVITY_TRACKER.getCreatedActivity();
+                QuickstepLauncher.ACTIVITY_TRACKER.getCreatedContext();
         if (activity != null) {
             activity.setPaused();
         }
@@ -404,7 +404,7 @@
             Log.d(TAG, "markLauncherResumed " + Debug.getCaller());
         }
         StatefulActivity<LauncherState> activity =
-                QuickstepLauncher.ACTIVITY_TRACKER.getCreatedActivity();
+                QuickstepLauncher.ACTIVITY_TRACKER.getCreatedContext();
         // Check activity state before calling setResumed(). Launcher may have been actually
         // paused (eg fullscreen task moved to front).
         // In this case we should not mark the activity as resumed.
diff --git a/quickstep/src/com/android/launcher3/taskbar/BarsLocationAnimatorHelper.kt b/quickstep/src/com/android/launcher3/taskbar/BarsLocationAnimatorHelper.kt
new file mode 100644
index 0000000..b8060e1
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/BarsLocationAnimatorHelper.kt
@@ -0,0 +1,178 @@
+/*
+ * 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.animation.Animator
+import android.animation.AnimatorSet
+import android.animation.ObjectAnimator
+import android.animation.ValueAnimator
+import android.view.View
+import androidx.dynamicanimation.animation.SpringForce
+import com.android.app.animation.Interpolators
+import com.android.launcher3.LauncherAnimUtils
+import com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X
+import com.android.launcher3.anim.SpringAnimationBuilder
+import com.android.wm.shell.shared.bubbles.BubbleBarLocation
+
+/** Animator helper that creates bars animators. */
+object BarsLocationAnimatorHelper {
+
+    private const val FADE_OUT_ANIM_ALPHA_DURATION_MS: Long = 50L
+    private const val FADE_OUT_ANIM_ALPHA_DELAY_MS: Long = 50L
+    private const val FADE_OUT_ANIM_POSITION_DURATION_MS: Long = 100L
+    private const val FADE_IN_ANIM_ALPHA_DURATION_MS: Long = 100L
+
+    // Use STIFFNESS_MEDIUMLOW which is not defined in the API constants
+    private const val FADE_IN_ANIM_POSITION_SPRING_STIFFNESS: Float = 400f
+
+    // During fade out animation we shift the bubble bar 1/80th of the screen width
+    private const val FADE_OUT_ANIM_POSITION_SHIFT: Float = 1 / 80f
+
+    // During fade in animation we shift the bubble bar 1/60th of the screen width
+    private const val FADE_IN_ANIM_POSITION_SHIFT: Float = 1 / 60f
+
+    private val View.screenWidth: Int
+        get() = resources.displayMetrics.widthPixels
+
+    private val View.outShift: Float
+        get() = screenWidth * FADE_OUT_ANIM_POSITION_SHIFT
+
+    private val View.inShiftX: Float
+        get() = screenWidth * FADE_IN_ANIM_POSITION_SHIFT
+
+    /**
+     * Creates out animation for targetView that animates it finalTx and plays targetViewAlphaAnim
+     * to its final value.
+     */
+    private fun createLocationOutAnimator(
+        finalTx: Float,
+        targetViewAlphaAnim: ObjectAnimator,
+        targetView: View,
+    ): Animator {
+        val positionAnim =
+            ObjectAnimator.ofFloat(targetView, VIEW_TRANSLATE_X, finalTx)
+                .setDuration(FADE_OUT_ANIM_POSITION_DURATION_MS)
+        positionAnim.interpolator = Interpolators.EMPHASIZED_ACCELERATE
+
+        targetViewAlphaAnim.setDuration(FADE_OUT_ANIM_ALPHA_DURATION_MS)
+        targetViewAlphaAnim.startDelay = FADE_OUT_ANIM_ALPHA_DELAY_MS
+
+        val animatorSet = AnimatorSet()
+        animatorSet.playTogether(positionAnim, targetViewAlphaAnim)
+        return animatorSet
+    }
+
+    /**
+     * Creates in animation for targetView that animates it from startTx to finalTx and plays
+     * targetViewAlphaAnim to its final value.
+     */
+    private fun createLocationInAnimator(
+        startTx: Float,
+        finalTx: Float,
+        targetViewAlphaAnim: ObjectAnimator,
+        targetView: View,
+    ): Animator {
+        targetViewAlphaAnim.setDuration(FADE_IN_ANIM_ALPHA_DURATION_MS)
+        val positionAnim: ValueAnimator =
+            SpringAnimationBuilder(targetView.context)
+                .setStartValue(startTx)
+                .setEndValue(finalTx)
+                .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
+                .setStiffness(FADE_IN_ANIM_POSITION_SPRING_STIFFNESS)
+                .build(targetView, VIEW_TRANSLATE_X)
+        val animatorSet = AnimatorSet()
+        animatorSet.playTogether(positionAnim, targetViewAlphaAnim)
+        return animatorSet
+    }
+
+    /** Creates an animator for the bubble bar view in part. */
+    @JvmStatic
+    fun getBubbleBarLocationInAnimator(
+        newLocation: BubbleBarLocation,
+        currentLocation: BubbleBarLocation,
+        distanceFromOtherSide: Float,
+        targetViewAlphaAnim: ObjectAnimator,
+        bubbleBarView: View,
+    ): Animator {
+        val shift: Float = bubbleBarView.outShift
+
+        val onLeft = newLocation.isOnLeft(bubbleBarView.isLayoutRtl)
+        val startTx: Float
+        val finalTx =
+            if (newLocation == currentLocation) {
+                // Animated location matches layout location.
+                0f
+            } else {
+                // We are animating in to a transient location, need to move the bar
+                // accordingly.
+                distanceFromOtherSide * (if (onLeft) -1 else 1)
+            }
+        startTx =
+            if (onLeft) {
+                // Bar will be shown on the left side. Start point is shifted right.
+                finalTx + shift
+            } else {
+                // Bar will be shown on the right side. Start point is shifted left.
+                finalTx - shift
+            }
+        return createLocationInAnimator(startTx, finalTx, targetViewAlphaAnim, bubbleBarView)
+    }
+
+    /** Creates an animator for the bubble bar view out part. */
+    @JvmStatic
+    fun getBubbleBarLocationOutAnimator(
+        bubbleBarView: View,
+        bubbleBarLocation: BubbleBarLocation,
+        targetViewAlphaAnim: ObjectAnimator,
+    ): Animator {
+        val onLeft = bubbleBarLocation.isOnLeft(bubbleBarView.isLayoutRtl)
+        val shift = bubbleBarView.outShift
+        val finalTx = bubbleBarView.translationX + (if (onLeft) -shift else shift)
+        return this.createLocationOutAnimator(finalTx, targetViewAlphaAnim, bubbleBarView)
+    }
+
+    /** Creates a teleport animator for the navigation buttons view. */
+    @JvmStatic
+    fun getTeleportAnimatorForNavButtons(
+        location: BubbleBarLocation,
+        navButtonsView: View,
+        navBarTargetTranslationX: Float,
+    ): Animator {
+        val outShift: Float = navButtonsView.outShift
+        val isNavBarOnRight: Boolean = location.isOnLeft(navButtonsView.isLayoutRtl)
+        val finalOutTx =
+            navButtonsView.translationX + (if (isNavBarOnRight) outShift else -outShift)
+        val fadeout: Animator =
+            createLocationOutAnimator(
+                finalOutTx,
+                ObjectAnimator.ofFloat(navButtonsView, LauncherAnimUtils.VIEW_ALPHA, 0f),
+                navButtonsView,
+            )
+        val inShift: Float = navButtonsView.inShiftX
+        val inStartX = navBarTargetTranslationX + (if (isNavBarOnRight) -inShift else inShift)
+        val fadeIn: Animator =
+            createLocationInAnimator(
+                inStartX,
+                navBarTargetTranslationX,
+                ObjectAnimator.ofFloat(navButtonsView, LauncherAnimUtils.VIEW_ALPHA, 1f),
+                navButtonsView,
+            )
+        val teleportAnimator = AnimatorSet()
+        teleportAnimator.play(fadeout).before(fadeIn)
+        return teleportAnimator
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/FallbackTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/FallbackTaskbarUIController.java
index 929e793..6a908ca 100644
--- a/quickstep/src/com/android/launcher3/taskbar/FallbackTaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/FallbackTaskbarUIController.java
@@ -25,20 +25,24 @@
 
 import com.android.launcher3.popup.SystemShortcut;
 import com.android.launcher3.statemanager.StateManager;
-import com.android.quickstep.RecentsActivity;
+import com.android.launcher3.statemanager.StatefulContainer;
 import com.android.quickstep.TopTaskTracker;
 import com.android.quickstep.fallback.RecentsState;
 import com.android.quickstep.util.TISBindHelper;
 import com.android.quickstep.views.RecentsView;
+import com.android.quickstep.views.RecentsViewContainer;
 
 import java.util.stream.Stream;
 
 /**
  * A data source which integrates with the fallback RecentsActivity instance (for 3P launchers).
+ * @param <T> The type of the RecentsViewContainer that will handle Recents state changes.
  */
-public class FallbackTaskbarUIController extends TaskbarUIController {
+public class FallbackTaskbarUIController
+        <T extends RecentsViewContainer & StatefulContainer<RecentsState>>
+        extends TaskbarUIController {
 
-    private final RecentsActivity mRecentsActivity;
+    private final T mRecentsContainer;
 
     private final StateManager.StateListener<RecentsState> mStateListener =
             new StateManager.StateListener<RecentsState>() {
@@ -46,8 +50,12 @@
                 public void onStateTransitionStart(RecentsState toState) {
                     animateToRecentsState(toState);
 
+                    RecentsView recentsView = getRecentsView();
+                    if (recentsView == null) {
+                        return;
+                    }
                     // Handle tapping on live tile.
-                    getRecentsView().setTaskLaunchListener(toState == RecentsState.DEFAULT
+                    recentsView.setTaskLaunchListener(toState == RecentsState.DEFAULT
                             ? (() -> animateToRecentsState(RecentsState.BACKGROUND_APP)) : null);
                 }
 
@@ -63,23 +71,26 @@
                 }
             };
 
-    public FallbackTaskbarUIController(RecentsActivity recentsActivity) {
-        mRecentsActivity = recentsActivity;
+    public FallbackTaskbarUIController(T recentsContainer) {
+        mRecentsContainer = recentsContainer;
     }
 
     @Override
     protected void init(TaskbarControllers taskbarControllers) {
         super.init(taskbarControllers);
-        mRecentsActivity.setTaskbarUIController(this);
-        mRecentsActivity.getStateManager().addStateListener(mStateListener);
+        mRecentsContainer.setTaskbarUIController(this);
+        mRecentsContainer.getStateManager().addStateListener(mStateListener);
     }
 
     @Override
     protected void onDestroy() {
         super.onDestroy();
-        getRecentsView().setTaskLaunchListener(null);
-        mRecentsActivity.setTaskbarUIController(null);
-        mRecentsActivity.getStateManager().removeStateListener(mStateListener);
+        RecentsView recentsView = getRecentsView();
+        if (recentsView != null) {
+            recentsView.setTaskLaunchListener(null);
+        }
+        mRecentsContainer.setTaskbarUIController(null);
+        mRecentsContainer.getStateManager().removeStateListener(mStateListener);
     }
 
     /**
@@ -108,8 +119,8 @@
     }
 
     @Override
-    public RecentsView getRecentsView() {
-        return mRecentsActivity.getOverviewPanel();
+    public @Nullable RecentsView getRecentsView() {
+        return mRecentsContainer.getOverviewPanel();
     }
 
     @Override
@@ -131,11 +142,11 @@
     @Nullable
     @Override
     protected TISBindHelper getTISBindHelper() {
-        return mRecentsActivity.getTISBindHelper();
+        return mRecentsContainer.getTISBindHelper();
     }
 
     @Override
     protected String getTaskbarUIControllerName() {
-        return "FallbackTaskbarUIController";
+        return "FallbackTaskbarUIController<" + mRecentsContainer.getClass().getSimpleName() + ">";
     }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
index ea432f3..23a5a27 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
@@ -17,13 +17,17 @@
 
 import android.content.ComponentName;
 import android.content.pm.ActivityInfo;
+import android.view.MotionEvent;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
+import com.android.launcher3.Flags;
 import com.android.launcher3.R;
 import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext;
+import com.android.launcher3.taskbar.overlay.TaskbarOverlayDragLayer;
+import com.android.launcher3.util.TouchController;
 import com.android.quickstep.RecentsModel;
 import com.android.quickstep.util.DesktopTask;
 import com.android.quickstep.util.GroupTask;
@@ -36,6 +40,7 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.Set;
 import java.util.function.Consumer;
 import java.util.stream.Collectors;
 
@@ -43,12 +48,14 @@
  * Handles initialization of the {@link KeyboardQuickSwitchViewController}.
  */
 public final class KeyboardQuickSwitchController implements
-        TaskbarControllers.LoggableTaskbarController {
+        TaskbarControllers.LoggableTaskbarController, TouchController {
 
     @VisibleForTesting
     public static final int MAX_TASKS = 6;
 
     @NonNull private final ControllerCallbacks mControllerCallbacks = new ControllerCallbacks();
+    // Callback used to notify when the KQS view is closed.
+    @Nullable private Runnable mOnClosed;
 
     // Initialized on init
     @Nullable private RecentsModel mModel;
@@ -58,12 +65,18 @@
     private int mTaskListChangeId = -1;
     // Only empty before the recent tasks list has been loaded the first time
     @NonNull private List<GroupTask> mTasks = new ArrayList<>();
+    // Set of task IDs filtered out of tasks in recents model to generate list of tasks to show in
+    // the Keyboard Quick Switch view. Non empty only if the view has been shown in response to
+    // toggling taskbar overflow button.
+    @NonNull private Set<Integer> mExcludedTaskIds = Collections.emptySet();
+
     private int mNumHiddenTasks = 0;
 
     // Initialized in init
     private TaskbarControllers mControllers;
 
     @Nullable private KeyboardQuickSwitchViewController mQuickSwitchViewController;
+    @Nullable private TaskbarOverlayContext mOverlayContext;
 
     private boolean mHasDesktopTask = false;
     private boolean mWasDesktopTaskFilteredOut = false;
@@ -84,10 +97,12 @@
             return;
         }
         int currentFocusedIndex = mQuickSwitchViewController.getCurrentFocusedIndex();
+        boolean wasOpenedFromTaskbar = mQuickSwitchViewController.wasOpenedFromTaskbar();
         onDestroy();
         if (currentFocusedIndex != -1) {
             mControllers.taskbarActivityContext.getMainThreadHandler().post(
-                    () -> openQuickSwitchView(currentFocusedIndex));
+                    () -> openQuickSwitchView(currentFocusedIndex, mExcludedTaskIds,
+                            wasOpenedFromTaskbar));
         }
     }
 
@@ -95,29 +110,65 @@
         openQuickSwitchView(-1);
     }
 
+    /**
+     * Opens or closes the view in response to taskbar action. The view shows a filtered list of
+     * tasks.
+     * @param taskIdsToExclude A list of tasks to exclude in the opened view.
+     * @param onClosed A callback used to notify when the KQS view is closed.
+     */
+    void toggleQuickSwitchViewForTaskbar(@NonNull Set<Integer> taskIdsToExclude,
+            @NonNull Runnable onClosed) {
+        mOnClosed = onClosed;
+
+        // Close the view if its shown, and was opened from the taskbar.
+        if (mQuickSwitchViewController != null
+                && !mQuickSwitchViewController.isCloseAnimationRunning()
+                && mQuickSwitchViewController.wasOpenedFromTaskbar()) {
+            closeQuickSwitchView(true);
+            return;
+        }
+
+        openQuickSwitchView(-1, taskIdsToExclude, true);
+    }
+
     private void openQuickSwitchView(int currentFocusedIndex) {
+        openQuickSwitchView(currentFocusedIndex, Collections.emptySet(), false);
+    }
+
+    private void openQuickSwitchView(int currentFocusedIndex,
+            @NonNull Set<Integer> taskIdsToExclude,
+            boolean wasOpenedFromTaskbar) {
         if (mQuickSwitchViewController != null) {
-            if (!mQuickSwitchViewController.isCloseAnimationRunning()) {
+            if (!mQuickSwitchViewController.isCloseAnimationRunning()
+                    && mQuickSwitchViewController.wasOpenedFromTaskbar() == wasOpenedFromTaskbar) {
                 return;
             }
-            // Allow the KQS to be reopened during the close animation to make it more responsive
+
+            // Allow the KQS to be reopened during the close animation to make it more responsive.
+            // Similarly, if KQS was opened in different mode (from taskbar vs. keyboard event),
+            // close it so it can be reopened in the correct mode.
+            // TODO(b/368119679) Consider updating list of shown tasks in place, or at least reopen
+            // the view in the same vertical location.
             closeQuickSwitchView(false);
         }
-        TaskbarOverlayContext overlayContext =
-                mControllers.taskbarOverlayController.requestWindow();
+        mOverlayContext = mControllers.taskbarOverlayController.requestWindow();
+        if (Flags.taskbarOverflow()) {
+            mOverlayContext.getDragLayer().addTouchController(this);
+        }
         KeyboardQuickSwitchView keyboardQuickSwitchView =
-                (KeyboardQuickSwitchView) overlayContext.getLayoutInflater()
+                (KeyboardQuickSwitchView) mOverlayContext.getLayoutInflater()
                         .inflate(
                                 R.layout.keyboard_quick_switch_view,
-                                overlayContext.getDragLayer(),
+                                mOverlayContext.getDragLayer(),
                                 /* attachToRoot= */ false);
         mQuickSwitchViewController = new KeyboardQuickSwitchViewController(
-                mControllers, overlayContext, keyboardQuickSwitchView, mControllerCallbacks);
+                mControllers, mOverlayContext, keyboardQuickSwitchView, mControllerCallbacks);
 
         final boolean onDesktop =
                 mControllers.taskbarDesktopModeController.getAreDesktopTasksVisible();
 
-        if (mModel.isTaskListValid(mTaskListChangeId)) {
+        if (mModel.isTaskListValid(mTaskListChangeId)
+                && taskIdsToExclude.equals(mExcludedTaskIds)) {
             // When we are opening the KQS with no focus override, check if the first task is
             // running. If not, focus that first task.
             mQuickSwitchViewController.openQuickSwitchView(
@@ -128,17 +179,19 @@
                             ? 0 : currentFocusedIndex,
                     onDesktop,
                     mHasDesktopTask,
-                    mWasDesktopTaskFilteredOut);
+                    mWasDesktopTaskFilteredOut,
+                    wasOpenedFromTaskbar);
             return;
         }
 
+        mExcludedTaskIds = taskIdsToExclude;
         mTaskListChangeId = mModel.getTasks((tasks) -> {
             mHasDesktopTask = false;
             mWasDesktopTaskFilteredOut = false;
             if (onDesktop) {
-                processLoadedTasksOnDesktop(tasks);
+                processLoadedTasksOnDesktop(tasks, taskIdsToExclude);
             } else {
-                processLoadedTasks(tasks);
+                processLoadedTasks(tasks, taskIdsToExclude);
             }
             // Check if the first task is running after the recents model has updated so that we use
             // the correct index.
@@ -150,15 +203,21 @@
                             ? 0 : currentFocusedIndex,
                     onDesktop,
                     mHasDesktopTask,
-                    mWasDesktopTaskFilteredOut);
+                    mWasDesktopTaskFilteredOut,
+                    wasOpenedFromTaskbar);
         });
     }
 
-    private void processLoadedTasks(List<GroupTask> tasks) {
+    private boolean shouldExcludeTask(GroupTask task, Set<Integer> taskIdsToExclude) {
+        return Flags.taskbarOverflow() && taskIdsToExclude.contains(task.task1.key.id);
+    }
+
+    private void processLoadedTasks(List<GroupTask> tasks, Set<Integer> taskIdsToExclude) {
         // Only store MAX_TASK tasks, from most to least recent
         Collections.reverse(tasks);
         mTasks = tasks.stream()
-                .filter(task -> !(task instanceof DesktopTask))
+                .filter(task -> !(task instanceof DesktopTask)
+                        && !shouldExcludeTask(task, taskIdsToExclude))
                 .limit(MAX_TASKS)
                 .collect(Collectors.toList());
 
@@ -176,12 +235,15 @@
                 tasks.size() - (mWasDesktopTaskFilteredOut ? 1 : 0) - MAX_TASKS);
     }
 
-    private void processLoadedTasksOnDesktop(List<GroupTask> tasks) {
+    private void processLoadedTasksOnDesktop(List<GroupTask> tasks, Set<Integer> taskIdsToExclude) {
         // Find the single desktop task that contains a grouping of desktop tasks
         DesktopTask desktopTask = findDesktopTask(tasks);
 
         if (desktopTask != null) {
-            mTasks = desktopTask.tasks.stream().map(GroupTask::new).collect(Collectors.toList());
+            mTasks = desktopTask.tasks.stream()
+                    .map(GroupTask::new)
+                    .filter(task -> !shouldExcludeTask(task, taskIdsToExclude))
+                    .collect(Collectors.toList());
             // All other tasks, apart from the grouped desktop task, are hidden
             mNumHiddenTasks = Math.max(0, tasks.size() - 1);
         } else {
@@ -208,6 +270,10 @@
             return;
         }
         mQuickSwitchViewController.closeQuickSwitchView(animate);
+        if (mOnClosed != null) {
+            mOnClosed.run();
+            mOnClosed = null;
+        }
     }
 
     /**
@@ -220,6 +286,27 @@
                 ? -1 : mQuickSwitchViewController.launchFocusedTask();
     }
 
+    @Override
+    public boolean onControllerTouchEvent(MotionEvent ev) {
+        return false;
+    }
+
+    @Override
+    public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
+        if (mQuickSwitchViewController == null
+                || mOverlayContext == null
+                || !Flags.taskbarOverflow()) {
+            return false;
+        }
+
+        TaskbarOverlayDragLayer dragLayer = mOverlayContext.getDragLayer();
+        if (ev.getAction() == MotionEvent.ACTION_DOWN
+                && !mQuickSwitchViewController.isEventOverKeyboardQuickSwitch(dragLayer, ev)) {
+            closeQuickSwitchView(true);
+        }
+        return false;
+    }
+
     void onDestroy() {
         if (mQuickSwitchViewController != null) {
             mQuickSwitchViewController.onDestroy();
@@ -279,6 +366,11 @@
         }
 
         void onCloseComplete() {
+            if (Flags.taskbarOverflow() && mOverlayContext != null) {
+                mOverlayContext.getDragLayer()
+                        .removeTouchController(KeyboardQuickSwitchController.this);
+            }
+            mOverlayContext = null;
             mQuickSwitchViewController = null;
         }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
index fc8204a..05d34b5 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
@@ -17,8 +17,6 @@
 
 import static androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.PARENT_ID;
 
-import static com.android.launcher3.taskbar.KeyboardQuickSwitchController.MAX_TASKS;
-
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
@@ -133,6 +131,15 @@
     }
 
     @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+
+        if (mViewCallbacks != null) {
+            mViewCallbacks.onViewDetchedFromWindow();
+        }
+    }
+
+    @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
         mNoRecentItemsPane = findViewById(R.id.no_recent_items_pane);
@@ -200,7 +207,7 @@
 
         View previousTaskView = null;
         LayoutInflater layoutInflater = LayoutInflater.from(context);
-        int tasksToDisplay = Math.min(MAX_TASKS, groupTasks.size());
+        int tasksToDisplay = groupTasks.size();
         for (int i = 0; i < tasksToDisplay; i++) {
             GroupTask groupTask = groupTasks.get(i);
             KeyboardQuickSwitchTaskView currentTaskView = createAndAddTaskView(
@@ -283,6 +290,10 @@
         return mDesktopTaskIndex;
     }
 
+    void resetViewCallbacks() {
+        mViewCallbacks = null;
+    }
+
     protected Animator getCloseAnimation() {
         AnimatorSet closeAnimation = new AnimatorSet();
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
index 40e77e2..390112e 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
@@ -15,11 +15,15 @@
  */
 package com.android.launcher3.taskbar;
 
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
+import android.content.res.Resources;
+import android.view.Gravity;
 import android.view.KeyEvent;
+import android.view.MotionEvent;
 import android.view.animation.AnimationUtils;
 import android.window.RemoteTransition;
 
@@ -27,9 +31,16 @@
 import androidx.annotation.Nullable;
 
 import com.android.internal.jank.Cuj;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimatorListeners;
+import com.android.launcher3.desktop.DesktopAppLaunchTransition;
+import com.android.launcher3.desktop.DesktopAppLaunchTransition.AppLaunchType;
 import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext;
+import com.android.launcher3.taskbar.overlay.TaskbarOverlayDragLayer;
+import com.android.launcher3.util.DisplayController;
+import com.android.launcher3.views.BaseDragLayer;
 import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.util.GroupTask;
 import com.android.quickstep.util.SlideInRemoteTransition;
@@ -60,6 +71,9 @@
 
     private boolean mOnDesktop;
     private boolean mWasDesktopTaskFilteredOut;
+    private boolean mWasOpenedFromTaskbar;
+
+    private boolean mDetachingFromWindow = false;
 
     protected KeyboardQuickSwitchViewController(
             @NonNull TaskbarControllers controllers,
@@ -76,6 +90,10 @@
         return mCurrentFocusIndex;
     }
 
+    protected boolean wasOpenedFromTaskbar() {
+        return mWasOpenedFromTaskbar;
+    }
+
     protected void openQuickSwitchView(
             @NonNull List<GroupTask> tasks,
             int numHiddenTasks,
@@ -83,10 +101,22 @@
             int currentFocusIndexOverride,
             boolean onDesktop,
             boolean hasDesktopTask,
-            boolean wasDesktopTaskFilteredOut) {
+            boolean wasDesktopTaskFilteredOut,
+            boolean wasOpenedFromTaskbar) {
+        final boolean isTransientTaskBar = DisplayController.isTransientTaskbar(
+                mControllers.taskbarActivityContext);
+        positionView(wasOpenedFromTaskbar, isTransientTaskBar);
+
+        // Keep the taskbar unstashed if the KQS is opened.
+        if (wasOpenedFromTaskbar && isTransientTaskBar) {
+            mControllers.taskbarStashController.updateTaskbarTimeout(/* isAutohideSuspended= */
+                    true);
+        }
+
         mOverlayContext.getDragLayer().addView(mKeyboardQuickSwitchView);
         mOnDesktop = onDesktop;
         mWasDesktopTaskFilteredOut = wasDesktopTaskFilteredOut;
+        mWasOpenedFromTaskbar = wasOpenedFromTaskbar;
 
         mKeyboardQuickSwitchView.applyLoadPlan(
                 mOverlayContext,
@@ -98,6 +128,33 @@
                 /* useDesktopTaskView= */ !onDesktop && hasDesktopTask);
     }
 
+    protected void positionView(boolean wasOpenedFromTaskbar, boolean isTransientTaskbar) {
+        if (!wasOpenedFromTaskbar) {
+            // Keep the default positioning.
+            return;
+        }
+
+        BaseDragLayer.LayoutParams lp = new BaseDragLayer.LayoutParams(
+                mKeyboardQuickSwitchView.getLayoutParams());
+        final Resources resources = mKeyboardQuickSwitchView.getResources();
+        final int marginHorizontal = resources.getDimensionPixelSize(
+                R.dimen.keyboard_quick_switch_margin_ends);
+
+        final DeviceProfile dp = mControllers.taskbarActivityContext.getDeviceProfile();
+        // Calculate the additional margin space that the KQS should move up for the transient
+        // taskbar. The value of spaceForTaskbar is the distance between the bottom of the KQS
+        // view with 0 bottom margin to the top of the transient taskbar view.
+        final int spaceForTaskbar = isTransientTaskbar ? dp.taskbarHeight + dp.taskbarBottomMargin
+                - dp.stashedTaskbarHeight : 0;
+        final int marginBottom = spaceForTaskbar + resources.getDimensionPixelSize(
+                R.dimen.keyboard_quick_switch_margin_bottom);
+
+        lp.setMargins(marginHorizontal, 0, marginHorizontal, marginBottom);
+        lp.width = BaseDragLayer.LayoutParams.WRAP_CONTENT;
+        lp.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL;
+        mKeyboardQuickSwitchView.setLayoutParams(lp);
+    }
+
     boolean isCloseAnimationRunning() {
         return mCloseAnimation != null;
     }
@@ -162,7 +219,7 @@
         Runnable onFinishCallback = () -> InteractionJankMonitorWrapper.end(
                 Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH);
         TaskbarActivityContext context = mControllers.taskbarActivityContext;
-        RemoteTransition remoteTransition = new RemoteTransition(new SlideInRemoteTransition(
+        final RemoteTransition slideInTransition = new RemoteTransition(new SlideInRemoteTransition(
                 Utilities.isRtl(mControllers.taskbarActivityContext.getResources()),
                 context.getDeviceProfile().overviewPageSpacing,
                 QuickStepContract.getWindowCornerRadius(context),
@@ -176,7 +233,7 @@
                     SystemUiProxy.INSTANCE.get(mKeyboardQuickSwitchView.getContext())
                             .showDesktopApps(
                                     mKeyboardQuickSwitchView.getDisplay().getDisplayId(),
-                                    remoteTransition));
+                                    slideInTransition));
             return -1;
         }
         // Even with a valid index, this can be null if the user tries to quick switch before the
@@ -189,6 +246,15 @@
             // Ignore attempts to run the selected task if it is already running.
             return -1;
         }
+        RemoteTransition remoteTransition = slideInTransition;
+        if (mOnDesktop
+                && mControllers.taskbarActivityContext.canUnminimizeDesktopTask(task.task1.key.id)
+        ) {
+            // This app is being unminimized - use our own transition runner.
+            remoteTransition = new RemoteTransition(
+                    new DesktopAppLaunchTransition(
+                        context, MAIN_EXECUTOR, AppLaunchType.UNMINIMIZE));
+        }
         mControllers.taskbarActivityContext.handleGroupTaskLaunch(
                 task,
                 remoteTransition,
@@ -200,7 +266,12 @@
 
     private void onCloseComplete() {
         mCloseAnimation = null;
-        mOverlayContext.getDragLayer().removeView(mKeyboardQuickSwitchView);
+        // Reset the view callbacks to prevent `onDetachedFromWindow` getting called in response to
+        // the `removeView(mKeyboardQuickSwitchView)` call.
+        mKeyboardQuickSwitchView.resetViewCallbacks();
+        if (!mDetachingFromWindow) {
+            mOverlayContext.getDragLayer().removeView(mKeyboardQuickSwitchView);
+        }
         mControllerCallbacks.onCloseComplete();
         InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_CLOSE);
     }
@@ -217,6 +288,14 @@
         pw.println(prefix + "\tmCurrentFocusIndex=" + mCurrentFocusIndex);
         pw.println(prefix + "\tmOnDesktop=" + mOnDesktop);
         pw.println(prefix + "\tmWasDesktopTaskFilteredOut=" + mWasDesktopTaskFilteredOut);
+        pw.println(prefix + "\tmWasOpenedFromTaskbar=" + mWasOpenedFromTaskbar);
+    }
+
+    /**
+     * @return True if the MotionEvent is over the {@link KeyboardQuickSwitchView}.
+     */
+    protected boolean isEventOverKeyboardQuickSwitch(TaskbarOverlayDragLayer dl, MotionEvent ev) {
+        return dl.isEventOverView(mKeyboardQuickSwitchView, ev);
     }
 
     class ViewCallbacks {
@@ -283,5 +362,11 @@
         boolean isAspectRatioSquare() {
             return mControllerCallbacks.isAspectRatioSquare();
         }
+
+        void onViewDetchedFromWindow() {
+            mDetachingFromWindow = true;
+            closeQuickSwitchView(false);
+            mDetachingFromWindow = false;
+        }
     }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
index 477f90c..4a94be7 100644
--- a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
@@ -15,8 +15,10 @@
  */
 package com.android.launcher3.taskbar;
 
-import static android.window.flags.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY;
+import static android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY;
 
+import static com.android.launcher3.QuickstepTransitionManager.TASKBAR_TO_APP_DURATION;
+import static com.android.launcher3.QuickstepTransitionManager.getTaskbarToHomeDuration;
 import static com.android.launcher3.QuickstepTransitionManager.TRANSIENT_TASKBAR_TRANSITION_DURATION;
 import static com.android.launcher3.statemanager.BaseState.FLAG_NON_INTERACTIVE;
 import static com.android.launcher3.taskbar.TaskbarEduTooltipControllerKt.TOOLTIP_STEP_FEATURES;
@@ -32,7 +34,6 @@
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Flags;
 import com.android.launcher3.LauncherState;
-import com.android.launcher3.QuickstepTransitionManager;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimatedFloat;
 import com.android.launcher3.logging.InstanceId;
@@ -50,6 +51,7 @@
 import com.android.quickstep.util.TISBindHelper;
 import com.android.quickstep.views.RecentsView;
 import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
+import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
 
 import java.io.PrintWriter;
@@ -204,11 +206,17 @@
                 isVisible,
                 fromInitOrDestroy,
                 /* startAnimation= */ true,
-                DisplayController.isTransientTaskbar(mLauncher)
-                        ? TRANSIENT_TASKBAR_TRANSITION_DURATION
-                        : (!isVisible
-                                ? QuickstepTransitionManager.TASKBAR_TO_APP_DURATION
-                                : QuickstepTransitionManager.getTaskbarToHomeDuration()));
+                getTaskbarAnimationDuration(isVisible));
+    }
+
+    private int getTaskbarAnimationDuration(boolean isVisible) {
+        if (isVisible && !mLauncher.getPredictiveBackToHomeInProgress()) {
+            return getTaskbarToHomeDuration();
+        } else {
+            return DisplayController.isTransientTaskbar(mLauncher)
+                    ? TRANSIENT_TASKBAR_TRANSITION_DURATION
+                    : TASKBAR_TO_APP_DURATION;
+        }
     }
 
     @Nullable
@@ -354,16 +362,22 @@
             // This method can be called before init() is called.
             return;
         }
-        if (mControllers.uiController.isIconAlignedWithHotseat()
-                && !mTaskbarLauncherStateController.isAnimatingToLauncher()) {
-            // Only animate the nav buttons while home and not animating home, otherwise let
-            // the TaskbarViewController handle it.
-            mControllers.navbarButtonsViewController
-                    .getTaskbarNavButtonTranslationYForInAppDisplay()
-                    .updateValue(mLauncher.getDeviceProfile().getTaskbarOffsetY()
-                            * mTaskbarInAppDisplayProgress.value);
-            mControllers.navbarButtonsViewController
-                    .getOnTaskbarBackgroundNavButtonColorOverride().updateValue(progress);
+        if (mControllers.uiController.isIconAlignedWithHotseat()) {
+            if (!mTaskbarLauncherStateController.isAnimatingToLauncher()) {
+                // Only animate the nav buttons while home and not animating home, otherwise let
+                // the TaskbarViewController handle it.
+                mControllers.navbarButtonsViewController
+                        .getTaskbarNavButtonTranslationYForInAppDisplay()
+                        .updateValue(mLauncher.getDeviceProfile().getTaskbarOffsetY()
+                                * mTaskbarInAppDisplayProgress.value);
+                mControllers.navbarButtonsViewController
+                        .getOnTaskbarBackgroundNavButtonColorOverride().updateValue(progress);
+            }
+            if (isBubbleBarEnabled()) {
+                mControllers.bubbleControllers.ifPresent(
+                        c -> c.bubbleStashController.setInAppDisplayOverrideProgress(
+                                mTaskbarInAppDisplayProgress.value));
+            }
         }
     }
 
@@ -469,6 +483,18 @@
     }
 
     @Override
+    public void onBubbleBarLocationAnimated(BubbleBarLocation location) {
+        mTaskbarLauncherStateController.onBubbleBarLocationChanged(location, /* animate = */ true);
+        mLauncher.setBubbleBarLocation(location);
+    }
+
+    @Override
+    public void onBubbleBarLocationUpdated(BubbleBarLocation location) {
+        mTaskbarLauncherStateController.onBubbleBarLocationChanged(location, /* animate = */ false);
+        mLauncher.setBubbleBarLocation(location);
+    }
+
+    @Override
     public void onSwipeToUnstashTaskbar() {
         // Once taskbar is unstashed, the user cannot return back to the overlay. We can
         // clear it here to set the expected state once the user goes home.
diff --git a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
index 2ac5793..fbd1b6e 100644
--- a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
@@ -23,6 +23,7 @@
 import static com.android.launcher3.LauncherAnimUtils.ROTATION_DRAWABLE_PERCENT;
 import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X;
 import static com.android.launcher3.Utilities.getDescendantCoordRelativeToAncestor;
+import static com.android.launcher3.anim.AnimatorListeners.forEndCallback;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_TASKBAR_NAVBAR_UNIFICATION;
 import static com.android.launcher3.taskbar.LauncherTaskbarUIController.SYSUI_SURFACE_PROGRESS_INDEX;
 import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_A11Y;
@@ -48,7 +49,10 @@
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SHORTCUT_HELPER_SHOWING;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING;
+import static com.android.window.flags.Flags.predictiveBackThreeButtonNav;
+import static com.android.wm.shell.Flags.enableBubbleBarInPersistentTaskBar;
 
+import android.animation.Animator;
 import android.animation.ArgbEvaluator;
 import android.animation.ObjectAnimator;
 import android.annotation.DrawableRes;
@@ -68,8 +72,10 @@
 import android.graphics.drawable.RotateDrawable;
 import android.inputmethodservice.InputMethodService;
 import android.os.Handler;
+import android.os.SystemClock;
 import android.util.Property;
 import android.view.Gravity;
+import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.View.OnAttachStateChangeListener;
@@ -170,11 +176,14 @@
     // Used for IME+A11Y buttons
     private final ViewGroup mEndContextualContainer;
     private final ViewGroup mStartContextualContainer;
-    private final int mLightIconColorOnHome;
-    private final int mDarkIconColorOnHome;
-    /** Color to use for navigation bar buttons, if they are on on a Taskbar surface background. */
+    private final int mLightIconColorOnWorkspace;
+    private final int mDarkIconColorOnWorkspace;
+    /** Color to use for navbar buttons, if they are on on a Taskbar surface background. */
     private final int mOnBackgroundIconColor;
 
+    private @Nullable Animator mNavBarLocationAnimator;
+    private @Nullable BubbleBarLocation mBubbleBarTargetLocation;
+
     private final AnimatedFloat mTaskbarNavButtonTranslationY = new AnimatedFloat(
             this::updateNavButtonTranslationY);
     private final AnimatedFloat mTaskbarNavButtonTranslationYForInAppDisplay = new AnimatedFloat(
@@ -185,7 +194,10 @@
     // Used for System UI state updates that should translate the nav button for in-app display.
     private final AnimatedFloat mNavButtonInAppDisplayProgressForSysui = new AnimatedFloat(
             this::updateNavButtonInAppDisplayProgressForSysui);
-    /** Expected nav button dark intensity communicated via the framework. */
+    /**
+     * Expected nav button dark intensity piped down from {@code LightBarController} in framework
+     * via {@code TaskbarDelegate}.
+     */
     private final AnimatedFloat mTaskbarNavButtonDarkIntensity = new AnimatedFloat(
             this::onDarkIntensityChanged);
     /** {@code 1} if the Taskbar background color is fully opaque. */
@@ -240,8 +252,8 @@
         mEndContextualContainer = mNavButtonsView.findViewById(R.id.end_contextual_buttons);
         mStartContextualContainer = mNavButtonsView.findViewById(R.id.start_contextual_buttons);
 
-        mLightIconColorOnHome = context.getColor(R.color.taskbar_nav_icon_light_color_on_home);
-        mDarkIconColorOnHome = context.getColor(R.color.taskbar_nav_icon_dark_color_on_home);
+        mLightIconColorOnWorkspace = context.getColor(R.color.taskbar_nav_icon_light_color_on_home);
+        mDarkIconColorOnWorkspace = context.getColor(R.color.taskbar_nav_icon_dark_color_on_home);
         mOnBackgroundIconColor = Utilities.isDarkTheme(context)
                 ? context.getColor(R.color.taskbar_nav_icon_light_color)
                 : context.getColor(R.color.taskbar_nav_icon_dark_color);
@@ -400,6 +412,12 @@
             }
         };
         mSeparateWindowParent.recreateControllers();
+        if (BubbleBarController.isBubbleBarEnabled()) {
+            mNavButtonsView.addOnLayoutChangeListener(
+                    (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) ->
+                            onLayoutsUpdated()
+            );
+        }
     }
 
     private void initButtons(ViewGroup navContainer, ViewGroup endContainer,
@@ -750,40 +768,68 @@
         mNavButtonsView.setTranslationY(mLastSetNavButtonTranslationY);
     }
 
+    /**
+     * Sets Taskbar 3-button mode icon colors based on the
+     * {@link #mTaskbarNavButtonDarkIntensity} value piped in from Framework. For certain cases
+     * in large screen taskbar where there may be opaque surfaces, the selected SystemUI button
+     * colors are intentionally overridden.
+     * <p>
+     * This method is also called when any of the AnimatedFloat instances change.
+     */
     private void updateNavButtonColor() {
         final ArgbEvaluator argbEvaluator = ArgbEvaluator.getInstance();
-        final int sysUiNavButtonIconColorOnHome = (int) argbEvaluator.evaluate(
-                mTaskbarNavButtonDarkIntensity.value,
-                mLightIconColorOnHome,
-                mDarkIconColorOnHome);
-
-        final int iconColor;
-        if (ENABLE_TASKBAR_NAVBAR_UNIFICATION && mContext.isPhoneMode()) {
-            iconColor = sysUiNavButtonIconColorOnHome;
-        } else {
-            // Override the color from framework if nav buttons are over an opaque Taskbar surface.
-            iconColor = (int) argbEvaluator.evaluate(
-                    mOnBackgroundNavButtonColorOverrideMultiplier.value * Math.max(
-                            mOnTaskbarBackgroundNavButtonColorOverride.value,
-                            mSlideInViewVisibleNavButtonColorOverride.value),
-                    sysUiNavButtonIconColorOnHome,
-                    mOnBackgroundIconColor);
+        int taskbarNavButtonColor = getSysUiIconColorOnHome(argbEvaluator);
+        // Only phone mode foldable button colors should be identical to SysUI navbar colors.
+        if (!(ENABLE_TASKBAR_NAVBAR_UNIFICATION && mContext.isPhoneMode())) {
+            taskbarNavButtonColor = getTaskbarButtonColor(argbEvaluator, taskbarNavButtonColor);
         }
+        applyButtonColors(taskbarNavButtonColor);
+    }
 
+    /**
+     * Taskbar 3-button mode icon colors based on the
+     * {@link #mTaskbarNavButtonDarkIntensity} value piped in from Framework.
+     */
+    private int getSysUiIconColorOnHome(ArgbEvaluator argbEvaluator) {
+        return (int) argbEvaluator.evaluate(getTaskbarNavButtonDarkIntensity().value,
+                mLightIconColorOnWorkspace, mDarkIconColorOnWorkspace);
+    }
+
+    /**
+     * If Taskbar background is opaque or slide in overlay is visible, the selected SystemUI button
+     * colors are intentionally overridden. The override can be disabled when
+     * {@link #mOnBackgroundNavButtonColorOverrideMultiplier} is {@code 0}.
+     */
+    private int getTaskbarButtonColor(ArgbEvaluator argbEvaluator, int sysUiIconColorOnHome) {
+        final float sysUIColorOverride =
+                mOnBackgroundNavButtonColorOverrideMultiplier.value * Math.max(
+                        mOnTaskbarBackgroundNavButtonColorOverride.value,
+                        mSlideInViewVisibleNavButtonColorOverride.value);
+        return (int) argbEvaluator.evaluate(sysUIColorOverride, sysUiIconColorOnHome,
+                mOnBackgroundIconColor);
+    }
+
+    /**
+     * Iteratively sets button colors for each button in {@link #mAllButtons}.
+     */
+    private void applyButtonColors(int iconColor) {
         for (ImageView button : mAllButtons) {
             button.setImageTintList(ColorStateList.valueOf(iconColor));
             Drawable background = button.getBackground();
             if (background instanceof KeyButtonRipple) {
                 ((KeyButtonRipple) background).setDarkIntensity(
-                        mTaskbarNavButtonDarkIntensity.value);
+                        getTaskbarNavButtonDarkIntensity().value);
             }
         }
     }
 
+    /**
+     * Updates Taskbar 3-Button icon colors as {@link #mTaskbarNavButtonDarkIntensity} changes.
+     */
     private void onDarkIntensityChanged() {
         updateNavButtonColor();
         if (mContext.isPhoneMode()) {
-            mTaskbarTransitions.onDarkIntensityChanged(mTaskbarNavButtonDarkIntensity.value);
+            mTaskbarTransitions.onDarkIntensityChanged(getTaskbarNavButtonDarkIntensity().value);
         }
     }
 
@@ -800,12 +846,43 @@
         buttonView.setImageResource(drawableId);
         buttonView.setContentDescription(parent.getContext().getString(
                 navButtonController.getButtonContentDescription(buttonType)));
-        buttonView.setOnClickListener(view -> navButtonController.onButtonClick(buttonType, view));
-        buttonView.setOnLongClickListener(view ->
-                navButtonController.onButtonLongClick(buttonType, view));
+        if (predictiveBackThreeButtonNav() && buttonType == BUTTON_BACK) {
+            // set up special touch listener for back button to support predictive back
+            setBackButtonTouchListener(buttonView, navButtonController);
+        } else {
+            buttonView.setOnClickListener(view ->
+                    navButtonController.onButtonClick(buttonType, view));
+            buttonView.setOnLongClickListener(view ->
+                    navButtonController.onButtonLongClick(buttonType, view));
+        }
         return buttonView;
     }
 
+    private void setBackButtonTouchListener(View buttonView,
+            TaskbarNavButtonController navButtonController) {
+        buttonView.setOnTouchListener((v, event) -> {
+            if (event.getAction() == MotionEvent.ACTION_MOVE) return false;
+            long time = SystemClock.uptimeMillis();
+            int action = event.getAction();
+            KeyEvent keyEvent = new KeyEvent(time, time,
+                    action == MotionEvent.ACTION_DOWN ? KeyEvent.ACTION_DOWN : KeyEvent.ACTION_UP,
+                    KeyEvent.KEYCODE_BACK, 0);
+            if (event.getAction() == MotionEvent.ACTION_CANCEL) {
+                keyEvent.cancel();
+            }
+            navButtonController.executeBack(keyEvent);
+
+            if (action == MotionEvent.ACTION_UP) {
+                buttonView.performClick();
+            }
+            return false;
+        });
+        buttonView.setOnLongClickListener((view) ->  {
+            navButtonController.onButtonLongClick(BUTTON_BACK, view);
+            return false;
+        });
+    }
+
     private ImageView addButton(ViewGroup parent, @IdRes int id, @LayoutRes int layoutId) {
         ImageView buttonView = (ImageView) mContext.getLayoutInflater()
                 .inflate(layoutId, parent, false);
@@ -1174,14 +1251,42 @@
     /** Adjusts navigation buttons layout accordingly to the bubble bar position. */
     @Override
     public void onBubbleBarLocationUpdated(BubbleBarLocation location) {
+        boolean locationUpdated = location != mBubbleBarTargetLocation;
+        if (locationUpdated) {
+            cancelExistingNavBarAnimation();
+        } else {
+            endExistingAnimation();
+        }
         mNavButtonContainer.setTranslationX(getNavBarTranslationX(location));
+        mBubbleBarTargetLocation = location;
     }
 
     /** Animates navigation buttons accordingly to the bubble bar position. */
     @Override
     public void onBubbleBarLocationAnimated(BubbleBarLocation location) {
-        // TODO(b/346381754) add the teleport animation similarly to the bubble bar
-        mNavButtonContainer.setTranslationX(getNavBarTranslationX(location));
+        if (location == mBubbleBarTargetLocation) return;
+        cancelExistingNavBarAnimation();
+        mBubbleBarTargetLocation = location;
+        int finalX = getNavBarTranslationX(location);
+        Animator teleportAnimator = BarsLocationAnimatorHelper
+                .getTeleportAnimatorForNavButtons(location, mNavButtonContainer, finalX);
+        teleportAnimator.addListener(forEndCallback(() -> mNavBarLocationAnimator = null));
+        mNavBarLocationAnimator = teleportAnimator;
+        mNavBarLocationAnimator.start();
+    }
+
+    private void endExistingAnimation() {
+        if (mNavBarLocationAnimator != null) {
+            mNavBarLocationAnimator.end();
+            mNavBarLocationAnimator = null;
+        }
+    }
+
+    private void cancelExistingNavBarAnimation() {
+        if (mNavBarLocationAnimator != null) {
+            mNavBarLocationAnimator.cancel();
+            mNavBarLocationAnimator = null;
+        }
     }
 
     private int getNavBarTranslationX(BubbleBarLocation location) {
@@ -1218,12 +1323,22 @@
     }
 
     /** Adjusts the navigation buttons layout position according to the bubble bar location. */
-    public void onTaskbarLayoutChange() {
-        if (com.android.wm.shell.Flags.enableBubbleBarInPersistentTaskBar()
+    public void onLayoutsUpdated() {
+        // no need to do anything if on phone, or if taskbar or navbar views were not placed on
+        // screen.
+        if (mContext.getDeviceProfile().isPhone
+                || mControllers.taskbarViewController.getIconLayoutBounds().isEmpty()
+                || mNavButtonsView.getWidth() == 0) {
+            return;
+        }
+        if (enableBubbleBarInPersistentTaskBar()
                 && mControllers.bubbleControllers.isPresent()) {
-            BubbleBarLocation bubblesLocation = mControllers.bubbleControllers.get()
-                    .bubbleBarViewController.getBubbleBarLocation();
-            onBubbleBarLocationUpdated(bubblesLocation);
+            if (mBubbleBarTargetLocation == null) {
+                // only set bubble bar location if it was not set before
+                mBubbleBarTargetLocation = mControllers.bubbleControllers.get()
+                        .bubbleBarViewController.getBubbleBarLocation();
+            }
+            onBubbleBarLocationUpdated(mBubbleBarTargetLocation);
         }
     }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java b/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java
index 7273fac..eb47bb0 100644
--- a/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java
@@ -192,7 +192,9 @@
 
 
     public void onDestroy() {
-        mRegionSamplingHelper.stopAndDestroy();
+        if (mRegionSamplingHelper != null) {
+            mRegionSamplingHelper.stopAndDestroy();
+        }
         mRegionSamplingHelper = null;
     }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index c355e46..d7e5c61 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -29,7 +29,6 @@
 import static com.android.launcher3.AbstractFloatingView.TYPE_REBIND_SAFE;
 import static com.android.launcher3.AbstractFloatingView.TYPE_TASKBAR_OVERLAY_PROXY;
 import static com.android.launcher3.Flags.enableCursorHoverStates;
-import static com.android.launcher3.Flags.taskbarOverflow;
 import static com.android.launcher3.Utilities.calculateTextHeight;
 import static com.android.launcher3.Utilities.isRunningInTestHarness;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_TASKBAR_NAVBAR_UNIFICATION;
@@ -60,6 +59,7 @@
 import android.graphics.PixelFormat;
 import android.graphics.Rect;
 import android.hardware.display.DisplayManager;
+import android.os.Bundle;
 import android.os.IRemoteCallback;
 import android.os.Process;
 import android.os.Trace;
@@ -71,7 +71,9 @@
 import android.view.View;
 import android.view.WindowInsets;
 import android.view.WindowManager;
+import android.widget.FrameLayout;
 import android.widget.Toast;
+import android.window.DesktopModeFlags;
 import android.window.RemoteTransition;
 
 import androidx.annotation.NonNull;
@@ -89,6 +91,8 @@
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.apppairs.AppPairIcon;
 import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.desktop.DesktopAppLaunchTransition;
+import com.android.launcher3.desktop.DesktopAppLaunchTransition.AppLaunchType;
 import com.android.launcher3.dot.DotInfo;
 import com.android.launcher3.folder.Folder;
 import com.android.launcher3.folder.FolderIcon;
@@ -108,6 +112,7 @@
 import com.android.launcher3.taskbar.allapps.TaskbarAllAppsController;
 import com.android.launcher3.taskbar.bubbles.BubbleBarController;
 import com.android.launcher3.taskbar.bubbles.BubbleBarPinController;
+import com.android.launcher3.taskbar.bubbles.BubbleBarSwipeController;
 import com.android.launcher3.taskbar.bubbles.BubbleBarView;
 import com.android.launcher3.taskbar.bubbles.BubbleBarViewController;
 import com.android.launcher3.taskbar.bubbles.BubbleControllers;
@@ -131,11 +136,11 @@
 import com.android.launcher3.touch.ItemClickHandler.ItemClickProxy;
 import com.android.launcher3.util.ActivityOptionsWrapper;
 import com.android.launcher3.util.ApiWrapper;
+import com.android.launcher3.util.ApplicationInfoWrapper;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.Executors;
 import com.android.launcher3.util.NavigationMode;
-import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.util.RunnableList;
 import com.android.launcher3.util.SettingsCache;
 import com.android.launcher3.util.SplitConfigurationOptions.SplitSelectSource;
@@ -267,8 +272,10 @@
         NearestTouchFrame navButtonsView = mDragLayer.findViewById(R.id.navbuttons_view);
         StashedHandleView stashedHandleView = mDragLayer.findViewById(R.id.stashed_handle);
         BubbleBarView bubbleBarView = null;
+        FrameLayout bubbleBarContainer = null;
         if (isTransientTaskbar || Flags.enableBubbleBarInPersistentTaskBar()) {
             bubbleBarView = mDragLayer.findViewById(R.id.taskbar_bubbles);
+            bubbleBarContainer = mDragLayer.findViewById(R.id.taskbar_bubbles_container);
         }
         StashedHandleView bubbleHandleView = mDragLayer.findViewById(R.id.stashed_bubble_handle);
 
@@ -277,20 +284,27 @@
         // If Bubble bar is present, TaskbarControllers depends on it so build it first.
         Optional<BubbleControllers> bubbleControllersOptional = Optional.empty();
         BubbleBarController.onTaskbarRecreated();
-        if (BubbleBarController.isBubbleBarEnabled() && bubbleBarView != null) {
+        if (BubbleBarController.isBubbleBarEnabled()
+                && !mDeviceProfile.isPhone
+                && !mDeviceProfile.isVerticalBarLayout()
+                && bubbleBarView != null
+        ) {
             Optional<BubbleStashedHandleViewController> bubbleHandleController = Optional.empty();
+            Optional<BubbleBarSwipeController> bubbleBarSwipeController = Optional.empty();
             if (isTransientTaskbar) {
                 bubbleHandleController = Optional.of(
                         new BubbleStashedHandleViewController(this, bubbleHandleView));
+                bubbleBarSwipeController = Optional.of(new BubbleBarSwipeController(this));
             }
             TaskbarHotseatDimensionsProvider dimensionsProvider =
                     new DeviceProfileDimensionsProviderAdapter(this);
             BubbleStashController bubbleStashController = isTransientTaskbar
                     ? new TransientBubbleStashController(dimensionsProvider, this)
                     : new PersistentBubbleStashController(dimensionsProvider);
+            bubbleStashController.setHotseatVerticalCenter(launcherDp.getHotseatVerticalCenter());
             bubbleControllersOptional = Optional.of(new BubbleControllers(
                     new BubbleBarController(this, bubbleBarView),
-                    new BubbleBarViewController(this, bubbleBarView),
+                    new BubbleBarViewController(this, bubbleBarView, bubbleBarContainer),
                     bubbleStashController,
                     bubbleHandleController,
                     new BubbleDragController(this),
@@ -299,6 +313,7 @@
                             () -> DisplayController.INSTANCE.get(this).getInfo().currentSize),
                     new BubblePinController(this, mDragLayer,
                             () -> DisplayController.INSTANCE.get(this).getInfo().currentSize),
+                    bubbleBarSwipeController,
                     new BubbleCreator(this)
             ));
         }
@@ -352,8 +367,11 @@
     /** Updates {@link DeviceProfile} instances for any Taskbar windows. */
     public void updateDeviceProfile(DeviceProfile launcherDp) {
         applyDeviceProfile(launcherDp);
-
         mControllers.taskbarOverlayController.updateLauncherDeviceProfile(launcherDp);
+        mControllers.bubbleControllers.ifPresent(bubbleControllers -> {
+            int hotseatVertCenter = launcherDp.getHotseatVerticalCenter();
+            bubbleControllers.bubbleStashController.setHotseatVerticalCenter(hotseatVertCenter);
+        });
         AbstractFloatingView.closeAllOpenViewsExcept(this, false, TYPE_REBIND_SAFE);
         // Reapply fullscreen to take potential new screen size into account.
         setTaskbarWindowFullscreen(mIsFullscreen);
@@ -400,7 +418,7 @@
     /** Called when the visibility of the bubble bar changed. */
     public void bubbleBarVisibilityChanged(boolean isVisible) {
         mControllers.uiController.adjustHotseatForBubbleBar(isVisible);
-        mControllers.taskbarViewController.resetIconAlignmentController();
+        mControllers.taskbarViewController.adjustTaskbarForBubbleBar();
     }
 
     public void init(@NonNull TaskbarSharedState sharedState) {
@@ -846,6 +864,33 @@
         return makeDefaultActivityOptions(SPLASH_SCREEN_STYLE_UNDEFINED);
     }
 
+    private ActivityOptionsWrapper getActivityLaunchDesktopOptions(ItemInfo info) {
+        if (!DesktopModeFlags.ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS.isTrue()) {
+            return null;
+        }
+        if (!areDesktopTasksVisible()) {
+            return null;
+        }
+        BubbleTextView.RunningAppState appState =
+                mControllers.taskbarRecentAppsController.getDesktopItemState(info);
+        AppLaunchType launchType = null;
+        switch (appState) {
+            case RUNNING:
+                return null;
+            case MINIMIZED:
+                launchType = AppLaunchType.UNMINIMIZE;
+                break;
+            case NOT_RUNNING:
+                launchType = AppLaunchType.LAUNCH;
+                break;
+        }
+        ActivityOptions options = ActivityOptions.makeRemoteTransition(
+                new RemoteTransition(
+                        new DesktopAppLaunchTransition(
+                                /* context= */ this, getMainExecutor(), launchType)));
+        return new ActivityOptionsWrapper(options, new RunnableList());
+    }
+
     /**
      * Sets a new data-source for this taskbar instance
      */
@@ -939,15 +984,27 @@
     }
 
     /**
-     * Hides the taskbar icons and background when the notication shade is expanded.
+     * Hides the taskbar icons and background when the notification shade is expanded.
      */
     private void onNotificationShadeExpandChanged(boolean isExpanded, boolean skipAnim) {
+        // Close all floating views within the Taskbar window to make sure nothing is shown over
+        // the notification shade.
+        if (isExpanded) {
+            AbstractFloatingView.closeAllOpenViewsExcept(this, TYPE_TASKBAR_OVERLAY_PROXY);
+        }
+
         float alpha = isExpanded ? 0 : 1;
         AnimatorSet anim = new AnimatorSet();
         anim.play(mControllers.taskbarViewController.getTaskbarIconAlpha().get(
                 TaskbarViewController.ALPHA_INDEX_NOTIFICATION_EXPANDED).animateToValue(alpha));
         anim.play(mControllers.taskbarDragLayerController.getNotificationShadeBgTaskbar()
                 .animateToValue(alpha));
+
+        mControllers.bubbleControllers.ifPresent(controllers -> {
+            BubbleBarViewController bubbleBarViewController = controllers.bubbleBarViewController;
+            anim.play(bubbleBarViewController.getBubbleBarAlpha().get(0).animateToValue(alpha));
+        });
+
         anim.start();
         if (skipAnim) {
             anim.end();
@@ -974,8 +1031,8 @@
     }
 
     public void onNavButtonsDarkIntensityChanged(float darkIntensity) {
-        mControllers.navbarButtonsViewController.getTaskbarNavButtonDarkIntensity()
-                .updateValue(darkIntensity);
+        mControllers.navbarButtonsViewController.getTaskbarNavButtonDarkIntensity().updateValue(
+                darkIntensity);
     }
 
     public void onNavigationBarLumaSamplingEnabled(int displayId, boolean enable) {
@@ -1191,15 +1248,13 @@
         boolean shouldCloseAllOpenViews = true;
         Object tag = view.getTag();
 
-        if (taskbarOverflow()) {
-            mControllers.keyboardQuickSwitchController.closeQuickSwitchView(false);
-        }
+        mControllers.keyboardQuickSwitchController.closeQuickSwitchView(false);
 
         if (tag instanceof GroupTask groupTask) {
-            handleGroupTaskLaunch(
-                    groupTask,
-                    /* remoteTransition= */ null,
-                    areDesktopTasksVisible());
+            RemoteTransition remoteTransition =
+                    (areDesktopTasksVisible() && canUnminimizeDesktopTask(groupTask.task1.key.id))
+                            ? createUnminimizeRemoteTransition() : null;
+            handleGroupTaskLaunch(groupTask, remoteTransition, areDesktopTasksVisible());
             mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true);
         } else if (tag instanceof FolderInfo) {
             // Tapping an expandable folder icon on Taskbar
@@ -1217,8 +1272,11 @@
                 mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true);
             }
         } else if (tag instanceof TaskItemInfo info) {
+            RemoteTransition remoteTransition = canUnminimizeDesktopTask(info.getTaskId())
+                    ? createUnminimizeRemoteTransition() : null;
             UI_HELPER_EXECUTOR.execute(() ->
-                    SystemUiProxy.INSTANCE.get(this).showDesktopApp(info.getTaskId()));
+                    SystemUiProxy.INSTANCE.get(this).showDesktopApp(
+                            info.getTaskId(), remoteTransition));
             mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(
                     /* stash= */ true);
         } else if (tag instanceof WorkspaceItemInfo) {
@@ -1233,7 +1291,8 @@
                     Intent intent = new Intent(info.getIntent())
                             .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                     try {
-                        if (mIsSafeModeEnabled && !PackageManagerHelper.isSystemApp(this, intent)) {
+                        if (mIsSafeModeEnabled
+                                && !new ApplicationInfoWrapper(this, intent).isSystem()) {
                             Toast.makeText(this, R.string.safemode_shortcut_error,
                                     Toast.LENGTH_SHORT).show();
                         } else if (info.isPromise()) {
@@ -1308,7 +1367,8 @@
             GroupTask task,
             @Nullable RemoteTransition remoteTransition,
             boolean onDesktop) {
-        handleGroupTaskLaunch(task, remoteTransition, onDesktop, null, null);
+        handleGroupTaskLaunch(task, remoteTransition, onDesktop,
+                /* onStartCallback= */ null, /* onFinishCallback= */ null);
     }
 
     /**
@@ -1332,17 +1392,23 @@
             UI_HELPER_EXECUTOR.execute(() ->
                     SystemUiProxy.INSTANCE.get(this).showDesktopApps(getDisplay().getDisplayId(),
                             remoteTransition));
-        } else if (onDesktop) {
+            return;
+        }
+        if (onDesktop) {
+            boolean useRemoteTransition = canUnminimizeDesktopTask(task.task1.key.id);
             UI_HELPER_EXECUTOR.execute(() -> {
                 if (onStartCallback != null) {
                     onStartCallback.run();
                 }
-                SystemUiProxy.INSTANCE.get(this).showDesktopApp(task.task1.key.id);
+                SystemUiProxy.INSTANCE.get(this).showDesktopApp(
+                        task.task1.key.id, useRemoteTransition ? remoteTransition : null);
                 if (onFinishCallback != null) {
                     onFinishCallback.run();
                 }
             });
-        } else if (task.task2 == null) {
+            return;
+        }
+        if (task.task2 == null) {
             UI_HELPER_EXECUTOR.execute(() -> {
                 ActivityOptions activityOptions =
                         makeDefaultActivityOptions(SPLASH_SCREEN_STYLE_UNDEFINED).options;
@@ -1351,9 +1417,23 @@
                 ActivityManagerWrapper.getInstance().startActivityFromRecents(
                         task.task1.key, activityOptions);
             });
-        } else {
-            mControllers.uiController.launchSplitTasks(task, remoteTransition);
+            return;
         }
+        mControllers.uiController.launchSplitTasks(task, remoteTransition);
+    }
+
+    /** Returns whether the given task is minimized and can be unminimized. */
+    public boolean canUnminimizeDesktopTask(int taskId) {
+        BubbleTextView.RunningAppState runningAppState =
+                mControllers.taskbarRecentAppsController.getRunningAppState(taskId);
+        return runningAppState == BubbleTextView.RunningAppState.MINIMIZED
+                && DesktopModeFlags.ENABLE_DESKTOP_APP_LAUNCH_ALTTAB_TRANSITIONS.isTrue();
+    }
+
+    private RemoteTransition createUnminimizeRemoteTransition() {
+        return new RemoteTransition(
+                new DesktopAppLaunchTransition(
+                        this, getMainExecutor(), AppLaunchType.UNMINIMIZE));
     }
 
     /**
@@ -1454,25 +1534,31 @@
                 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
         try {
             TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "start: taskbarAppIcon");
-            if (info.user.equals(Process.myUserHandle())) {
-                // TODO(b/216683257): Use startActivityForResult for search results that require it.
-                if (taskInRecents != null) {
-                    // Re launch instance from recents
-                    ActivityOptionsWrapper opts = getActivityLaunchOptions(null, info);
-                    opts.options.setLaunchDisplayId(
-                            getDisplay() == null ? DEFAULT_DISPLAY : getDisplay().getDisplayId());
-                    if (ActivityManagerWrapper.getInstance()
-                            .startActivityFromRecents(taskInRecents.key, opts.options)) {
-                        mControllers.uiController.getRecentsView()
-                                .addSideTaskLaunchCallback(opts.onEndCallback);
-                        return;
-                    }
-                }
-                startActivity(intent);
-            } else {
+            if (!info.user.equals(Process.myUserHandle())) {
+                // TODO b/376819104: support Desktop launch animations for apps in managed profiles
                 getSystemService(LauncherApps.class).startMainActivity(
                         intent.getComponent(), info.user, intent.getSourceBounds(), null);
+                return;
             }
+            // TODO(b/216683257): Use startActivityForResult for search results that require it.
+            if (taskInRecents != null) {
+                // Re launch instance from recents
+                ActivityOptionsWrapper opts = getActivityLaunchOptions(null, info);
+                opts.options.setLaunchDisplayId(
+                        getDisplay() == null ? DEFAULT_DISPLAY : getDisplay().getDisplayId());
+                if (ActivityManagerWrapper.getInstance()
+                        .startActivityFromRecents(taskInRecents.key, opts.options)) {
+                    mControllers.uiController.getRecentsView()
+                            .addSideTaskLaunchCallback(opts.onEndCallback);
+                    return;
+                }
+            }
+            ActivityOptionsWrapper opts = null;
+            if (areDesktopTasksVisible()) {
+                opts = getActivityLaunchDesktopOptions(info);
+            }
+            Bundle optionsBundle = opts == null ? null : opts.options.toBundle();
+            startActivity(intent, optionsBundle);
         } catch (NullPointerException | ActivityNotFoundException | SecurityException e) {
             Toast.makeText(this, R.string.activity_not_found, Toast.LENGTH_SHORT)
                     .show();
@@ -1530,6 +1616,7 @@
 
     /**
      * Called when we want to unstash taskbar when user performs swipes up gesture.
+     *
      * @param delayTaskbarBackground whether we will delay the taskbar background animation
      */
     public void onSwipeToUnstashTaskbar(boolean delayTaskbarBackground) {
@@ -1671,7 +1758,7 @@
                 duration);
 
         View allAppsButton = mControllers.taskbarViewController.getAllAppsButtonView();
-        if (allAppsButton != null && !FeatureFlags.enableAllAppsButtonInHotseat()) {
+        if (!FeatureFlags.enableAllAppsButtonInHotseat()) {
             ValueAnimator alphaOverride = ValueAnimator.ofFloat(0, 1);
             alphaOverride.setDuration(duration);
             alphaOverride.addUpdateListener(a -> {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
index 56fd2bb..5a63ca6 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
@@ -26,6 +26,7 @@
 import com.android.launcher3.taskbar.bubbles.BubbleControllers;
 import com.android.launcher3.taskbar.overlay.TaskbarOverlayController;
 import com.android.systemui.shared.rotation.RotationButtonController;
+import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -168,7 +169,7 @@
         taskbarOverlayController.init(this);
         taskbarAllAppsController.init(this, sharedState.allAppsVisible);
         navButtonController.init(this);
-        bubbleControllers.ifPresent(controllers -> controllers.init(this));
+        bubbleControllers.ifPresent(controllers -> controllers.init(sharedState, this));
         taskbarInsetsController.init(this);
         voiceInteractionWindowController.init(this);
         taskbarRecentAppsController.init(this);
@@ -194,11 +195,12 @@
         };
 
         if (taskbarDesktopModeController.getAreDesktopTasksVisible()) {
-            mCornerRoundness.updateValue(taskbarDesktopModeController.getTaskbarCornerRoundness(
-                    mSharedState.showCornerRadiusInDesktopMode));
+            mCornerRoundness.value = taskbarDesktopModeController.getTaskbarCornerRoundness(
+                    mSharedState.showCornerRadiusInDesktopMode);
         } else {
-            mCornerRoundness.updateValue(TaskbarBackgroundRenderer.MAX_ROUNDNESS);
+            mCornerRoundness.value = TaskbarBackgroundRenderer.MAX_ROUNDNESS;
         }
+        updateCornerRoundness();
         onPostInit();
     }
 
@@ -219,7 +221,12 @@
         uiController = newUiController;
         uiController.init(this);
         uiController.updateStateForSysuiFlags(mSharedState.sysuiStateFlags);
-
+        // if bubble controllers are present take bubble bar location, else set it to null
+        bubbleControllers.ifPresentOrElse(bubbleControllers -> {
+            BubbleBarLocation location =
+                    bubbleControllers.bubbleBarViewController.getBubbleBarLocation();
+            uiController.onBubbleBarLocationUpdated(location);
+        }, () -> uiController.onBubbleBarLocationUpdated(null));
         // Notify that the ui controller has changed
         navbarButtonsViewController.onUiControllerChanged();
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDividerPopupView.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarDividerPopupView.kt
index b5a3314..3f6ebe2 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDividerPopupView.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDividerPopupView.kt
@@ -31,21 +31,21 @@
 import android.widget.Switch
 import androidx.core.view.postDelayed
 import com.android.app.animation.Interpolators.EMPHASIZED_ACCELERATE
+import com.android.launcher3.Flags
 import com.android.launcher3.R
 import com.android.launcher3.popup.ArrowPopup
 import com.android.launcher3.popup.RoundedArrowDrawable
 import com.android.launcher3.util.DisplayController
 import com.android.launcher3.util.Themes
 import com.android.launcher3.views.ActivityContext
+import kotlin.math.max
+import kotlin.math.min
 
 /** Popup view with arrow for taskbar pinning */
 class TaskbarDividerPopupView<T : TaskbarActivityContext>
 @JvmOverloads
-constructor(
-    context: Context,
-    attrs: AttributeSet? = null,
-    defStyleAttr: Int = 0,
-) : ArrowPopup<T>(context, attrs, defStyleAttr) {
+constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
+    ArrowPopup<T>(context, attrs, defStyleAttr) {
     companion object {
         private const val TAG = "TaskbarDividerPopupView"
         private const val DIVIDER_POPUP_CLOSING_DELAY = 333L
@@ -55,24 +55,28 @@
         fun createAndPopulate(
             view: View,
             taskbarActivityContext: TaskbarActivityContext,
+            horizontalPosition: Float,
         ): TaskbarDividerPopupView<*> {
             val taskMenuViewWithArrow =
                 taskbarActivityContext.layoutInflater.inflate(
                     R.layout.taskbar_divider_popup_menu,
                     taskbarActivityContext.dragLayer,
-                    false
+                    false,
                 ) as TaskbarDividerPopupView<*>
 
-            return taskMenuViewWithArrow.populateForView(view)
+            return taskMenuViewWithArrow.populateForView(view, horizontalPosition)
         }
     }
 
     private lateinit var dividerView: View
+    private var horizontalPosition = 0.0f
 
     private val popupCornerRadius = Themes.getDialogCornerRadius(context)
     private val arrowWidth = resources.getDimension(R.dimen.popup_arrow_width)
     private val arrowHeight = resources.getDimension(R.dimen.popup_arrow_height)
     private val arrowPointRadius = resources.getDimension(R.dimen.popup_arrow_corner_radius)
+    private val minPaddingFromScreenEdge =
+        resources.getDimension(R.dimen.taskbar_pinning_popup_menu_min_padding_from_screen_edge)
 
     private var alwaysShowTaskbarOn = !DisplayController.isTransientTaskbar(context)
     private var didPreferenceChange = false
@@ -128,7 +132,36 @@
     /** Orient object as usual and then center object horizontally. */
     override fun orientAboutObject() {
         super.orientAboutObject()
-        x = mTempRect.centerX() - measuredWidth / 2f
+        x =
+            if (Flags.showTaskbarPinningPopupFromAnywhere()) {
+                val xForCenterAlignment = horizontalPosition - measuredWidth / 2f
+                val maxX = popupContainer.getWidth() - measuredWidth - minPaddingFromScreenEdge
+                when {
+                    // Left-aligned popup and its arrow pointing to the event position if there is
+                    // not enough space to center it.
+                    xForCenterAlignment < minPaddingFromScreenEdge ->
+                        max(
+                            minPaddingFromScreenEdge,
+                            horizontalPosition - mArrowOffsetHorizontal - mArrowWidth / 2,
+                        )
+
+                    // Right-aligned popup and its arrow pointing to the event position if there
+                    // is not enough space to center it.
+                    xForCenterAlignment > maxX ->
+                        min(
+                            horizontalPosition - measuredWidth +
+                                mArrowOffsetHorizontal +
+                                mArrowWidth / 2,
+                            popupContainer.getWidth() - measuredWidth - minPaddingFromScreenEdge,
+                        )
+
+                    // Default alignment where the popup and its arrow are centered relative to the
+                    // event position.
+                    else -> xForCenterAlignment
+                }
+            } else {
+                mTempRect.centerX() - measuredWidth / 2f
+            }
     }
 
     override fun onControllerInterceptTouchEvent(ev: MotionEvent?): Boolean {
@@ -142,8 +175,9 @@
         return false
     }
 
-    private fun populateForView(view: View): TaskbarDividerPopupView<*> {
+    private fun populateForView(view: View, horizontalPosition: Float): TaskbarDividerPopupView<*> {
         dividerView = view
+        this@TaskbarDividerPopupView.horizontalPosition = horizontalPosition
         tryUpdateBackground()
         return this
     }
@@ -169,15 +203,31 @@
 
     override fun addArrow() {
         super.addArrow()
-        val location = IntArray(2)
-        popupContainer.getLocationInDragLayer(dividerView, location)
-        val dividerViewX = location[0].toFloat()
-        // Change arrow location to the middle of popup.
-        mArrow.x = (dividerViewX + dividerView.width / 2) - (mArrowWidth / 2)
+        if (Flags.showTaskbarPinningPopupFromAnywhere()) {
+            mArrow.x =
+                min(
+                    max(
+                        minPaddingFromScreenEdge + mArrowOffsetHorizontal,
+                        horizontalPosition - mArrowWidth / 2,
+                    ),
+                    popupContainer.getWidth() -
+                        minPaddingFromScreenEdge -
+                        mArrowOffsetHorizontal -
+                        mArrowWidth,
+                )
+        } else {
+            val location = IntArray(2)
+            popupContainer.getLocationInDragLayer(dividerView, location)
+            val dividerViewX = location[0].toFloat()
+            // Change arrow location to the middle of popup.
+            mArrow.x = (dividerViewX + dividerView.width / 2) - (mArrowWidth / 2)
+        }
     }
 
     override fun updateArrowColor() {
-        if (!Gravity.isVertical(mGravity)) {
+        if (Flags.showTaskbarPinningPopupFromAnywhere()) {
+            super.updateArrowColor()
+        } else if (!Gravity.isVertical(mGravity)) {
             mArrow.background =
                 RoundedArrowDrawable(
                     arrowWidth,
@@ -213,7 +263,7 @@
 
     /** Aligning the view pivot to center for animation. */
     override fun setPivotForOpenCloseAnimation() {
-        pivotX = measuredWidth / 2f
+        pivotX = mArrow.x + mArrowWidth / 2 - x
         pivotY = measuredHeight.toFloat()
     }
 
@@ -227,13 +277,13 @@
             ObjectAnimator.ofFloat(
                 this,
                 TRANSLATION_Y,
-                *floatArrayOf(this.translationY, this.translationY + translateYValue)
+                *floatArrayOf(this.translationY, this.translationY + translateYValue),
             )
         val arrowTranslateY =
             ObjectAnimator.ofFloat(
                 mArrow,
                 TRANSLATION_Y,
-                *floatArrayOf(mArrow.translationY, mArrow.translationY + translateYValue)
+                *floatArrayOf(mArrow.translationY, mArrow.translationY + translateYValue),
             )
         val animatorSet = AnimatorSet()
         animatorSet.playTogether(alpha, arrowAlpha, translateY, arrowTranslateY)
@@ -243,7 +293,7 @@
     private fun getAnimatorOfFloat(
         view: View,
         property: Property<View, Float>,
-        vararg values: Float
+        vararg values: Float,
     ): Animator {
         val animator: Animator = ObjectAnimator.ofFloat(view, property, *values)
         animator.setDuration(DIVIDER_POPUP_CLOSING_ANIMATION_DURATION)
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
index a9b34d2..e16c76d 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
@@ -99,7 +99,7 @@
     public TaskbarDragLayer(@NonNull Context context, @Nullable AttributeSet attrs,
             int defStyleAttr, int defStyleRes) {
         super(context, attrs, 1 /* alphaChannelCount */);
-        mBackgroundRenderer = new TaskbarBackgroundRenderer(mActivity);
+        mBackgroundRenderer = new TaskbarBackgroundRenderer(mContainer);
 
         mTaskbarBackgroundAlpha = new MultiPropertyFactory<>(this, BG_ALPHA, INDEX_COUNT,
                 (a, b) -> a * b, 1f);
@@ -108,7 +108,7 @@
 
     public void init(TaskbarDragLayerController.TaskbarDragLayerCallbacks callbacks) {
         mControllerCallbacks = callbacks;
-        mBackgroundRenderer.updateStashedHandleWidth(mActivity, getResources());
+        mBackgroundRenderer.updateStashedHandleWidth(mContainer, getResources());
         recreateControllers();
     }
 
@@ -269,7 +269,7 @@
     @Override
     public boolean dispatchKeyEvent(KeyEvent event) {
         if (event.getAction() == ACTION_UP && event.getKeyCode() == KEYCODE_BACK) {
-            AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(mActivity);
+            AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(mContainer);
             if (topView != null && topView.canHandleBack()) {
                 topView.onBackInvoked();
                 // Handled by the floating view.
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt
index 06376d3..a89bc3a 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt
@@ -49,6 +49,7 @@
 import com.android.launcher3.util.ResourceBasedOverride
 import com.android.launcher3.views.ActivityContext
 import com.android.launcher3.views.BaseDragLayer
+import com.android.quickstep.util.ContextualSearchInvoker
 import com.android.quickstep.util.LottieAnimationColorUtils
 import java.io.PrintWriter
 
@@ -80,7 +81,11 @@
     ResourceBasedOverride, LoggableTaskbarController {
 
     protected val activityContext: TaskbarActivityContext = ActivityContext.lookupContext(context)
-    open val shouldShowSearchEdu = false
+    open val shouldShowSearchEdu: Boolean
+        get() =
+            ContextualSearchInvoker.newInstance(activityContext)
+                .runContextualSearchInvocationChecksAndLogFailures()
+
     private val isTooltipEnabled: Boolean
         get() {
             return !Utilities.isRunningInTestHarness() &&
@@ -260,7 +265,8 @@
                 !DisplayController.isPinnedTaskbar(activityContext) ||
                 !isTooltipEnabled ||
                 !shouldShowSearchEdu ||
-                userHasSeenSearchEdu
+                userHasSeenSearchEdu ||
+                !controllers.taskbarStashController.isTaskbarVisibleAndNotStashing
         ) {
             return
         }
@@ -351,19 +357,19 @@
             overlayContext.layoutInflater.inflate(
                 R.layout.taskbar_edu_tooltip,
                 overlayContext.dragLayer,
-                false
+                false,
             ) as TaskbarEduTooltip
 
         controllers.taskbarAutohideSuspendController.updateFlag(
             FLAG_AUTOHIDE_SUSPEND_EDU_OPEN,
-            true
+            true,
         )
 
         tooltip.onCloseCallback = {
             this.tooltip = null
             controllers.taskbarAutohideSuspendController.updateFlag(
                 FLAG_AUTOHIDE_SUSPEND_EDU_OPEN,
-                false
+                false,
             )
             controllers.taskbarStashController.updateAndAnimateTransientTaskbar(true)
         }
@@ -378,7 +384,7 @@
             override fun performAccessibilityAction(
                 host: View,
                 action: Int,
-                args: Bundle?
+                args: Bundle?,
             ): Boolean {
                 if (action == R.id.close) {
                     hide()
@@ -396,13 +402,13 @@
 
             override fun onInitializeAccessibilityNodeInfo(
                 host: View,
-                info: AccessibilityNodeInfo
+                info: AccessibilityNodeInfo,
             ) {
                 super.onInitializeAccessibilityNodeInfo(host, info)
                 info.addAction(
                     AccessibilityNodeInfo.AccessibilityAction(
                         R.id.close,
-                        host.context?.getText(R.string.taskbar_edu_close)
+                        host.context?.getText(R.string.taskbar_edu_close),
                     )
                 )
             }
@@ -421,7 +427,7 @@
             return ResourceBasedOverride.Overrides.getObject(
                 TaskbarEduTooltipController::class.java,
                 context,
-                R.string.taskbar_edu_tooltip_controller_class
+                R.string.taskbar_edu_tooltip_controller_class,
             )
         }
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarForceVisibleImmersiveController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarForceVisibleImmersiveController.java
index 8a86402..b7f5575 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarForceVisibleImmersiveController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarForceVisibleImmersiveController.java
@@ -108,8 +108,10 @@
     /** Clean up animations. */
     public void onDestroy() {
         startIconUndimming();
-        mControllers.navbarButtonsViewController.setHomeButtonAccessibilityDelegate(null);
-        mControllers.navbarButtonsViewController.setBackButtonAccessibilityDelegate(null);
+        if (mControllers != null) {
+            mControllers.navbarButtonsViewController.setHomeButtonAccessibilityDelegate(null);
+            mControllers.navbarButtonsViewController.setBackButtonAccessibilityDelegate(null);
+        }
     }
 
     private void startIconUndimming() {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
index 685c109..a8ce10f 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
@@ -21,7 +21,6 @@
 import android.graphics.Paint
 import android.graphics.Rect
 import android.graphics.Region
-import android.inputmethodservice.InputMethodService.ENABLE_HIDE_IME_CAPTION_BAR
 import android.os.Binder
 import android.os.IBinder
 import android.view.DisplayInfo
@@ -82,7 +81,7 @@
             context.mainThreadHandler,
             Executors.UI_HELPER_EXECUTOR.handler,
             context,
-            this::onTaskbarOrBubblebarWindowHeightOrInsetsChanged
+            this::onTaskbarOrBubblebarWindowHeightOrInsetsChanged,
         )
     private val debugTouchableRegion = DebugTouchableRegion()
 
@@ -120,7 +119,7 @@
             if (enableTaskbarNoRecreate() && controllers.sharedState != null) {
                 getProvidedInsets(
                     controllers.sharedState!!.insetsFrameProviders,
-                    insetsRoundedCornerFlag
+                    insetsRoundedCornerFlag,
                 )
             } else {
                 getProvidedInsets(insetsRoundedCornerFlag)
@@ -145,11 +144,12 @@
             // if bubble bar is visible or animating new bubble, add bar bounds to the touch region
             if (isBubbleBarVisible || isAnimatingNewBubble) {
                 defaultTouchableRegion.addBoundsToRegion(bubbleBarViewController.bubbleBarBounds)
+                defaultTouchableRegion.addBoundsToRegion(bubbleBarViewController.flyoutBounds)
             }
         }
         if (
             taskbarStashController.isInApp ||
-                taskbarStashController.isInOverview ||
+                controllers.uiController.isInOverviewUi ||
                 DisplayController.showLockedTaskbarOnHome(context)
         ) {
             // only add the taskbar touch region if not on home
@@ -181,7 +181,7 @@
      */
     private fun getProvidedInsets(
         providedInsets: Array<InsetsFrameProvider>,
-        insetsRoundedCornerFlag: Int
+        insetsRoundedCornerFlag: Int,
     ): Array<InsetsFrameProvider> {
         val navBarsFlag =
             (if (context.isGestureNav) FLAG_SUPPRESS_SCRIM else 0) or insetsRoundedCornerFlag
@@ -207,14 +207,14 @@
             InsetsFrameProvider(insetsOwner, 0, navigationBars())
                 .setFlags(
                     navBarsFlag,
-                    FLAG_SUPPRESS_SCRIM or FLAG_ANIMATE_RESIZING or FLAG_INSETS_ROUNDED_CORNER
+                    FLAG_SUPPRESS_SCRIM or FLAG_ANIMATE_RESIZING or FLAG_INSETS_ROUNDED_CORNER,
                 ),
             InsetsFrameProvider(insetsOwner, 0, tappableElement()),
             InsetsFrameProvider(insetsOwner, 0, mandatorySystemGestures()),
             InsetsFrameProvider(insetsOwner, INDEX_LEFT, systemGestures())
                 .setSource(SOURCE_DISPLAY),
             InsetsFrameProvider(insetsOwner, INDEX_RIGHT, systemGestures())
-                .setSource(SOURCE_DISPLAY)
+                .setSource(SOURCE_DISPLAY),
         )
     }
 
@@ -232,7 +232,7 @@
                 val gestureHeight =
                     ResourceUtils.getNavbarSize(
                         ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE,
-                        context.resources
+                        context.resources,
                     )
                 val isPinnedTaskbar =
                     context.deviceProfile.isTaskbarPresent &&
@@ -258,7 +258,7 @@
         // When in gesture nav, report the stashed height to the IME, to allow hiding the
         // IME navigation bar.
         val imeInsetsSize =
-            if (ENABLE_HIDE_IME_CAPTION_BAR && context.isGestureNav) {
+            if (context.isGestureNav) {
                 getInsetsForGravity(controllers.taskbarStashController.stashedHeight, gravity)
             } else {
                 getInsetsForGravity(taskbarHeightForIme, gravity)
@@ -272,8 +272,8 @@
                     // override below (insetsSizeOverrides must have the same length and
                     // types after the window is added according to
                     // WindowManagerService#relayoutWindow)
-                    provider.insetsSize
-                )
+                    provider.insetsSize,
+                ),
             )
         // Use 0 tappableElement insets for the VoiceInteractionWindow when gesture nav is enabled.
         val visInsetsSizeForTappableElement =
@@ -284,7 +284,7 @@
                 InsetsFrameProvider.InsetsSizeOverride(TYPE_INPUT_METHOD, imeInsetsSize),
                 InsetsFrameProvider.InsetsSizeOverride(
                     TYPE_VOICE_INTERACTION,
-                    visInsetsSizeForTappableElement
+                    visInsetsSizeForTappableElement,
                 ),
             )
         if (
@@ -427,7 +427,7 @@
         // Always have nav buttons be touchable
         controllers.navbarButtonsViewController.addVisibleButtonsRegion(
             context.dragLayer,
-            insetsInfo.touchableRegion
+            insetsInfo.touchableRegion,
         )
         debugTouchableRegion.lastSetTouchableBounds.set(insetsInfo.touchableRegion.bounds)
         context.excludeFromMagnificationRegion(insetsIsTouchableRegion)
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
index 876221b..fa04739 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
@@ -16,21 +16,24 @@
 package com.android.launcher3.taskbar;
 
 import static com.android.app.animation.Interpolators.EMPHASIZED;
+import static com.android.app.animation.Interpolators.FINAL_FRAME;
+import static com.android.launcher3.Flags.enableScalingRevealHomeAnimation;
 import static com.android.launcher3.Hotseat.ALPHA_CHANNEL_TASKBAR_ALIGNMENT;
 import static com.android.launcher3.Hotseat.ALPHA_CHANNEL_TASKBAR_STASH;
 import static com.android.launcher3.LauncherState.HOTSEAT_ICONS;
+import static com.android.launcher3.Utilities.isRtl;
 import static com.android.launcher3.taskbar.TaskbarStashController.FLAG_IN_APP;
 import static com.android.launcher3.taskbar.TaskbarStashController.FLAG_IN_OVERVIEW;
 import static com.android.launcher3.taskbar.TaskbarStashController.FLAG_IN_STASHED_LAUNCHER_STATE;
 import static com.android.launcher3.taskbar.TaskbarStashController.FLAG_STASHED_FOR_BUBBLES;
+import static com.android.launcher3.taskbar.TaskbarStashController.UNLOCK_TRANSITION_MEMOIZATION_MS;
 import static com.android.launcher3.taskbar.TaskbarViewController.ALPHA_INDEX_HOME;
+import static com.android.launcher3.taskbar.bubbles.BubbleBarView.FADE_IN_ANIM_ALPHA_DURATION_MS;
+import static com.android.launcher3.taskbar.bubbles.BubbleBarView.FADE_OUT_ANIM_POSITION_DURATION_MS;
 import static com.android.launcher3.util.FlagDebugUtils.appendFlag;
 import static com.android.launcher3.util.FlagDebugUtils.formatFlagChange;
+import static com.android.quickstep.util.SystemUiFlagUtils.isTaskbarHidden;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_AWAKE;
-import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_COMMUNAL_HUB_SHOWING;
-import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DEVICE_DREAMING;
-import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_WAKEFULNESS_MASK;
-import static com.android.systemui.shared.system.QuickStepContract.WAKEFULNESS_AWAKE;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -38,12 +41,15 @@
 import android.animation.ObjectAnimator;
 import android.os.SystemClock;
 import android.util.Log;
+import android.view.animation.Interpolator;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.android.app.animation.Interpolators;
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Hotseat;
 import com.android.launcher3.Hotseat.HotseatQsbAlphaId;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.QuickstepTransitionManager;
@@ -52,16 +58,19 @@
 import com.android.launcher3.anim.AnimatorListeners;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.statemanager.StateManager;
+import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.BubbleLauncherState;
 import com.android.launcher3.uioverrides.QuickstepLauncher;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.MultiPropertyFactory.MultiProperty;
 import com.android.quickstep.RecentsAnimationCallbacks;
 import com.android.quickstep.RecentsAnimationController;
+import com.android.quickstep.util.ScalingWorkspaceRevealAnim;
 import com.android.quickstep.util.SystemUiFlagUtils;
 import com.android.quickstep.views.RecentsView;
 import com.android.systemui.animation.ViewRootSync;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
+import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
 
 import java.io.PrintWriter;
 import java.util.HashMap;
@@ -151,6 +160,7 @@
     private AnimatedFloat mTaskbarAlpha;
     private AnimatedFloat mTaskbarCornerRoundness;
     private MultiProperty mTaskbarAlphaForHome;
+    private @Nullable Animator mHotseatTranslationXAnimation;
     private QuickstepLauncher mLauncher;
 
     private boolean mIsDestroyed = false;
@@ -160,7 +170,12 @@
     private boolean mSkipNextRecentsAnimEnd;
 
     // Time when FLAG_TASKBAR_HIDDEN was last cleared, SystemClock.elapsedRealtime (milliseconds).
-    private long mLastUnlockTimeMs = 0;
+    private long mLastRemoveTaskbarHiddenTimeMs = 0;
+    /**
+     * Time when FLAG_DEVICE_LOCKED was last cleared, plus
+     * {@link TaskbarStashController#UNLOCK_TRANSITION_MEMOIZATION_MS}
+     */
+    private long mLastUnlockTransitionTimeout;
 
     private @Nullable TaskBarRecentsAnimationListener mTaskBarRecentsAnimationListener;
 
@@ -168,6 +183,8 @@
 
     private boolean mShouldDelayLauncherStateAnim;
 
+    private @Nullable BubbleBarLocation mBubbleBarLocation;
+
     // We skip any view synchronizations during init/destroy.
     private boolean mCanSyncViews;
 
@@ -185,6 +202,8 @@
                     mIsQsbInline = dp.isQsbInline;
                     TaskbarLauncherStateController.this.updateIconAlphaForHome(
                             mTaskbarAlphaForHome.getValue(), ALPHA_CHANNEL_TASKBAR_ALIGNMENT);
+                    TaskbarLauncherStateController.this.onBubbleBarLocationChanged(
+                            mBubbleBarLocation, /* animate = */ false);
                 }
             };
 
@@ -357,16 +376,7 @@
 
         updateStateForFlag(FLAG_DEVICE_LOCKED, SystemUiFlagUtils.isLocked(systemUiStateFlags));
 
-        // Taskbar is hidden whenever the device is dreaming. The dreaming state includes the
-        // interactive dreams, AoD, screen off. Since the SYSUI_STATE_DEVICE_DREAMING only kicks in
-        // when the device is asleep, the second condition extends ensures that the transition from
-        // and to the WAKEFULNESS_ASLEEP state also hide the taskbar, and improves the taskbar
-        // hide/reveal animation timings. The Taskbar can show when dreaming if the glanceable hub
-        // is showing on top.
-        boolean isTaskbarHidden = (hasAnyFlag(systemUiStateFlags, SYSUI_STATE_DEVICE_DREAMING)
-                && !hasAnyFlag(systemUiStateFlags, SYSUI_STATE_COMMUNAL_HUB_SHOWING))
-                || (systemUiStateFlags & SYSUI_STATE_WAKEFULNESS_MASK) != WAKEFULNESS_AWAKE;
-        updateStateForFlag(FLAG_TASKBAR_HIDDEN, isTaskbarHidden);
+        updateStateForFlag(FLAG_TASKBAR_HIDDEN, isTaskbarHidden(systemUiStateFlags));
 
         if (applyState) {
             applyState();
@@ -462,8 +472,12 @@
             boolean onOverview = mLauncherState == LauncherState.OVERVIEW;
             boolean hotseatIconsVisible = isInLauncher && mLauncherState.areElementsVisible(
                     mLauncher, HOTSEAT_ICONS);
-            controllers.bubbleStashController.setBubblesShowingOnHome(hotseatIconsVisible);
-            controllers.bubbleStashController.setBubblesShowingOnOverview(onOverview);
+            BubbleLauncherState state = onOverview
+                    ? BubbleLauncherState.OVERVIEW
+                    : hotseatIconsVisible
+                            ? BubbleLauncherState.HOME
+                            : BubbleLauncherState.IN_APP;
+            controllers.bubbleStashController.setLauncherState(state);
         });
 
         TaskbarStashController stashController = mControllers.taskbarStashController;
@@ -524,7 +538,7 @@
 
         if (hasAnyFlag(changedFlags, FLAG_TASKBAR_HIDDEN) && !hasAnyFlag(FLAG_TASKBAR_HIDDEN)) {
             // Take note of the current time, as the taskbar is made visible again.
-            mLastUnlockTimeMs = SystemClock.elapsedRealtime();
+            mLastRemoveTaskbarHiddenTimeMs = SystemClock.elapsedRealtime();
         }
 
         boolean isHidden = hasAnyFlag(FLAG_TASKBAR_HIDDEN);
@@ -550,7 +564,8 @@
                 // with a fingerprint reader. This should only be done when the device was woken
                 // up via fingerprint reader, however since this information is currently not
                 // available, opting to always delay the fade-in a bit.
-                long durationSinceLastUnlockMs = SystemClock.elapsedRealtime() - mLastUnlockTimeMs;
+                long durationSinceLastUnlockMs = SystemClock.elapsedRealtime()
+                        - mLastRemoveTaskbarHiddenTimeMs;
                 taskbarVisibility.setStartDelay(
                         Math.max(0, TASKBAR_SHOW_DELAY_MS - durationSinceLastUnlockMs));
             }
@@ -620,6 +635,15 @@
         boolean isUnlockTransition =
                 hasAnyFlag(changedFlags, FLAG_DEVICE_LOCKED) && !hasAnyFlag(FLAG_DEVICE_LOCKED);
         if (isUnlockTransition) {
+            // the launcher might not be resumed at the time the device is considered
+            // unlocked (when the keyguard goes away), but possibly shortly afterwards.
+            // To play the unlock transition at the time the unstash animation actually happens,
+            // this memoizes the state transition for UNLOCK_TRANSITION_MEMOIZATION_MS.
+            mLastUnlockTransitionTimeout =
+                    SystemClock.elapsedRealtime() + UNLOCK_TRANSITION_MEMOIZATION_MS;
+        }
+        boolean isInUnlockTimeout = SystemClock.elapsedRealtime() < mLastUnlockTransitionTimeout;
+        if (isUnlockTransition || isInUnlockTimeout) {
             // When transitioning to unlocked, ensure the hotseat is fully visible from the
             // beginning. The hotseat itself is animated by LauncherUnlockAnimationController.
             mIconAlignment.cancelAnimation();
@@ -647,10 +671,16 @@
                         + mIconAlignment.value
                         + " -> " + toAlignment + ": " + duration);
             }
-            animatorSet.play(iconAlignAnim);
+            if (hasAnyFlag(FLAG_TASKBAR_HIDDEN)) {
+                iconAlignAnim.setInterpolator(FINAL_FRAME);
+            } else {
+                animatorSet.play(iconAlignAnim);
+            }
         }
 
-        animatorSet.setInterpolator(EMPHASIZED);
+        Interpolator interpolator = enableScalingRevealHomeAnimation()
+                ? ScalingWorkspaceRevealAnim.SCALE_INTERPOLATOR : EMPHASIZED;
+        animatorSet.setInterpolator(interpolator);
 
         if (start) {
             animatorSet.start();
@@ -749,6 +779,9 @@
     }
 
     protected void stashHotseat(boolean stash) {
+        // align taskbar with the hotseat icons before performing any animation
+        mControllers.taskbarViewController.setLauncherIconAlignment(/* alignmentRatio = */ 1,
+                mLauncher.getDeviceProfile());
         TaskbarStashController stashController = mControllers.taskbarStashController;
         stashController.updateStateForFlag(FLAG_STASHED_FOR_BUBBLES, stash);
         Runnable swapHotseatWithTaskbar = new Runnable() {
@@ -833,6 +866,64 @@
         }
     }
 
+    /** Updates launcher home screen appearance accordingly to the bubble bar location. */
+    public void onBubbleBarLocationChanged(@Nullable BubbleBarLocation location, boolean animate) {
+        mBubbleBarLocation = location;
+        if (location == null) {
+            // bubble bar is not present, hence no location, resetting the hotseat
+            updateHotseatAndQsbTranslationX(/* targetValue = */ 0, animate);
+            mBubbleBarLocation = null;
+            return;
+        }
+        DeviceProfile deviceProfile = mLauncher.getDeviceProfile();
+        if (!deviceProfile.shouldAdjustHotseatOnNavBarLocationUpdate(
+                mControllers.taskbarActivityContext)) {
+            return;
+        }
+        boolean isBubblesOnLeft = location.isOnLeft(isRtl(mLauncher.getResources()));
+        int targetX = deviceProfile
+                .getHotseatTranslationXForNavBar(mLauncher, isBubblesOnLeft);
+        updateHotseatAndQsbTranslationX(targetX, animate);
+    }
+
+    /** Used to translate hotseat and QSB to make room for bubbles. */
+    private void updateHotseatAndQsbTranslationX(float targetValue, boolean animate) {
+        // cancel existing animation
+        if (mHotseatTranslationXAnimation != null) {
+            mHotseatTranslationXAnimation.cancel();
+            mHotseatTranslationXAnimation = null;
+        }
+        Hotseat hotseat = mLauncher.getHotseat();
+        AnimatorSet translationXAnimation = new AnimatorSet();
+        MultiProperty iconsTranslationX = mLauncher.getHotseat()
+                .getIconsTranslationX(Hotseat.ICONS_TRANSLATION_X_NAV_BAR_ALIGNMENT);
+        if (animate) {
+            translationXAnimation.playTogether(iconsTranslationX.animateToValue(targetValue));
+        } else {
+            iconsTranslationX.setValue(targetValue);
+        }
+        float qsbTargetX = 0;
+        if (mIsQsbInline) {
+            qsbTargetX = targetValue;
+        }
+        MultiProperty qsbTranslationX = hotseat.getQsbTranslationX();
+        if (qsbTranslationX != null) {
+            if (animate) {
+                translationXAnimation.playTogether(qsbTranslationX.animateToValue(qsbTargetX));
+            } else {
+                qsbTranslationX.setValue(qsbTargetX);
+            }
+        }
+        if (!animate) {
+            return;
+        }
+        mHotseatTranslationXAnimation = translationXAnimation;
+        translationXAnimation.setStartDelay(FADE_OUT_ANIM_POSITION_DURATION_MS);
+        translationXAnimation.setDuration(FADE_IN_ANIM_ALPHA_DURATION_MS);
+        translationXAnimation.setInterpolator(Interpolators.EMPHASIZED);
+        translationXAnimation.start();
+    }
+
     private final class TaskBarRecentsAnimationListener implements
             RecentsAnimationCallbacks.RecentsAnimationListener {
         private final RecentsAnimationCallbacks mCallbacks;
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
index 78e7b47..0807ee9 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
@@ -16,7 +16,6 @@
 package com.android.launcher3.taskbar;
 
 import static android.content.Context.RECEIVER_NOT_EXPORTED;
-import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR;
 import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL;
 
@@ -72,7 +71,9 @@
 import com.android.quickstep.AllAppsActionManager;
 import com.android.quickstep.RecentsActivity;
 import com.android.quickstep.SystemUiProxy;
-import com.android.quickstep.util.AssistUtils;
+import com.android.quickstep.fallback.window.RecentsWindowManager;
+import com.android.quickstep.util.ContextualSearchInvoker;
+import com.android.quickstep.views.RecentsViewContainer;
 import com.android.systemui.shared.statusbar.phone.BarTransitions;
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
@@ -116,7 +117,6 @@
     private WindowManager mWindowManager;
     private FrameLayout mTaskbarRootLayout;
     private boolean mAddedWindow;
-    private boolean mIsSuspended;
     private final TaskbarNavButtonController mNavButtonController;
     private final ComponentCallbacks mComponentCallbacks;
 
@@ -132,6 +132,8 @@
 
     private TaskbarActivityContext mTaskbarActivityContext;
     private StatefulActivity mActivity;
+    private RecentsViewContainer mRecentsViewContainer;
+
     /**
      * Cache a copy here so we can initialize state whenever taskbar is recreated, since
      * this class does not get re-initialized w/ new taskbars.
@@ -173,6 +175,9 @@
                                 + "onActivityDestroyed.");
                 mActivity.removeEventCallback(EVENT_DESTROYED, this);
             }
+            if (mActivity == mRecentsViewContainer) {
+                mRecentsViewContainer = null;
+            }
             mActivity = null;
             debugWhyTaskbarNotDestroyed("clearActivity");
             if (mTaskbarActivityContext != null) {
@@ -220,7 +225,7 @@
             TaskbarNavButtonCallbacks navCallbacks,
             @NonNull DesktopVisibilityController desktopVisibilityController) {
         Display display =
-                context.getSystemService(DisplayManager.class).getDisplay(DEFAULT_DISPLAY);
+                context.getSystemService(DisplayManager.class).getDisplay(context.getDisplayId());
         mContext = context.createWindowContext(display,
                 ENABLE_TASKBAR_NAVBAR_UNIFICATION ? TYPE_NAVIGATION_BAR : TYPE_NAVIGATION_BAR_PANEL,
                 null);
@@ -250,7 +255,7 @@
                 SystemUiProxy.INSTANCE.get(mContext),
                 ContextualEduStatsManager.INSTANCE.get(mContext),
                 new Handler(),
-                AssistUtils.newInstance(mContext));
+                ContextualSearchInvoker.newInstance(mContext));
         mComponentCallbacks = new ComponentCallbacks() {
             private Configuration mOldConfig = mContext.getResources().getConfiguration();
 
@@ -405,9 +410,28 @@
         }
         mUnfoldProgressProvider.setSourceProvider(unfoldTransitionProgressProvider);
 
+        if (activity instanceof RecentsViewContainer recentsViewContainer) {
+            setRecentsViewContainer(recentsViewContainer);
+        }
+    }
+
+    /**
+     * Sets the current RecentsViewContainer, from which we create a TaskbarUIController.
+     */
+    public void setRecentsViewContainer(@NonNull RecentsViewContainer recentsViewContainer) {
+        if (mRecentsViewContainer == recentsViewContainer) {
+            return;
+        }
+        if (mRecentsViewContainer == mActivity) {
+            // When switching to RecentsWindowManager (not an Activity), the old mActivity is not
+            // destroyed, nor is there a new Activity to replace it. Thus if we don't clear it here,
+            // it will not get re-set properly if we return to the Activity (e.g. NexusLauncher).
+            mActivityOnDestroyCallback.run();
+        }
+        mRecentsViewContainer = recentsViewContainer;
         if (mTaskbarActivityContext != null) {
             mTaskbarActivityContext.setUIController(
-                    createTaskbarUIControllerForActivity(mActivity));
+                    createTaskbarUIControllerForRecentsViewContainer(mRecentsViewContainer));
         }
     }
 
@@ -430,12 +454,18 @@
     /**
      * Creates a {@link TaskbarUIController} to use while the given StatefulActivity is active.
      */
-    private TaskbarUIController createTaskbarUIControllerForActivity(StatefulActivity activity) {
-        if (activity instanceof QuickstepLauncher) {
-            return new LauncherTaskbarUIController((QuickstepLauncher) activity);
+    private TaskbarUIController createTaskbarUIControllerForRecentsViewContainer(
+            RecentsViewContainer container) {
+        if (container instanceof QuickstepLauncher quickstepLauncher) {
+            return new LauncherTaskbarUIController(quickstepLauncher);
         }
-        if (activity instanceof RecentsActivity) {
-            return new FallbackTaskbarUIController((RecentsActivity) activity);
+        // If a 3P Launcher is default, always use FallbackTaskbarUIController regardless of
+        // whether the recents container is RecentsActivity or RecentsWindowManager.
+        if (container instanceof RecentsActivity recentsActivity) {
+            return new FallbackTaskbarUIController<>(recentsActivity);
+        }
+        if (container instanceof RecentsWindowManager recentsWindowManager) {
+            return new FallbackTaskbarUIController<>(recentsWindowManager);
         }
         return TaskbarUIController.DEFAULT;
     }
@@ -447,8 +477,6 @@
      */
     @VisibleForTesting
     public synchronized void recreateTaskbar() {
-        if (mIsSuspended) return;
-
         Trace.beginSection("recreateTaskbar");
         try {
             DeviceProfile dp = mUserUnlocked ?
@@ -485,9 +513,9 @@
             mSharedState.allAppsVisible = mSharedState.allAppsVisible && isLargeScreenTaskbar;
             mTaskbarActivityContext.init(mSharedState);
 
-            if (mActivity != null) {
+            if (mRecentsViewContainer != null) {
                 mTaskbarActivityContext.setUIController(
-                    createTaskbarUIControllerForActivity(mActivity));
+                        createTaskbarUIControllerForRecentsViewContainer(mRecentsViewContainer));
             }
 
             if (enableTaskbarNoRecreate()) {
@@ -535,25 +563,25 @@
         }
     }
 
-    public void checkNavBarModes() {
+    public void checkNavBarModes(int displayId) {
         if (mTaskbarActivityContext != null) {
             mTaskbarActivityContext.checkNavBarModes();
         }
     }
 
-    public void finishBarAnimations() {
+    public void finishBarAnimations(int displayId) {
         if (mTaskbarActivityContext != null) {
             mTaskbarActivityContext.finishBarAnimations();
         }
     }
 
-    public void touchAutoDim(boolean reset) {
+    public void touchAutoDim(int displayId, boolean reset) {
         if (mTaskbarActivityContext != null) {
             mTaskbarActivityContext.touchAutoDim(reset);
         }
     }
 
-    public void transitionTo(@BarTransitions.TransitionMode int barMode,
+    public void transitionTo(int displayId, @BarTransitions.TransitionMode int barMode,
             boolean animate) {
         if (mTaskbarActivityContext != null) {
             mTaskbarActivityContext.transitionTo(barMode, animate);
@@ -634,6 +662,7 @@
      * Called when the manager is no longer needed
      */
     public void destroy() {
+        mRecentsViewContainer = null;
         debugWhyTaskbarNotDestroyed("TaskbarManager#destroy()");
         removeActivityCallbacksAndListeners();
         mTaskbarBroadcastReceiver.unregisterReceiverSafely(mContext);
@@ -664,21 +693,6 @@
         }
     }
 
-    /**
-     * 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
-    public void setSuspended(boolean isSuspended) {
-        mIsSuspended = isSuspended;
-        if (mIsSuspended) {
-            removeTaskbarRootViewFromWindow();
-        } else {
-            addTaskbarRootViewToWindow();
-        }
-    }
-
     private void addTaskbarRootViewToWindow() {
         if (enableTaskbarNoRecreate() && !mAddedWindow && mTaskbarActivityContext != null) {
             mWindowManager.addView(mTaskbarRootLayout,
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java
index bdefea6..c20617d 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java
@@ -42,7 +42,6 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 import java.util.function.Predicate;
 
 /**
@@ -196,26 +195,21 @@
         final TaskbarRecentAppsController recentAppsController =
                 mControllers.taskbarRecentAppsController;
         hotseatItemInfos = recentAppsController.updateHotseatItemInfos(hotseatItemInfos);
-        Set<Integer> runningTaskIds = recentAppsController.getRunningTaskIds();
-        Set<Integer> minimizedTaskIds = recentAppsController.getMinimizedTaskIds();
 
         if (mDeferUpdatesForSUW) {
             ItemInfo[] finalHotseatItemInfos = hotseatItemInfos;
             mDeferredUpdates = () ->
                     commitHotseatItemUpdates(finalHotseatItemInfos,
-                            recentAppsController.getShownTasks(), runningTaskIds,
-                            minimizedTaskIds);
+                            recentAppsController.getShownTasks());
         } else {
-            commitHotseatItemUpdates(hotseatItemInfos,
-                    recentAppsController.getShownTasks(), runningTaskIds, minimizedTaskIds);
+            commitHotseatItemUpdates(hotseatItemInfos, recentAppsController.getShownTasks());
         }
     }
 
-    private void commitHotseatItemUpdates(ItemInfo[] hotseatItemInfos, List<GroupTask> recentTasks,
-            Set<Integer> runningTaskIds, Set<Integer> minimizedTaskIds) {
+    private void commitHotseatItemUpdates(
+            ItemInfo[] hotseatItemInfos, List<GroupTask> recentTasks) {
         mContainer.updateHotseatItems(hotseatItemInfos, recentTasks);
-        mControllers.taskbarViewController.updateIconViewsRunningStates(
-                runningTaskIds, minimizedTaskIds);
+        mControllers.taskbarViewController.updateIconViewsRunningStates();
     }
 
     /**
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java
index 15c35b6..0f9ede9 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3.taskbar;
 
+import static android.view.MotionEvent.ACTION_UP;
+
 import static com.android.internal.app.AssistUtils.INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS;
 import static com.android.internal.app.AssistUtils.INVOCATION_TYPE_KEY;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_A11Y_BUTTON_LONGPRESS;
@@ -31,12 +33,14 @@
 import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_HOME_KEY;
 import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING;
+import static com.android.window.flags.Flags.predictiveBackThreeButtonNav;
 
 import android.content.Context;
 import android.os.Bundle;
 import android.os.Handler;
 import android.util.Log;
 import android.view.HapticFeedbackConstants;
+import android.view.KeyEvent;
 import android.view.View;
 import android.view.inputmethod.Flags;
 
@@ -51,7 +55,7 @@
 import com.android.launcher3.testing.shared.TestProtocol;
 import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.TaskUtils;
-import com.android.quickstep.util.AssistUtils;
+import com.android.quickstep.util.ContextualSearchInvoker;
 import com.android.systemui.contextualeducation.GestureType;
 import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
 
@@ -113,7 +117,7 @@
     private final SystemUiProxy mSystemUiProxy;
     private final ContextualEduStatsManager mContextualEduStatsManager;
     private final Handler mHandler;
-    private final AssistUtils mAssistUtils;
+    private final ContextualSearchInvoker mContextualSearchInvoker;
     @Nullable private StatsLogManager mStatsLogManager;
 
     private final Runnable mResetLongPress = this::resetScreenUnpin;
@@ -124,13 +128,13 @@
             SystemUiProxy systemUiProxy,
             ContextualEduStatsManager contextualEduStatsManager,
             Handler handler,
-            AssistUtils assistUtils) {
+            ContextualSearchInvoker contextualSearchInvoker) {
         mContext = context;
         mCallbacks = callbacks;
         mSystemUiProxy = systemUiProxy;
         mContextualEduStatsManager = contextualEduStatsManager;
         mHandler = handler;
-        mAssistUtils = assistUtils;
+        mContextualSearchInvoker = contextualSearchInvoker;
     }
 
     public void onButtonClick(@TaskbarButton int buttonType, View view) {
@@ -141,10 +145,7 @@
         view.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
         switch (buttonType) {
             case BUTTON_BACK:
-                logEvent(LAUNCHER_TASKBAR_BACK_BUTTON_TAP);
-                mContextualEduStatsManager.updateEduStats(/* isTrackpadGesture= */ false,
-                        GestureType.BACK);
-                executeBack();
+                executeBack(/* keyEvent */ null);
                 break;
             case BUTTON_HOME:
                 logEvent(LAUNCHER_TASKBAR_HOME_BUTTON_TAP);
@@ -182,7 +183,9 @@
 
         // 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) {
+        // There are no haptics for long pressing the back button if predictive back is enabled
+        if (buttonType != BUTTON_HOME
+                && (!predictiveBackThreeButtonNav() || buttonType != BUTTON_BACK)) {
             view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
         }
         switch (buttonType) {
@@ -320,8 +323,13 @@
         mCallbacks.onToggleOverview();
     }
 
-    private void executeBack() {
-        mSystemUiProxy.onBackPressed();
+    void executeBack(@Nullable KeyEvent keyEvent) {
+        if (keyEvent == null || (keyEvent.getAction() == ACTION_UP && !keyEvent.isCanceled())) {
+            logEvent(LAUNCHER_TASKBAR_BACK_BUTTON_TAP);
+            mContextualEduStatsManager.updateEduStats(/* isTrackpadGesture= */ false,
+                    GestureType.BACK);
+        }
+        mSystemUiProxy.onBackEvent(keyEvent);
     }
 
     private void onImeSwitcherPress() {
@@ -344,8 +352,9 @@
         if (mScreenPinned || !mAssistantLongPressEnabled) {
             return;
         }
-        // Attempt to start Assist with AssistUtils, otherwise fall back to SysUi's implementation.
-        if (!mAssistUtils.tryStartAssistOverride(INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS)) {
+        // Attempt to start Contextual Search, otherwise fall back to SysUi's implementation.
+        if (!mContextualSearchInvoker.tryStartAssistOverride(
+                INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS)) {
             Bundle args = new Bundle();
             args.putInt(INVOCATION_TYPE_KEY, INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS);
             mSystemUiProxy.startAssistant(args);
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarOverflowView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarOverflowView.java
new file mode 100644
index 0000000..712478e
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarOverflowView.java
@@ -0,0 +1,483 @@
+/*
+ * 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.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.BlendMode;
+import android.graphics.BlendModeColorFilter;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.util.FloatProperty;
+import android.util.IntProperty;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+import androidx.annotation.NonNull;
+import androidx.core.graphics.ColorUtils;
+
+import com.android.app.animation.Interpolators;
+import com.android.launcher3.R;
+import com.android.launcher3.Reorderable;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.util.MultiTranslateDelegate;
+import com.android.launcher3.util.Themes;
+import com.android.systemui.shared.recents.model.Task;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * View used as overflow icon within task bar, when the list of recent/running apps overflows the
+ * available display bounds - if display is not wide enough to show all running apps in the taskbar,
+ * this icon is added to the taskbar as an entry point to open UI that surfaces all running apps.
+ * The icon contains icon representations of up to 4 more recent tasks in overflow, stacked on top
+ * each other in counter clockwise manner (icons of tasks partially overlapping with each other).
+ */
+public class TaskbarOverflowView extends FrameLayout implements Reorderable {
+    private static final int ALPHA_TRANSPARENT = 0;
+    private static final int ALPHA_OPAQUE = 255;
+    private static final long ANIMATION_DURATION_APPS_TO_LEAVE_BEHIND = 300L;
+    private static final long ANIMATION_DURATION_LEAVE_BEHIND_TO_APPS = 500L;
+    private static final long ANIMATION_SET_DURATION = 1000L;
+    private static final long ITEM_ICON_CENTER_OFFSET_ANIMATION_DURATION = 500L;
+    private static final long ITEM_ICON_COLOR_FILTER_OPACITY_ANIMATION_DURATION = 600L;
+    private static final long ITEM_ICON_SIZE_ANIMATION_DURATION = 500L;
+    private static final long ITEM_ICON_STROKE_WIDTH_ANIMATION_DURATION = 500L;
+    private static final long LEAVE_BEHIND_ANIMATIONS_DELAY = 500L;
+    private static final long LEAVE_BEHIND_OPACITY_ANIMATION_DURATION = 100L;
+    private static final long LEAVE_BEHIND_SIZE_ANIMATION_DURATION = 500L;
+    private static final int MAX_ITEMS_IN_PREVIEW = 4;
+
+    private static final FloatProperty<TaskbarOverflowView> ITEM_ICON_CENTER_OFFSET =
+            new FloatProperty<>("itemIconCenterOffset") {
+                @Override
+                public Float get(TaskbarOverflowView view) {
+                    return view.mItemIconCenterOffset;
+                }
+
+                @Override
+                public void setValue(TaskbarOverflowView view, float value) {
+                    view.mItemIconCenterOffset = value;
+                    view.invalidate();
+                }
+            };
+
+    private static final IntProperty<TaskbarOverflowView> ITEM_ICON_COLOR_FILTER_OPACITY =
+            new IntProperty<>("itemIconColorFilterOpacity") {
+                @Override
+                public Integer get(TaskbarOverflowView view) {
+                    return view.mItemIconColorFilterOpacity;
+                }
+
+                @Override
+                public void setValue(TaskbarOverflowView view, int value) {
+                    view.mItemIconColorFilterOpacity = value;
+                    view.invalidate();
+                }
+            };
+
+    private static final FloatProperty<TaskbarOverflowView> ITEM_ICON_SIZE =
+            new FloatProperty<>("itemIconSize") {
+                @Override
+                public Float get(TaskbarOverflowView view) {
+                    return view.mItemIconSize;
+                }
+
+                @Override
+                public void setValue(TaskbarOverflowView view, float value) {
+                    view.mItemIconSize = value;
+                    view.invalidate();
+                }
+            };
+
+    private static final FloatProperty<TaskbarOverflowView> ITEM_ICON_STROKE_WIDTH =
+            new FloatProperty<>("itemIconStrokeWidth") {
+                @Override
+                public Float get(TaskbarOverflowView view) {
+                    return view.mItemIconStrokeWidth;
+                }
+
+                @Override
+                public void setValue(TaskbarOverflowView view, float value) {
+                    view.mItemIconStrokeWidth = value;
+                    view.invalidate();
+                }
+            };
+
+    private static final IntProperty<TaskbarOverflowView> LEAVE_BEHIND_OPACITY =
+            new IntProperty<>("leaveBehindOpacity") {
+                @Override
+                public Integer get(TaskbarOverflowView view) {
+                    return view.mLeaveBehindOpacity;
+                }
+
+                @Override
+                public void setValue(TaskbarOverflowView view, int value) {
+                    view.mLeaveBehindOpacity = value;
+                    view.invalidate();
+                }
+            };
+
+    private static final FloatProperty<TaskbarOverflowView> LEAVE_BEHIND_SIZE =
+            new FloatProperty<>("leaveBehindSize") {
+                @Override
+                public Float get(TaskbarOverflowView view) {
+                    return view.mLeaveBehindSize;
+                }
+
+                @Override
+                public void setValue(TaskbarOverflowView view, float value) {
+                    view.mLeaveBehindSize = value;
+                    view.invalidate();
+                }
+            };
+
+    private boolean mIsRtlLayout;
+    private final List<Task> mItems = new ArrayList<Task>();
+    private int mIconSize;
+    private int mPadding;
+    private Paint mItemBackgroundPaint;
+    private final MultiTranslateDelegate mTranslateDelegate = new MultiTranslateDelegate(this);
+    private float mScaleForReorderBounce = 1f;
+    private int mItemBackgroundColor;
+    private int mLeaveBehindColor;
+
+    // Active means the overflow icon has been pressed, which replaces the app icons with the
+    // leave-behind circle and shows the KQS UI.
+    private boolean mIsActive = false;
+    private ValueAnimator mStateTransitionAnimationWrapper;
+
+    private float mItemIconCenterOffsetDefault;
+    private float mItemIconCenterOffset;  // [0..mItemIconCenterOffsetDefault]
+    private int mItemIconColorFilterOpacity;  // [ALPHA_TRANSPARENT..ALPHA_OPAQUE]
+    private float mItemIconSizeDefault;
+    private float mItemIconSizeScaledDown;
+    private float mItemIconSize;  // [mItemIconSizeScaledDown..mItemIconSizeDefault]
+    private float mItemIconStrokeWidthDefault;
+    private float mItemIconStrokeWidth;  // [0..mItemIconStrokeWidthDefault]
+    private int mLeaveBehindOpacity;  // [ALPHA_TRANSPARENT..ALPHA_OPAQUE]
+    private float mLeaveBehindSizeScaledDown;
+    private float mLeaveBehindSizeDefault;
+    private float mLeaveBehindSize;  // [mLeaveBehindSizeScaledDown..mLeaveBehindSizeDefault]
+
+    public TaskbarOverflowView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init();
+    }
+
+    public TaskbarOverflowView(Context context) {
+        super(context);
+        init();
+    }
+
+    /**
+     * Inflates the taskbar overflow button view.
+     * @param resId The resource to inflate the view from.
+     * @param group The parent view.
+     * @param iconSize The size of the overflow button icon.
+     * @param padding The internal padding of the overflow view.
+     * @return A taskbar overflow button.
+     */
+    public static TaskbarOverflowView inflateIcon(int resId, ViewGroup group, int iconSize,
+            int padding) {
+        LayoutInflater inflater = LayoutInflater.from(group.getContext());
+        TaskbarOverflowView icon = (TaskbarOverflowView) inflater.inflate(resId, group, false);
+
+        icon.mIconSize = iconSize;
+        icon.mPadding = padding;
+
+        final float radius = iconSize / 2f - padding;
+        final float size = radius + icon.mItemIconStrokeWidth;
+        icon.mItemIconCenterOffsetDefault = radius - size / 2 - icon.mItemIconStrokeWidth;
+        icon.mItemIconCenterOffset = icon.mItemIconCenterOffsetDefault;
+
+        return icon;
+    }
+
+    private void init() {
+        mIsRtlLayout = Utilities.isRtl(getResources());
+        mItemBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+        mItemBackgroundColor = getContext().getColor(R.color.taskbar_background);
+        mLeaveBehindColor = Themes.getAttrColor(getContext(), android.R.attr.textColorTertiary);
+
+        mItemIconSizeDefault = getResources().getDimension(
+                R.dimen.taskbar_overflow_item_icon_size_default);
+        mItemIconSizeScaledDown = getResources().getDimension(
+                R.dimen.taskbar_overflow_item_icon_size_scaled_down);
+        mItemIconSize = mItemIconSizeDefault;
+
+        mItemIconStrokeWidthDefault = getResources().getDimension(
+                R.dimen.taskbar_overflow_item_icon_stroke_width_default);
+        mItemIconStrokeWidth = mItemIconStrokeWidthDefault;
+
+        mLeaveBehindSizeDefault = getResources().getDimension(
+                R.dimen.taskbar_overflow_leave_behind_size_default);
+        mLeaveBehindSizeScaledDown = getResources().getDimension(
+                R.dimen.taskbar_overflow_leave_behind_size_scaled_down);
+        mLeaveBehindSize = mLeaveBehindSizeScaledDown;
+
+        setWillNotDraw(false);
+    }
+
+    @Override
+    protected void onDraw(@NonNull Canvas canvas) {
+        super.onDraw(canvas);
+
+        drawAppIcons(canvas);
+        drawLeaveBehindCircle(canvas);
+    }
+
+    private void drawAppIcons(@NonNull Canvas canvas) {
+        mItemBackgroundPaint.setColor(mItemBackgroundColor);
+        float radius = mIconSize / 2f - mPadding;
+        int adjustedItemIconSize = Math.round(mItemIconSize);
+
+        int itemsToShow = Math.min(mItems.size(), MAX_ITEMS_IN_PREVIEW);
+        for (int i = itemsToShow - 1; i >= 0; --i) {
+            Drawable icon = mItems.get(mItems.size() - i - 1).icon;
+            if (icon == null) {
+                continue;
+            }
+
+            float itemCenterX = getItemXOffset(mItemIconCenterOffset, mIsRtlLayout, i, itemsToShow);
+            float itemCenterY = getItemYOffset(mItemIconCenterOffset, i, itemsToShow);
+
+            Drawable iconCopy = icon.getConstantState().newDrawable().mutate();
+            iconCopy.setBounds(0, 0, adjustedItemIconSize, adjustedItemIconSize);
+            iconCopy.setColorFilter(new BlendModeColorFilter(
+                    ColorUtils.setAlphaComponent(mLeaveBehindColor, mItemIconColorFilterOpacity),
+                    BlendMode.SRC_ATOP));
+
+            canvas.save();
+            float itemIconRadius = adjustedItemIconSize / 2f;
+            canvas.translate(
+                    mPadding + itemCenterX + radius - itemIconRadius,
+                    mPadding + itemCenterY + radius - itemIconRadius);
+            canvas.drawCircle(itemIconRadius, itemIconRadius,
+                    itemIconRadius + mItemIconStrokeWidth, mItemBackgroundPaint);
+            iconCopy.draw(canvas);
+            canvas.restore();
+        }
+    }
+
+    private void drawLeaveBehindCircle(@NonNull Canvas canvas) {
+        mItemBackgroundPaint.setColor(
+                ColorUtils.setAlphaComponent(mLeaveBehindColor, mLeaveBehindOpacity));
+
+        final float xyCenter = mIconSize / 2f;
+        canvas.drawCircle(xyCenter, xyCenter, mLeaveBehindSize / 2f, mItemBackgroundPaint);
+    }
+
+    /**
+     * Clears the list of tasks tracked by the view.
+     */
+    public void clearItems() {
+        mItems.clear();
+        invalidate();
+    }
+
+    /**
+     * Update the view to represent a new list of recent tasks.
+     * @param items Items to be shown in the view.
+     */
+    public void setItems(List<Task> items) {
+        mItems.clear();
+        mItems.addAll(items);
+        invalidate();
+    }
+
+    /**
+     * Called when a task is updated. If the task is contained within the view, it's cached value
+     * gets updated. If the task is shown within the icon, invalidates the view, so the task icon
+     * gets updated.
+     * @param task The updated task.
+     */
+    public void updateTaskIsShown(Task task) {
+        for (int i = 0; i < mItems.size(); ++i) {
+            if (mItems.get(i).key.id == task.key.id) {
+                mItems.set(i, task);
+                if (i >= mItems.size() - MAX_ITEMS_IN_PREVIEW) {
+                    invalidate();
+                }
+                break;
+            }
+        }
+    }
+
+    /**
+     * Returns the view's state (whether it shows a set of app icons or a leave-behind circle).
+     */
+    public boolean getIsActive() {
+        return mIsActive;
+    }
+
+    /**
+     * Updates the view's state to draw either a set of app icons or a leave-behind circle.
+     * @param isActive The next state of the view.
+     */
+    public void setIsActive(boolean isActive) {
+        if (mIsActive == isActive) {
+            return;
+        }
+        mIsActive = isActive;
+
+        if (mStateTransitionAnimationWrapper != null
+                && mStateTransitionAnimationWrapper.isRunning()) {
+            mStateTransitionAnimationWrapper.reverse();
+            return;
+        }
+
+        final AnimatorSet stateTransitionAnimation = getStateTransitionAnimation();
+        mStateTransitionAnimationWrapper = ValueAnimator.ofFloat(0, 1f);
+        mStateTransitionAnimationWrapper.setDuration(mIsActive
+                ? ANIMATION_DURATION_APPS_TO_LEAVE_BEHIND
+                : ANIMATION_DURATION_LEAVE_BEHIND_TO_APPS);
+        mStateTransitionAnimationWrapper.setInterpolator(
+                mIsActive ? Interpolators.STANDARD : Interpolators.EMPHASIZED);
+        mStateTransitionAnimationWrapper.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mStateTransitionAnimationWrapper = null;
+            }
+        });
+        mStateTransitionAnimationWrapper.addUpdateListener(
+                new ValueAnimator.AnimatorUpdateListener() {
+                    @Override
+                    public void onAnimationUpdate(ValueAnimator animator) {
+                        stateTransitionAnimation.setCurrentPlayTime(
+                                (long) (ANIMATION_SET_DURATION * animator.getAnimatedFraction()));
+                    }
+                });
+        mStateTransitionAnimationWrapper.start();
+    }
+
+    private AnimatorSet getStateTransitionAnimation() {
+        final AnimatorSet animation = new AnimatorSet();
+        animation.setInterpolator(Interpolators.LINEAR);
+        animation.playTogether(
+                buildAnimator(ITEM_ICON_CENTER_OFFSET, 0f, mItemIconCenterOffsetDefault,
+                        ITEM_ICON_CENTER_OFFSET_ANIMATION_DURATION, 0L,
+                        ITEM_ICON_CENTER_OFFSET_ANIMATION_DURATION),
+                buildAnimator(ITEM_ICON_COLOR_FILTER_OPACITY, ALPHA_OPAQUE, ALPHA_TRANSPARENT,
+                        ITEM_ICON_COLOR_FILTER_OPACITY_ANIMATION_DURATION, 0L,
+                        ANIMATION_SET_DURATION - ITEM_ICON_COLOR_FILTER_OPACITY_ANIMATION_DURATION),
+                buildAnimator(ITEM_ICON_SIZE, mItemIconSizeScaledDown, mItemIconSizeDefault,
+                        ITEM_ICON_SIZE_ANIMATION_DURATION, 0L,
+                        ITEM_ICON_SIZE_ANIMATION_DURATION),
+                buildAnimator(ITEM_ICON_STROKE_WIDTH, 0f, mItemIconStrokeWidthDefault,
+                        ITEM_ICON_STROKE_WIDTH_ANIMATION_DURATION, 0L,
+                        ITEM_ICON_STROKE_WIDTH_ANIMATION_DURATION),
+                buildAnimator(LEAVE_BEHIND_OPACITY, ALPHA_OPAQUE, ALPHA_TRANSPARENT,
+                        LEAVE_BEHIND_OPACITY_ANIMATION_DURATION, LEAVE_BEHIND_ANIMATIONS_DELAY,
+                        ANIMATION_SET_DURATION - LEAVE_BEHIND_ANIMATIONS_DELAY
+                                - LEAVE_BEHIND_OPACITY_ANIMATION_DURATION),
+                buildAnimator(LEAVE_BEHIND_SIZE, mLeaveBehindSizeDefault,
+                        mLeaveBehindSizeScaledDown, LEAVE_BEHIND_SIZE_ANIMATION_DURATION,
+                        LEAVE_BEHIND_ANIMATIONS_DELAY, 0L)
+        );
+        return animation;
+    }
+
+    private ObjectAnimator buildAnimator(IntProperty<TaskbarOverflowView> property,
+            int finalValueWhenAnimatingToLeaveBehind, int finalValueWhenAnimatingToAppIcons,
+            long duration, long delayWhenAnimatingToLeaveBehind,
+            long delayWhenAnimatingToAppIcons) {
+        final ObjectAnimator animator = ObjectAnimator.ofInt(this, property,
+                mIsActive ? finalValueWhenAnimatingToLeaveBehind
+                        : finalValueWhenAnimatingToAppIcons);
+        applyTiming(animator, duration, delayWhenAnimatingToLeaveBehind,
+                delayWhenAnimatingToAppIcons);
+        return animator;
+    }
+
+    private ObjectAnimator buildAnimator(FloatProperty<TaskbarOverflowView> property,
+            float finalValueWhenAnimatingToLeaveBehind, float finalValueWhenAnimatingToAppIcons,
+            long duration, long delayWhenAnimatingToLeaveBehind,
+            long delayWhenAnimatingToAppIcons) {
+        final ObjectAnimator animator = ObjectAnimator.ofFloat(this, property,
+                mIsActive ? finalValueWhenAnimatingToLeaveBehind
+                        : finalValueWhenAnimatingToAppIcons);
+        applyTiming(animator, duration, delayWhenAnimatingToLeaveBehind,
+                delayWhenAnimatingToAppIcons);
+        return animator;
+    }
+
+    private void applyTiming(ObjectAnimator animator, long duration,
+            long delayWhenAnimatingToLeaveBehind,
+            long delayWhenAnimatingToAppIcons) {
+        animator.setDuration(duration);
+        animator.setStartDelay(
+                mIsActive ? delayWhenAnimatingToLeaveBehind : delayWhenAnimatingToAppIcons);
+    }
+
+    @Override
+    public MultiTranslateDelegate getTranslateDelegate() {
+        return mTranslateDelegate;
+    }
+
+    @Override
+    public float getReorderBounceScale() {
+        return mScaleForReorderBounce;
+    }
+
+    @Override
+    public void setReorderBounceScale(float scale) {
+        mScaleForReorderBounce = scale;
+        super.setScaleX(scale);
+        super.setScaleY(scale);
+    }
+
+    private float getItemXOffset(float baseOffset, boolean isRtl, int itemIndex, int itemCount) {
+        // Item with index 1 is on the left in all cases.
+        if (itemIndex == 1) {
+            return (isRtl ? 1 : -1) * baseOffset;
+        }
+
+        // First item is centered if total number of items shown is 3, on the right otherwise.
+        if (itemIndex == 0) {
+            if (itemCount == 3) {
+                return 0;
+            }
+            return (isRtl ? -1 : 1) * baseOffset;
+        }
+
+        // Last item is on the right when there are more than 2 items (case which is already handled
+        // as `itemIndex == 1`).
+        if (itemIndex == itemCount - 1) {
+            return (isRtl ? -1 : 1) * baseOffset;
+        }
+
+        return (isRtl ? 1 : -1) * baseOffset;
+    }
+
+    private float getItemYOffset(float baseOffset, int itemIndex, int itemCount) {
+        // If icon contains two items, they are both centered vertically.
+        if (itemCount == 2) {
+            return 0;
+        }
+        // First half of items is on top, later half is on bottom.
+        return (itemIndex + 1 <= itemCount / 2 ? -1 : 1) * baseOffset;
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarPinningController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarPinningController.kt
index 1867cd0..bcfc718 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarPinningController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarPinningController.kt
@@ -30,6 +30,7 @@
 import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_UNPINNED
 import com.android.launcher3.taskbar.TaskbarDividerPopupView.Companion.createAndPopulate
 import java.io.PrintWriter
+import kotlin.jvm.optionals.getOrNull
 
 /** Controls taskbar pinning through a popup view. */
 class TaskbarPinningController(private val context: TaskbarActivityContext) :
@@ -76,10 +77,10 @@
             }
     }
 
-    fun showPinningView(view: View) {
+    fun showPinningView(view: View, horizontalPosition: Float = -1f) {
         context.isTaskbarWindowFullscreen = true
         view.post {
-            val popupView = getPopupView(view)
+            val popupView = getPopupView(view, horizontalPosition)
             popupView.requestFocus()
             popupView.onCloseCallback = onCloseCallback
             context.onPopupVisibilityChanged(true)
@@ -89,8 +90,8 @@
     }
 
     @VisibleForTesting
-    fun getPopupView(view: View): TaskbarDividerPopupView<*> {
-        return createAndPopulate(view, context)
+    fun getPopupView(view: View, horizontalPosition: Float = -1f): TaskbarDividerPopupView<*> {
+        return createAndPopulate(view, context, horizontalPosition)
     }
 
     @VisibleForTesting
@@ -119,7 +120,11 @@
             taskbarViewController.taskbarIconScaleForPinning.animateToValue(animateToValue),
             taskbarViewController.taskbarIconTranslationXForPinning.animateToValue(animateToValue),
         )
-
+        controllers.bubbleControllers.getOrNull()?.bubbleBarViewController?.let {
+            // if bubble bar is not visible no need to add it`s animations
+            if (!it.isBubbleBarVisible) return@let
+            animatorSet.playTogether(it.bubbleBarPinning.animateToValue(animateToValue))
+        }
         animatorSet.interpolator = Interpolators.EMPHASIZED
         return animatorSet
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
index 57d4dbb..3d57de4 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
@@ -16,8 +16,9 @@
 package com.android.launcher3.taskbar
 
 import android.content.Context
-import android.window.flags.DesktopModeFlags
+import android.window.DesktopModeFlags
 import androidx.annotation.VisibleForTesting
+import com.android.launcher3.BubbleTextView.RunningAppState
 import com.android.launcher3.Flags.enableRecentsInTaskbar
 import com.android.launcher3.model.data.ItemInfo
 import com.android.launcher3.model.data.TaskItemInfo
@@ -72,6 +73,43 @@
     var shownTasks: List<GroupTask> = emptyList()
         private set
 
+    /**
+     * Returns the state of the most active Desktop task represented by the given [ItemInfo].
+     *
+     * If there are several tasks represented by the same [ItemInfo] we return the most active one,
+     * i.e. we return [DesktopAppState.RUNNING] over [DesktopAppState.MINIMIZED], and
+     * [DesktopAppState.MINIMIZED] over [DesktopAppState.NOT_RUNNING].
+     */
+    fun getDesktopItemState(itemInfo: ItemInfo?): RunningAppState {
+        val packageName = itemInfo?.getTargetPackage() ?: return RunningAppState.NOT_RUNNING
+        return getDesktopAppState(packageName, itemInfo.user.identifier)
+    }
+
+    private fun getDesktopAppState(packageName: String, userId: Int): RunningAppState {
+        val tasks = desktopTask?.tasks ?: return RunningAppState.NOT_RUNNING
+        val appTasks =
+            tasks.filter { task ->
+                packageName == task.key.packageName && task.key.userId == userId
+            }
+        if (appTasks.find { getRunningAppState(it.key.id) == RunningAppState.RUNNING } != null) {
+            return RunningAppState.RUNNING
+        }
+        if (appTasks.find { getRunningAppState(it.key.id) == RunningAppState.MINIMIZED } != null) {
+            return RunningAppState.MINIMIZED
+        }
+        return RunningAppState.NOT_RUNNING
+    }
+
+    /** Get the [RunningAppState] for the given task. */
+    fun getRunningAppState(taskId: Int): RunningAppState {
+        return when (taskId) {
+            in minimizedTaskIds -> RunningAppState.MINIMIZED
+            in runningTaskIds -> RunningAppState.RUNNING
+            else -> RunningAppState.NOT_RUNNING
+        }
+    }
+
+    @VisibleForTesting
     val runningTaskIds: Set<Int>
         /**
          * Returns the task IDs of apps that should be indicated as "running" to the user.
@@ -88,6 +126,7 @@
             return tasks.map { task -> task.key.id }.toSet()
         }
 
+    @VisibleForTesting
     val minimizedTaskIds: Set<Int>
         /**
          * Returns the task IDs for the tasks that should be indicated as "minimized" to the user.
@@ -251,7 +290,7 @@
         // Remove any newly-missing Tasks, and actual group-tasks
         val newShownTasks =
             shownTasks
-                .filter { !it.hasMultipleTasks() }
+                .filter { !it.supportsMultipleTasks() }
                 .filter { it.task1.key.id in desktopTaskIds }
                 .toMutableList()
         // Add any new Tasks, maintaining the order from previous shownTasks.
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimViewController.java
index 751a42a..4a7e4f0 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimViewController.java
@@ -27,6 +27,8 @@
 import android.view.animation.Interpolator;
 import android.view.animation.PathInterpolator;
 
+import androidx.annotation.VisibleForTesting;
+
 import com.android.launcher3.anim.AnimatedFloat;
 import com.android.launcher3.taskbar.bubbles.BubbleControllers;
 import com.android.launcher3.util.DisplayController;
@@ -77,7 +79,7 @@
     public void onTaskbarVisibilityChanged(int visibility) {
         mTaskbarVisible = visibility == VISIBLE;
         if (shouldShowScrim()) {
-            showScrim(true, getScrimAlpha(), false /* skipAnim */);
+            showScrim(true, computeScrimAlpha(), false /* skipAnim */);
         } else if (mScrimView.getScrimAlpha() > 0f) {
             showScrim(false, 0, false /* skipAnim */);
         }
@@ -96,7 +98,7 @@
             return;
         }
         mSysUiStateFlags = stateFlags;
-        showScrim(shouldShowScrim(), getScrimAlpha(), skipAnim);
+        showScrim(shouldShowScrim(), computeScrimAlpha(), skipAnim);
     }
 
     private boolean shouldShowScrim() {
@@ -119,7 +121,7 @@
                 && !mControllers.taskbarStashController.isHiddenForBubbles();
     }
 
-    private float getScrimAlpha() {
+    private float computeScrimAlpha() {
         final boolean isPersistentTaskBarVisible =
                 mTaskbarVisible && !DisplayController.isTransientTaskbar(mScrimView.getContext());
         final boolean manageMenuExpanded =
@@ -140,7 +142,7 @@
         mScrimView.setOnClickListener(showScrim ? (view) -> onClick() : null);
         mScrimView.setClickable(showScrim);
         if (skipAnim) {
-            mScrimView.setScrimAlpha(alpha);
+            mScrimAlpha.updateValue(alpha);
         } else {
             ObjectAnimator anim = mScrimAlpha.animateToValue(showScrim ? alpha : 0);
             anim.setInterpolator(showScrim ? SCRIM_ALPHA_IN : SCRIM_ALPHA_OUT);
@@ -153,7 +155,7 @@
     }
 
     private void onClick() {
-        SystemUiProxy.INSTANCE.get(mActivity).onBackPressed();
+        SystemUiProxy.INSTANCE.get(mActivity).onBackEvent(null);
     }
 
     @Override
@@ -167,4 +169,14 @@
 
         pw.println(prefix + "\tmScrimAlpha.value=" + mScrimAlpha.value);
     }
+
+    @VisibleForTesting
+    TaskbarScrimView getScrimView() {
+        return mScrimView;
+    }
+
+    @VisibleForTesting
+    float getScrimAlpha() {
+        return mScrimAlpha.value;
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarSharedState.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarSharedState.java
index 729cbe9..a64dab1 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarSharedState.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarSharedState.java
@@ -30,6 +30,10 @@
 import android.view.InsetsFrameProvider;
 
 import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
+import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
+import com.android.wm.shell.shared.bubbles.BubbleInfo;
+
+import java.util.List;
 
 /**
  * State shared across different taskbar instance
@@ -69,6 +73,15 @@
 
     public boolean allAppsVisible = false;
 
+    public BubbleBarLocation bubbleBarLocation;
+
+    public List<BubbleInfo> bubbleInfoItems;
+
+    /** Returns whether there are a saved bubbles. */
+    public boolean hasSavedBubbles() {
+        return bubbleInfoItems != null && !bubbleInfoItems.isEmpty();
+    }
+
     // LauncherTaskbarUIController#mTaskbarInAppDisplayProgressMultiProp
     public float[] inAppDisplayProgressMultiPropValues = new float[DISPLAY_PROGRESS_COUNT];
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
index 266f384..c1dd216 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
@@ -30,6 +30,7 @@
 import static com.android.launcher3.util.FlagDebugUtils.appendFlag;
 import static com.android.launcher3.util.FlagDebugUtils.formatFlagChange;
 import static com.android.quickstep.util.SystemActionConstants.SYSTEM_ACTION_ID_TASKBAR;
+import static com.android.quickstep.util.SystemUiFlagUtils.isTaskbarHidden;
 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;
@@ -61,6 +62,7 @@
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.R;
 import com.android.launcher3.anim.AnimatedFloat;
+import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.AnimatorListeners;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.MultiPropertyFactory.MultiProperty;
@@ -81,6 +83,8 @@
     private static final String TAG = "TaskbarStashController";
     private static final boolean DEBUG = false;
 
+    private static boolean sEnableSoftwareImeForTests = false;
+
     /**
      * Def. value for @param shouldBubblesFollow in
      * {@link #updateAndAnimateTransientTaskbar(boolean)} */
@@ -101,6 +105,7 @@
     // An internal no-op flag to determine whether we should delay the taskbar background animation
     private static final int FLAG_DELAY_TASKBAR_BG_TAG = 1 << 12;
     public static final int FLAG_STASHED_FOR_BUBBLES = 1 << 13; // show handle for stashed hotseat
+    public static final int FLAG_TASKBAR_HIDDEN = 1 << 14; // taskbar hidden during dream, etc...
 
     // If any of these flags are enabled, isInApp should return true.
     private static final int FLAGS_IN_APP = FLAG_IN_APP | FLAG_IN_SETUP;
@@ -130,19 +135,22 @@
      *
      * Use {@link #getStashDuration()} to query duration
      */
-    private static final long TASKBAR_STASH_DURATION = InsetsController.ANIMATION_DURATION_RESIZE;
+    @VisibleForTesting
+    static final long TASKBAR_STASH_DURATION = InsetsController.ANIMATION_DURATION_RESIZE;
 
     /**
      * How long to stash/unstash transient taskbar.
      *
      * Use {@link #getStashDuration()} to query duration.
      */
-    private static final long TRANSIENT_TASKBAR_STASH_DURATION = 417;
+    @VisibleForTesting
+    static final long TRANSIENT_TASKBAR_STASH_DURATION = 417;
 
     /**
      * How long to stash/unstash when keyboard is appearing/disappearing.
      */
-    private static final long TASKBAR_STASH_DURATION_FOR_IME = 80;
+    @VisibleForTesting
+    static final long TASKBAR_STASH_DURATION_FOR_IME = 80;
 
     /**
      * The scale TaskbarView animates to when being stashed.
@@ -163,7 +171,7 @@
     /**
      * How long the icon/stash handle alpha animation plays.
      */
-    public static final long TASKBAR_STASH_ALPHA_DURATION = 50;
+    public static final long TRANSIENT_TASKBAR_STASH_ALPHA_DURATION = 50;
 
     /**
      * How long to delay the icon/stash handle alpha for the home to app taskbar animation.
@@ -185,7 +193,7 @@
 
     // Duration for which an unlock event is considered "current", as other events are received
     // asynchronously.
-    private static final long UNLOCK_TRANSITION_MEMOIZATION_MS = 200;
+    public static final long UNLOCK_TRANSITION_MEMOIZATION_MS = 200;
 
     /**
      * The default stash animation, morphing the taskbar into the navbar.
@@ -209,6 +217,13 @@
      */
     private static final int TRANSITION_UNSTASH_SUW_MANUAL = 3;
 
+    /**
+     * total duration of entering dream state animation, which we use as start delay to
+     * applyState() when SYSUI_STATE_DEVICE_DREAMING flag is present. Keep this in sync with
+     * DreamAnimationController.TOTAL_ANIM_DURATION.
+     */
+    private static final int SKIP_TOTAL_DREAM_ANIM_DURATION = 450;
+
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(value = {
             TRANSITION_DEFAULT,
@@ -252,7 +267,7 @@
     private boolean mEnableBlockingTimeoutDuringTests = false;
 
     private Animator mTaskbarBackgroundAlphaAnimator;
-    private long mTaskbarBackgroundDuration;
+    private final long mTaskbarBackgroundDuration;
     private boolean mUserIsNotGoingHome = false;
 
     // Evaluate whether the handle should be stashed
@@ -395,12 +410,20 @@
         return mIsStashed;
     }
 
-    /** Sets the hotseat stashed. */
+    /**
+     * Sets the hotseat stashed.
+     * b/373429249 - we might change this behavior if we remove the scrim, that's why we're keeping
+     * this method
+     */
     public void stashHotseat(boolean stash) {
         mControllers.uiController.stashHotseat(stash);
     }
 
-    /** Instantly un-stashes the hotseat. */
+    /**
+     * Instantly un-stashes the hotseat.
+     * * b/373429249 - we might change this behavior if we remove the scrim, that's why we're
+     * keeping this method
+     */
     public void unStashHotseatInstantly() {
         mControllers.uiController.unStashHotseatInstantly();
     }
@@ -444,6 +467,11 @@
         return hasAnyFlag(FLAG_IN_OVERVIEW);
     }
 
+    /** Returns whether the taskbar is currently on launcher home screen. */
+    public boolean isOnHome() {
+        return !isInOverview() && !isInApp();
+    }
+
     /** Returns whether taskbar is hidden for bubbles. */
     public boolean isHiddenForBubbles() {
         return hasAnyFlag(FLAG_STASHED_FOR_BUBBLES);
@@ -799,14 +827,14 @@
             if (animationType == TRANSITION_HANDLE_FADE) {
                 // When fading, the handle fades in/out at the beginning of the transition with
                 // TASKBAR_STASH_ALPHA_DURATION.
-                backgroundAndHandleAlphaDuration = TASKBAR_STASH_ALPHA_DURATION;
+                backgroundAndHandleAlphaDuration = TRANSIENT_TASKBAR_STASH_ALPHA_DURATION;
                 // The iconAlphaDuration must be set to duration for the skippable interpolators
                 // below to work.
                 iconAlphaDuration = duration;
             } else {
                 iconAlphaStartDelay = TASKBAR_STASH_ALPHA_START_DELAY;
-                iconAlphaDuration = TASKBAR_STASH_ALPHA_DURATION;
-                backgroundAndHandleAlphaDuration = TASKBAR_STASH_ALPHA_DURATION;
+                iconAlphaDuration = TRANSIENT_TASKBAR_STASH_ALPHA_DURATION;
+                backgroundAndHandleAlphaDuration = TRANSIENT_TASKBAR_STASH_ALPHA_DURATION;
 
                 if (isStashed) {
                     if (animationType == TRANSITION_HOME_TO_APP) {
@@ -951,7 +979,7 @@
         }
         int action = expanding ? InteractionJankMonitor.CUJ_TASKBAR_EXPAND :
                 InteractionJankMonitor.CUJ_TASKBAR_COLLAPSE;
-        animator.addListener(new AnimatorListenerAdapter() {
+        animator.addListener(new AnimationSuccessListener() {
             @Override
             public void onAnimationStart(@NonNull Animator animation) {
                 final Configuration.Builder builder =
@@ -963,9 +991,16 @@
             }
 
             @Override
-            public void onAnimationEnd(@NonNull Animator animation) {
+            public void onAnimationSuccess(@NonNull Animator animator) {
                 InteractionJankMonitor.getInstance().end(action);
             }
+
+            @Override
+            public void onAnimationCancel(@NonNull Animator animation) {
+                super.onAnimationCancel(animation);
+
+                InteractionJankMonitor.getInstance().cancel(action);
+            }
         });
     }
 
@@ -1070,7 +1105,8 @@
     /**
      * When hiding the IME, delay the unstash animation to align with the end of the transition.
      */
-    private long getTaskbarStashStartDelayForIme() {
+    @VisibleForTesting
+    long getTaskbarStashStartDelayForIme() {
         if (mIsImeShowing) {
             // Only delay when IME is exiting, not entering.
             return 0;
@@ -1104,7 +1140,13 @@
             startDelay = getTaskbarStashStartDelayForIme();
         }
 
-        applyState(skipAnim ? 0 : animDuration, skipAnim ? 0 : startDelay);
+        if (isTaskbarHidden(systemUiStateFlags) && !hasAnyFlag(FLAG_TASKBAR_HIDDEN)) {
+            updateStateForFlag(FLAG_TASKBAR_HIDDEN, isTaskbarHidden(systemUiStateFlags));
+            applyState(0, SKIP_TOTAL_DREAM_ANIM_DURATION);
+        } else {
+            updateStateForFlag(FLAG_TASKBAR_HIDDEN, isTaskbarHidden(systemUiStateFlags));
+            applyState(skipAnim ? 0 : animDuration, skipAnim ? 0 : startDelay);
+        }
     }
 
     /**
@@ -1126,13 +1168,13 @@
         }
 
         // Do not stash if pinned taskbar, hardware keyboard is attached and no IME is docked
-        if (mActivity.isHardwareKeyboard() && DisplayController.isPinnedTaskbar(mActivity)
+        if (isHardwareKeyboard() && DisplayController.isPinnedTaskbar(mActivity)
                 && !mActivity.isImeDocked()) {
             return false;
         }
 
         // Do not stash if hardware keyboard is attached, in 3 button nav and desktop windowing mode
-        if (mActivity.isHardwareKeyboard()
+        if (isHardwareKeyboard()
                 && mActivity.isThreeButtonNav()
                 && mControllers.taskbarDesktopModeController.getAreDesktopTasksVisible()) {
             return false;
@@ -1146,6 +1188,21 @@
         return mIsImeShowing || mIsImeSwitcherShowing;
     }
 
+    private boolean isHardwareKeyboard() {
+        return mActivity.isHardwareKeyboard() && !sEnableSoftwareImeForTests;
+    }
+
+    /**
+     * Overrides {@link #isHardwareKeyboard()} to {@code false} for testing, if enabled.
+     * <p>
+     * Virtual devices are sometimes in hardware keyboard mode, leading to an inconsistent
+     * testing environment.
+     */
+    @VisibleForTesting
+    static void enableSoftwareImeForTests(boolean enable) {
+        sEnableSoftwareImeForTests = enable;
+    }
+
     /**
      * Updates the proper flag to indicate whether the task bar should be stashed.
      *
@@ -1271,7 +1328,7 @@
     /**
      * Attempts to start timer to auto hide the taskbar based on time.
      */
-    public void tryStartTaskbarTimeout() {
+    private void tryStartTaskbarTimeout() {
         if (!DisplayController.isTransientTaskbar(mActivity)
                 || mIsStashed
                 || mEnableBlockingTimeoutDuringTests) {
@@ -1299,6 +1356,11 @@
         updateAndAnimateTransientTaskbarForTimeout();
     }
 
+    @VisibleForTesting
+    Alarm getTimeoutAlarm() {
+        return mTimeoutAlarm;
+    }
+
     @Override
     public void dumpLogs(String prefix, PrintWriter pw) {
         pw.println(prefix + "TaskbarStashController:");
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
index 9c8c2a9..f7f5cf6 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
@@ -36,6 +36,7 @@
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.ItemInfoWithIcon;
 import com.android.launcher3.popup.SystemShortcut;
+import com.android.launcher3.taskbar.bubbles.BubbleBarController;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.SplitConfigurationOptions;
 import com.android.quickstep.OverviewCommandHelper;
@@ -47,6 +48,7 @@
 import com.android.quickstep.views.TaskView;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
+import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
 
 import java.io.PrintWriter;
 import java.util.Collections;
@@ -55,7 +57,7 @@
 /**
  * Base class for providing different taskbar UI
  */
-public class TaskbarUIController {
+public class TaskbarUIController implements BubbleBarController.BubbleBarLocationListener {
     public static final TaskbarUIController DEFAULT = new TaskbarUIController();
 
     // Initialized in init.
@@ -116,6 +118,8 @@
      * Manually closes the overlay window.
      */
     public void hideOverlayWindow() {
+        mControllers.keyboardQuickSwitchController.closeQuickSwitchView();
+
         if (!DisplayController.isTransientTaskbar(mControllers.taskbarActivityContext)
                 || mControllers.taskbarAllAppsController.isOpen()) {
             mControllers.taskbarOverlayController.hideWindow();
@@ -433,6 +437,14 @@
     public void stashHotseat(boolean stash) {
     }
 
+    @Override
+    public void onBubbleBarLocationAnimated(BubbleBarLocation location) {
+    }
+
+    @Override
+    public void onBubbleBarLocationUpdated(BubbleBarLocation location) {
+    }
+
     /** Un-stash the hotseat instantly */
     public void unStashHotseatInstantly() {
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
index 2734137..8816a6d 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
@@ -64,12 +64,12 @@
 import com.android.launcher3.util.LauncherBindableItemsContainer;
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.views.ActivityContext;
-import com.android.launcher3.views.IconButtonView;
-import com.android.quickstep.util.DesktopTask;
 import com.android.quickstep.util.GroupTask;
+import com.android.quickstep.views.TaskViewType;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
 
+import java.util.ArrayList;
 import java.util.List;
 import java.util.function.Predicate;
 
@@ -100,13 +100,13 @@
     @Nullable private FolderIcon mLeaveBehindFolderIcon;
 
     // Only non-null when device supports having an All Apps button.
-    @Nullable private final TaskbarAllAppsButtonContainer mAllAppsButtonContainer;
+    private final TaskbarAllAppsButtonContainer mAllAppsButtonContainer;
 
     // Only non-null when device supports having a Divider button.
     @Nullable private TaskbarDividerContainer mTaskbarDividerContainer;
 
     // Only non-null when device supports having a Taskbar Overflow button.
-    @Nullable private IconButtonView mTaskbarOverflowView;
+    @Nullable private TaskbarOverflowView mTaskbarOverflowView;
 
     /**
      * Whether the divider is between Hotseat icons and Recents,
@@ -120,6 +120,11 @@
 
     private boolean mShouldTryStartAlign;
 
+    private int mMaxNumIcons = 0;
+    private int mIdealNumIcons = 0;
+
+    private final int mAllAppsButtonTranslationOffset;
+
     public TaskbarView(@NonNull Context context) {
         this(context, null);
     }
@@ -139,8 +144,6 @@
         mActivityContext = ActivityContext.lookupContext(context);
         mIconLayoutBounds = mActivityContext.getTransientTaskbarBounds();
         Resources resources = getResources();
-        boolean isTransientTaskbar = DisplayController.isTransientTaskbar(mActivityContext)
-                && !mActivityContext.isPhoneMode();
         mIsRtl = Utilities.isRtl(resources);
         mTransientTaskbarMinWidth = resources.getDimension(R.dimen.transient_taskbar_min_width);
 
@@ -171,22 +174,81 @@
         setWillNotDraw(false);
 
         mAllAppsButtonContainer = new TaskbarAllAppsButtonContainer(context);
+        mAllAppsButtonTranslationOffset =  (int) getResources().getDimension(
+                mAllAppsButtonContainer.getAllAppsButtonTranslationXOffset(isTransientTaskbar()));
 
         if (enableTaskbarPinning() || enableRecentsInTaskbar()) {
             mTaskbarDividerContainer = new TaskbarDividerContainer(context);
         }
 
         if (Flags.taskbarOverflow()) {
-            mTaskbarOverflowView = (IconButtonView) LayoutInflater.from(context)
-                    .inflate(R.layout.taskbar_overflow_button, this, false);
-            mTaskbarOverflowView.setIconDrawable(
-                    resources.getDrawable(R.drawable.taskbar_overflow_icon));
-            mTaskbarOverflowView.setPadding(mItemPadding, mItemPadding, mItemPadding, mItemPadding);
+            mTaskbarOverflowView = TaskbarOverflowView.inflateIcon(
+                    R.layout.taskbar_overflow_view, this,
+                    mIconTouchSize, mItemPadding);
         }
+
         // TODO: Disable touch events on QSB otherwise it can crash.
         mQsb = LayoutInflater.from(context).inflate(R.layout.search_container_hotseat, this, false);
     }
 
+    /**
+     * @return the maximum number of 'icons' that can fit in the taskbar.
+     */
+    private int calculateMaxNumIcons() {
+        DeviceProfile deviceProfile = mActivityContext.getDeviceProfile();
+        int availableWidth = deviceProfile.widthPx;
+        int defaultEdgeMargin =
+                (int) getResources().getDimension(deviceProfile.inv.inlineNavButtonsEndSpacing);
+        int spaceForBubbleBar =
+                Math.round(mControllerCallbacks.getBubbleBarMaxCollapsedWidthIfVisible());
+
+        // Reserve space required for edge margins, or for navbar if shown. If task bar needs to be
+        // center aligned with nav bar shown, reserve space on both sides.
+        availableWidth -=
+                Math.max(defaultEdgeMargin + spaceForBubbleBar, deviceProfile.hotseatBarEndOffset);
+        availableWidth -= Math.max(
+                defaultEdgeMargin + (mShouldTryStartAlign ? 0 : spaceForBubbleBar),
+                mShouldTryStartAlign ? 0 : deviceProfile.hotseatBarEndOffset);
+
+        // The space taken by an item icon used during layout.
+        int iconSize = 2 * mItemMarginLeftRight + mIconTouchSize;
+
+        int additionalIcons = 0;
+
+        if (mTaskbarDividerContainer != null) {
+            // Space for divider icon is reduced during layout compared to normal icon size, reserve
+            // space for the divider separately.
+            availableWidth -= iconSize - 4 * mItemMarginLeftRight;
+            ++additionalIcons;
+        }
+
+        // All apps icon takes less space compared to normal icon size, reserve space for the icon
+        // separately.
+        boolean forceTransientTaskbarSize =
+                enableTaskbarPinning() && !mActivityContext.isThreeButtonNav();
+        availableWidth -= iconSize - (int) getResources().getDimension(
+                mAllAppsButtonContainer.getAllAppsButtonTranslationXOffset(
+                        forceTransientTaskbarSize || isTransientTaskbar()));
+        ++additionalIcons;
+
+        return Math.floorDiv(availableWidth, iconSize) + additionalIcons;
+    }
+
+    /**
+     * Recalculates the max number of icons the taskbar view can show without entering overflow.
+     * Returns whether the max number of icons changed and the change affects the number of icons
+     * that should be shown in the taskbar.
+     */
+    boolean updateMaxNumIcons() {
+        if (!Flags.taskbarOverflow()) {
+            return false;
+        }
+        int oldMaxNumIcons = mMaxNumIcons;
+        mMaxNumIcons = calculateMaxNumIcons();
+        return oldMaxNumIcons != mMaxNumIcons
+                && (mIdealNumIcons > oldMaxNumIcons || mIdealNumIcons > mMaxNumIcons);
+    }
+
     @Override
     public void setVisibility(int visibility) {
         boolean changed = getVisibility() != visibility;
@@ -269,10 +331,9 @@
         mIconClickListener = mControllerCallbacks.getIconOnClickListener();
         mIconLongClickListener = mControllerCallbacks.getIconOnLongClickListener();
 
-        if (mAllAppsButtonContainer != null) {
-            mAllAppsButtonContainer.setUpCallbacks(callbacks);
-        }
-        if (mTaskbarDividerContainer != null && callbacks.supportsDividerLongPress()) {
+        mAllAppsButtonContainer.setUpCallbacks(callbacks);
+        if (mTaskbarDividerContainer != null
+                && mActivityContext.getTaskbarFeatureEvaluator().getSupportsPinningPopup()) {
             mTaskbarDividerContainer.setUpCallbacks(callbacks);
         }
         if (mTaskbarOverflowView != null) {
@@ -281,6 +342,14 @@
             mTaskbarOverflowView.setOnLongClickListener(
                     mControllerCallbacks.getOverflowOnLongClickListener());
         }
+        if (Flags.showTaskbarPinningPopupFromAnywhere()
+                && mActivityContext.getTaskbarFeatureEvaluator().getSupportsPinningPopup()) {
+            setOnTouchListener(mControllerCallbacks.getTaskbarTouchListener());
+        }
+
+        if (Flags.taskbarOverflow()) {
+            mMaxNumIcons = calculateMaxNumIcons();
+        }
     }
 
     private void removeAndRecycle(View view) {
@@ -301,12 +370,10 @@
         int numViewsAnimated = 0;
         mAddedDividerForRecents = false;
 
-        if (mAllAppsButtonContainer != null) {
-            removeView(mAllAppsButtonContainer);
+        removeView(mAllAppsButtonContainer);
 
-            if (mTaskbarDividerContainer != null) {
-                removeView(mTaskbarDividerContainer);
-            }
+        if (mTaskbarDividerContainer != null) {
+            removeView(mTaskbarDividerContainer);
         }
         if (mTaskbarOverflowView != null) {
             removeView(mTaskbarOverflowView);
@@ -398,18 +465,62 @@
         if (mTaskbarDividerContainer != null && !recentTasks.isEmpty()) {
             addView(mTaskbarDividerContainer, nextViewIndex++);
             mAddedDividerForRecents = true;
-            if (mTaskbarOverflowView != null) {
-                addView(mTaskbarOverflowView, nextViewIndex++);
+        }
+
+        // At this point, the all apps button has not been added as a child view, but needs to be
+        // accounted for when comparing current icon count to max number of icons.
+        int nonTaskIconsToBeAdded = 1;
+
+        boolean supportsOverflow = Flags.taskbarOverflow();
+        int overflowSize = 0;
+        if (supportsOverflow) {
+            int numberOfSupportedRecents = 0;
+            for (GroupTask task : recentTasks) {
+                // TODO(b/343289567 and b/316004172): support app pairs and desktop mode.
+                if (!task.supportsMultipleTasks()) {
+                    ++numberOfSupportedRecents;
+                }
             }
+
+            mIdealNumIcons = nextViewIndex + numberOfSupportedRecents + nonTaskIconsToBeAdded;
+            overflowSize = mIdealNumIcons - mMaxNumIcons;
+
+            if (overflowSize > 0 && mTaskbarOverflowView != null) {
+                addView(mTaskbarOverflowView, nextViewIndex++);
+            } else if (mTaskbarOverflowView != null) {
+                mTaskbarOverflowView.clearItems();
+            }
+        }
+
+        List<Task> overflownTasks = null;
+        // An extra item needs to be added to overflow button to account for the space taken up by
+        // the overflow button.
+        final int itemsToAddToOverflow = overflowSize > 0 ? overflowSize + 1 : 0;
+        if (overflowSize > 0) {
+            overflownTasks = new ArrayList<Task>(itemsToAddToOverflow);
         }
 
         // Add Recent/Running icons.
         for (GroupTask task : recentTasks) {
+            if (mTaskbarOverflowView != null && overflownTasks != null
+                    && overflownTasks.size() < itemsToAddToOverflow) {
+                // TODO(b/343289567 and b/316004172): support app pairs and desktop mode.
+                if (task.supportsMultipleTasks()) {
+                    continue;
+                }
+
+                overflownTasks.add(task.task1);
+                if (overflownTasks.size() == itemsToAddToOverflow) {
+                    mTaskbarOverflowView.setItems(overflownTasks);
+                }
+                continue;
+            }
+
             // Replace any Recent views with the appropriate type if it's not already that type.
             final int expectedLayoutResId;
             boolean isCollection = false;
-            if (task.hasMultipleTasks()) {
-                if (task instanceof DesktopTask) {
+            if (task.supportsMultipleTasks()) {
+                if (task.taskViewType == TaskViewType.DESKTOP) {
                     // TODO(b/316004172): use Desktop tile layout.
                     expectedLayoutResId = -1;
                 } else {
@@ -463,16 +574,16 @@
             removeAndRecycle(getChildAt(nextViewIndex));
         }
 
-        if (mAllAppsButtonContainer != null) {
-            addView(mAllAppsButtonContainer, mIsRtl ? hotseatItemInfos.length : 0);
+        addView(mAllAppsButtonContainer, mIsRtl ? hotseatItemInfos.length : 0);
 
-            // If there are no recent tasks, add divider after All Apps (unless it's the only view).
-            if (!mAddedDividerForRecents
-                    && mTaskbarDividerContainer != null
-                    && getChildCount() > 1) {
-                addView(mTaskbarDividerContainer, mIsRtl ? (getChildCount() - 1) : 1);
-            }
+        // If there are no recent tasks, add divider after All Apps (unless it's the only view).
+        if (!mAddedDividerForRecents
+                && mTaskbarDividerContainer != null
+                && getChildCount() > 1) {
+            addView(mTaskbarDividerContainer, mIsRtl ? (getChildCount() - 1) : 1);
         }
+
+
         if (mActivityContext.getDeviceProfile().isQsbInline) {
             addView(mQsb, mIsRtl ? getChildCount() : 0);
             // Always set QSB to invisible after re-adding.
@@ -604,6 +715,15 @@
         mIconLayoutBounds.right = iconEnd;
         mIconLayoutBounds.top = (bottom - top - mIconTouchSize) / 2;
         mIconLayoutBounds.bottom = mIconLayoutBounds.top + mIconTouchSize;
+
+        // With rtl layout, the all apps button will be translated by `allAppsButtonOffset` after
+        // layout completion (by `TaskbarViewController`). Offset the icon end by the same amount
+        // when laying out icons, so the taskbar content remains centered after all apps button
+        // translation.
+        if (layoutRtl) {
+            iconEnd += mAllAppsButtonTranslationOffset;
+        }
+
         int count = getChildCount();
         for (int i = count; i > 0; i--) {
             View child = getChildAt(i - 1);
@@ -635,6 +755,15 @@
 
         mIconLayoutBounds.left = iconEnd;
 
+        // Adjust the icon layout bounds by the amount by which all apps button will be translated
+        // post layout to maintain margin between all apps button and the edge of the transient
+        // taskbar background. Done for ltr layout only - for rtl layout, the offset needs to be
+        // adjusted on the right, which is done by offsetting `iconEnd` after setting
+        // `mIconLayoutBounds.right`.
+        if (!layoutRtl) {
+            mIconLayoutBounds.left += mAllAppsButtonTranslationOffset;
+        }
+
         if (mIconLayoutBounds.right - mIconLayoutBounds.left < mTransientTaskbarMinWidth) {
             int center = mIconLayoutBounds.centerX();
             int distanceFromCenter = (int) mTransientTaskbarMinWidth / 2;
@@ -677,12 +806,13 @@
     /**
      * Returns the space used by the icons
      */
-    public int getIconLayoutWidth() {
+    private int getIconLayoutWidth() {
         int countExcludingQsb = getChildCount();
         DeviceProfile deviceProfile = mActivityContext.getDeviceProfile();
         if (deviceProfile.isQsbInline) {
             countExcludingQsb--;
         }
+
         int iconLayoutBoundsWidth =
                 countExcludingQsb * (mItemMarginLeftRight * 2 + mIconTouchSize);
 
@@ -691,17 +821,28 @@
             // All Apps icon, divider icon, and first app icon in taskbar
             iconLayoutBoundsWidth -= mItemMarginLeftRight * 4;
         }
+
+        // The all apps button container gets offset horizontally, reducing the overall taskbar
+        // view size.
+        iconLayoutBoundsWidth -= mAllAppsButtonTranslationOffset;
+
         return iconLayoutBoundsWidth;
     }
 
     /**
-     * Returns the app icons currently shown in the taskbar.
+     * Returns the app icons currently shown in the taskbar. The returned list does not include qsb,
+     * but it includes all apps button and icon divider views.
      */
     public View[] getIconViews() {
         final int count = getChildCount();
-        View[] icons = new View[count];
+        if (count == 0) {
+            return new View[0];
+        }
+        View[] icons = new View[count - (mActivityContext.getDeviceProfile().isQsbInline ? 1 : 0)];
+        int insertionPoint = 0;
         for (int i = 0; i < count; i++) {
-            icons[i] = getChildAt(i);
+            if (getChildAt(i)  == mQsb) continue;
+            icons[insertionPoint++] = getChildAt(i);
         }
         return icons;
     }
@@ -709,7 +850,6 @@
     /**
      * Returns the all apps button in the taskbar.
      */
-    @Nullable
     public TaskbarAllAppsButtonContainer getAllAppsButtonContainer() {
         return mAllAppsButtonContainer;
     }
@@ -726,7 +866,7 @@
      * Returns the taskbar overflow view in the taskbar.
      */
     @Nullable
-    public IconButtonView getTaskbarOverflowView() {
+    public TaskbarOverflowView getTaskbarOverflowView() {
         return mTaskbarOverflowView;
     }
 
@@ -784,12 +924,25 @@
         // Ignore, we just implement Insettable to draw behind system insets.
     }
 
+    private boolean isTransientTaskbar() {
+        return DisplayController.isTransientTaskbar(mActivityContext)
+                && !mActivityContext.isPhoneMode();
+    }
+
     public boolean areIconsVisible() {
         // Consider the overall visibility
         return getVisibility() == VISIBLE;
     }
 
     /**
+     * @return The all apps button horizontal offset used to calculate the taskbar contents width
+     * during layout.
+     */
+    public int getAllAppsButtonTranslationXOffsetUsedForLayout() {
+        return mAllAppsButtonTranslationOffset;
+    }
+
+    /**
      * Maps {@code op} over all the child views.
      */
     public void mapOverItems(LauncherBindableItemsContainer.ItemOperator op) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java
index d108d8c..f65f307 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacks.java
@@ -19,14 +19,19 @@
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_ALLAPPS_BUTTON_LONG_PRESS;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_ALLAPPS_BUTTON_TAP;
 
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.view.GestureDetector;
 import android.view.InputDevice;
 import android.view.MotionEvent;
 import android.view.View;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import com.android.internal.jank.Cuj;
 import com.android.launcher3.taskbar.bubbles.BubbleBarViewController;
+import com.android.launcher3.util.DisplayController;
 import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
 import com.android.wm.shell.Flags;
 import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
@@ -39,12 +44,14 @@
     private final TaskbarActivityContext mActivity;
     private final TaskbarControllers mControllers;
     private final TaskbarView mTaskbarView;
+    private final GestureDetector mGestureDetector;
 
     public TaskbarViewCallbacks(TaskbarActivityContext activity, TaskbarControllers controllers,
             TaskbarView taskbarView) {
         mActivity = activity;
         mControllers = controllers;
         mTaskbarView = taskbarView;
+        mGestureDetector = new GestureDetector(activity, new TaskbarViewGestureListener());
     }
 
     public View.OnClickListener getIconOnClickListener() {
@@ -64,27 +71,28 @@
         mActivity.getStatsLogManager().logger().log(LAUNCHER_TASKBAR_ALLAPPS_BUTTON_LONG_PRESS);
     }
 
-    public boolean isAllAppsButtonHapticFeedbackEnabled() {
+    /** @return true if haptic feedback should occur when long pressing the all apps button. */
+    public boolean isAllAppsButtonHapticFeedbackEnabled(Context context) {
         return false;
     }
 
+    @SuppressLint("ClickableViewAccessibility")
+    public View.OnTouchListener getTaskbarTouchListener() {
+        return (view, event) -> mGestureDetector.onTouchEvent(event);
+    }
+
     public View.OnLongClickListener getTaskbarDividerLongClickListener() {
         return v -> {
-            mControllers.taskbarPinningController.showPinningView(v);
+            mControllers.taskbarPinningController.showPinningView(v, getDividerCenterX());
             return true;
         };
     }
 
-    /** Check to see if we support long press on taskbar divider */
-    public boolean supportsDividerLongPress() {
-        return !mActivity.isThreeButtonNav();
-    }
-
     public View.OnTouchListener getTaskbarDividerRightClickListener() {
         return (v, event) -> {
             if (event.isFromSource(InputDevice.SOURCE_MOUSE)
                     && event.getButtonState() == MotionEvent.BUTTON_SECONDARY) {
-                mControllers.taskbarPinningController.showPinningView(v);
+                mControllers.taskbarPinningController.showPinningView(v, getDividerCenterX());
                 return true;
             }
             return false;
@@ -129,6 +137,17 @@
         return null;
     }
 
+    /**
+     * Get the max bubble bar collapsed width for the current bubble bar visibility state. Used to
+     * reserve space for the bubble bar when transitioning taskbar view into overflow.
+     */
+    public float getBubbleBarMaxCollapsedWidthIfVisible() {
+        return mControllers.bubbleControllers
+                .filter(c -> !c.bubbleBarViewController.isHiddenForNoBubbles())
+                .map(c -> c.bubbleBarViewController.getCollapsedWidthWithMaxVisibleBubbles())
+                .orElse(0f);
+    }
+
     /** Returns true if bubble bar controllers present and enabled in persistent taskbar. */
     public boolean isBubbleBarEnabledInPersistentTaskbar() {
         return Flags.enableBubbleBarInPersistentTaskBar()
@@ -140,7 +159,7 @@
         return new View.OnClickListener() {
             @Override
             public void onClick(View v) {
-                mControllers.keyboardQuickSwitchController.openQuickSwitchView();
+                toggleKeyboardQuickSwitchView();
             }
         };
     }
@@ -150,9 +169,53 @@
         return new View.OnLongClickListener() {
             @Override
             public boolean onLongClick(View v) {
-                mControllers.keyboardQuickSwitchController.openQuickSwitchView();
+                toggleKeyboardQuickSwitchView();
                 return true;
             }
         };
     }
+
+    private void toggleKeyboardQuickSwitchView() {
+        if (mTaskbarView.getTaskbarOverflowView() != null) {
+            mTaskbarView.getTaskbarOverflowView().setIsActive(
+                    !mTaskbarView.getTaskbarOverflowView().getIsActive());
+        }
+        mControllers.keyboardQuickSwitchController.toggleQuickSwitchViewForTaskbar(
+                mControllers.taskbarViewController.getTaskIdsForPinnedApps(),
+                this::onKeyboardQuickSwitchViewClosed);
+    }
+
+    private void onKeyboardQuickSwitchViewClosed() {
+        if (mTaskbarView.getTaskbarOverflowView() != null) {
+            mTaskbarView.getTaskbarOverflowView().setIsActive(false);
+        }
+    }
+
+    private float getDividerCenterX() {
+        View divider = mTaskbarView.getTaskbarDividerViewContainer();
+        if (divider == null) {
+            return 0.0f;
+        }
+        return divider.getX() + (float) divider.getWidth() / 2;
+    }
+
+    private class TaskbarViewGestureListener extends GestureDetector.SimpleOnGestureListener {
+        @Override
+        public boolean onDown(@NonNull MotionEvent event) {
+            return true;
+        }
+
+        @Override
+        public boolean onSingleTapUp(@NonNull MotionEvent event) {
+            return true;
+        }
+
+        @Override
+        public void onLongPress(MotionEvent event) {
+            if (DisplayController.isPinnedTaskbar(mActivity)) {
+                mControllers.taskbarPinningController.showPinningView(mTaskbarView,
+                        event.getRawX());
+            }
+        }
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacksFactory.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacksFactory.kt
index ba0f5a0..704d6cf 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacksFactory.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewCallbacksFactory.kt
@@ -16,10 +16,14 @@
 
 package com.android.launcher3.taskbar
 
+import android.app.contextualsearch.ContextualSearchManager.ENTRYPOINT_LONG_PRESS_META
 import android.content.Context
 import com.android.launcher3.R
+import com.android.launcher3.logging.StatsLogManager
 import com.android.launcher3.util.ResourceBasedOverride
 import com.android.launcher3.util.ResourceBasedOverride.Overrides
+import com.android.quickstep.TopTaskTracker
+import com.android.quickstep.util.ContextualSearchInvoker
 
 /** Creates [TaskbarViewCallbacks] instances. */
 open class TaskbarViewCallbacksFactory : ResourceBasedOverride {
@@ -28,7 +32,35 @@
         activity: TaskbarActivityContext,
         controllers: TaskbarControllers,
         taskbarView: TaskbarView,
-    ): TaskbarViewCallbacks = TaskbarViewCallbacks(activity, controllers, taskbarView)
+    ): TaskbarViewCallbacks {
+        return object : TaskbarViewCallbacks(activity, controllers, taskbarView) {
+            override fun triggerAllAppsButtonLongClick() {
+                super.triggerAllAppsButtonLongClick()
+
+                val contextualSearchInvoked =
+                    ContextualSearchInvoker.newInstance(activity).show(ENTRYPOINT_LONG_PRESS_META)
+                if (contextualSearchInvoked) {
+                    val runningPackage =
+                        TopTaskTracker.INSTANCE[activity].getCachedTopTask(
+                                /* filterOnlyVisibleRecents */ true
+                            )
+                            .getPackageName()
+                    activity.statsLogManager
+                        .logger()
+                        .withPackageName(runningPackage)
+                        .log(StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_SUCCESSFUL_META)
+                }
+            }
+
+            override fun isAllAppsButtonHapticFeedbackEnabled(context: Context): Boolean {
+                return longPressAllAppsToStartContextualSearch(context)
+            }
+        }
+    }
+
+    open fun longPressAllAppsToStartContextualSearch(context: Context): Boolean =
+        ContextualSearchInvoker.newInstance(context)
+            .runContextualSearchInvocationChecksAndLogFailures()
 
     companion object {
         @JvmStatic
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
index b207b37..cebabff 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
@@ -18,6 +18,7 @@
 import static com.android.app.animation.Interpolators.FINAL_FRAME;
 import static com.android.app.animation.Interpolators.LINEAR;
 import static com.android.launcher3.Flags.enableScalingRevealHomeAnimation;
+import static com.android.launcher3.Flags.taskbarOverflow;
 import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
 import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA;
 import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X;
@@ -81,6 +82,8 @@
 import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
 
 import java.io.PrintWriter;
+import java.util.Collections;
+import java.util.HashSet;
 import java.util.Set;
 import java.util.function.Predicate;
 
@@ -156,7 +159,9 @@
     private final View.OnLayoutChangeListener mTaskbarViewLayoutChangeListener =
             (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
                 updateTaskbarIconTranslationXForPinning();
-                mControllers.navbarButtonsViewController.onTaskbarLayoutChange();
+                if (BubbleBarController.isBubbleBarEnabled()) {
+                    mControllers.navbarButtonsViewController.onLayoutsUpdated();
+                }
             };
 
     // Animation to align icons with Launcher, created lazily. This allows the controller to be
@@ -326,7 +331,8 @@
      */
     public void setRecentsButtonDisabled(boolean isDisabled) {
         // TODO: check TaskbarStashController#supportsStashing(), to stash instead of setting alpha.
-        mTaskbarIconAlpha.get(ALPHA_INDEX_RECENTS_DISABLED).setValue(isDisabled ? 0 : 1);
+        mTaskbarIconAlpha.get(ALPHA_INDEX_RECENTS_DISABLED).animateToValue(isDisabled ? 0 : 1)
+                .start();
     }
 
     /**
@@ -353,15 +359,10 @@
         return mTaskbarView.getIconLayoutBounds();
     }
 
-    public int getIconLayoutWidth() {
-        return mTaskbarView.getIconLayoutWidth();
-    }
-
     public View[] getIconViews() {
         return mTaskbarView.getIconViews();
     }
 
-    @Nullable
     public View getAllAppsButtonView() {
         return mTaskbarView.getAllAppsButtonContainer();
     }
@@ -437,6 +438,19 @@
         float allAppIconTranslateRange = mapRange(scale, transientTaskbarAllAppsOffset,
                 persistentTaskbarAllAppsOffset);
 
+        // Task icons are laid out so the taskbar content is centered. The taskbar width (used for
+        // centering taskbar icons) depends on the all apps button X translation, and is different
+        // for persistent and transient taskbar. If the offset used for current taskbar layout is
+        // different than the offset used in final taskbar state, the icons may jump when the
+        // animation completes, and the taskbar is replaced. Adjust item transform to account for
+        // this mismatch.
+        float sizeDiffTranslationRange =
+                mapRange(scale,
+                        (mTaskbarView.getAllAppsButtonTranslationXOffsetUsedForLayout()
+                                - transientTaskbarAllAppsOffset) / 2,
+                        (mTaskbarView.getAllAppsButtonTranslationXOffsetUsedForLayout()
+                                - persistentTaskbarAllAppsOffset) / 2);
+
         // no x translation required when all apps button is the only icon in taskbar.
         if (iconViews.length <= 1) {
             allAppIconTranslateRange = 0f;
@@ -444,6 +458,7 @@
 
         if (mIsRtl) {
             allAppIconTranslateRange *= -1;
+            sizeDiffTranslationRange *= -1;
         }
 
         if (mActivity.isThreeButtonNav()) {
@@ -452,25 +467,18 @@
             return;
         }
 
-        float taskbarCenterX =
-                mTaskbarView.getLeft() + (mTaskbarView.getRight() - mTaskbarView.getLeft()) / 2.0f;
-
         float finalMarginScale = mapRange(scale, 0f, mTransientIconSize - mPersistentIconSize);
 
-        float halfIconCount = iconViews.length / 2.0f;
+        // The index of the "middle" icon which will be used as a index from which the icon margins
+        // will be scaled. If number of icons is even, using the middle point between indices of two
+        // central icons.
+        float middleIndex = (iconViews.length - 1) / 2.0f;
         for (int iconIndex = 0; iconIndex < iconViews.length; iconIndex++) {
             View iconView = iconViews[iconIndex];
             MultiTranslateDelegate translateDelegate =
                     ((Reorderable) iconView).getTranslateDelegate();
-            float iconCenterX =
-                    iconView.getLeft() + (iconView.getRight() - iconView.getLeft()) / 2.0f;
-            if (iconCenterX <= taskbarCenterX) {
-                translateDelegate.getTranslationX(INDEX_TASKBAR_PINNING_ANIM).setValue(
-                        finalMarginScale * (halfIconCount - iconIndex));
-            } else {
-                translateDelegate.getTranslationX(INDEX_TASKBAR_PINNING_ANIM).setValue(
-                        -finalMarginScale * (iconIndex - halfIconCount));
-            }
+            translateDelegate.getTranslationX(INDEX_TASKBAR_PINNING_ANIM).setValue(
+                    finalMarginScale * (middleIndex - iconIndex) + sizeDiffTranslationRange);
 
             if (iconView.equals(mTaskbarView.getAllAppsButtonContainer())) {
                 mTaskbarView.getAllAppsButtonContainer().setTranslationXForTaskbarAllAppsIcon(
@@ -483,18 +491,14 @@
      * Calculates visual taskbar view width.
      */
     public float getCurrentVisualTaskbarWidth() {
-        if (mTaskbarView.getIconViews().length == 0) {
+        View[] iconViews = mTaskbarView.getIconViews();
+        if (iconViews.length == 0) {
             return 0;
         }
 
-        View[] iconViews = mTaskbarView.getIconViews();
+        float left = iconViews[0].getX();
 
-        int leftIndex = mActivity.getDeviceProfile().isQsbInline && !mIsRtl ? 1 : 0;
-        int rightIndex = mActivity.getDeviceProfile().isQsbInline && mIsRtl
-                ? iconViews.length - 2
-                : iconViews.length - 1;
-
-        float left = iconViews[leftIndex].getX();
+        int rightIndex = iconViews.length - 1;
         float right = iconViews[rightIndex].getRight() + iconViews[rightIndex].getTranslationX();
 
         return right - left + (2 * mTaskbarLeftRightMargin);
@@ -619,36 +623,41 @@
      * 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) {
+    public void updateIconViewsRunningStates() {
         for (View iconView : getIconViews()) {
             if (iconView instanceof BubbleTextView btv) {
-                btv.updateRunningState(
-                        getRunningAppState(btv, runningTaskIds, minimizedTaskIds));
+                btv.updateRunningState(getRunningAppState(btv));
             }
         }
     }
 
-    private BubbleTextView.RunningAppState getRunningAppState(
-            BubbleTextView btv,
-            Set<Integer> runningTaskIds,
-            Set<Integer> minimizedTaskIds) {
-        Object tag = btv.getTag();
-        if (tag instanceof TaskItemInfo itemInfo) {
-            if (minimizedTaskIds.contains(itemInfo.getTaskId())) {
-                return BubbleTextView.RunningAppState.MINIMIZED;
-            }
-            if (runningTaskIds.contains(itemInfo.getTaskId())) {
-                return BubbleTextView.RunningAppState.RUNNING;
+    /**
+     * @return A set of Task ids of running apps that are pinned in the taskbar.
+     */
+    protected Set<Integer> getTaskIdsForPinnedApps() {
+        if (!taskbarOverflow()) {
+            return Collections.emptySet();
+        }
+
+        Set<Integer> pinnedAppsWithTasks = new HashSet<>();
+        for (View iconView : getIconViews()) {
+            if (iconView instanceof BubbleTextView btv
+                    && btv.getTag() instanceof TaskItemInfo itemInfo) {
+                pinnedAppsWithTasks.add(itemInfo.getTaskId());
             }
         }
+        return pinnedAppsWithTasks;
+    }
+
+    private BubbleTextView.RunningAppState getRunningAppState(BubbleTextView btv) {
+        Object tag = btv.getTag();
+        if (tag instanceof TaskItemInfo itemInfo) {
+            return mControllers.taskbarRecentAppsController.getRunningAppState(
+                    itemInfo.getTaskId());
+        }
         if (tag instanceof GroupTask groupTask && !groupTask.hasMultipleTasks()) {
-            if (minimizedTaskIds.contains(groupTask.task1.key.id)) {
-                return BubbleTextView.RunningAppState.MINIMIZED;
-            }
-            if (runningTaskIds.contains(groupTask.task1.key.id)) {
-                return BubbleTextView.RunningAppState.RUNNING;
-            }
+            return mControllers.taskbarRecentAppsController.getRunningAppState(
+                    groupTask.task1.key.id);
         }
         return BubbleTextView.RunningAppState.NOT_RUNNING;
     }
@@ -770,9 +779,16 @@
         }
     }
 
-    /** Resets the icon alignment controller so that it can be recreated again later. */
-    void resetIconAlignmentController() {
+    /**
+     * Resets the icon alignment controller so that it can be recreated again later, and updates
+     * the list of icons shown in the taskbar if the bubble bar visibility changes the taskbar
+     * overflow state.
+     */
+    void adjustTaskbarForBubbleBar() {
         mIconAlignControllerLazy = null;
+        if (mTaskbarView.updateMaxNumIcons()) {
+            commitRunningAppsToUI();
+        }
     }
 
     /**
@@ -812,6 +828,13 @@
                 : mPersistentTaskbarDp.taskbarBottomMargin;
 
         int firstRecentTaskIndex = -1;
+        int hotseatNavBarTranslationX = 0;
+        if (mCurrentBubbleBarLocation != null) {
+            boolean isBubblesOnLeft = mCurrentBubbleBarLocation
+                    .isOnLeft(mTaskbarView.isLayoutRtl());
+            hotseatNavBarTranslationX = taskbarDp
+                    .getHotseatTranslationXForNavBar(mActivity, isBubblesOnLeft);
+        }
         for (int i = 0; i < mTaskbarView.getChildCount(); i++) {
             View child = mTaskbarView.getChildAt(i);
             boolean isAllAppsButton = child == mTaskbarView.getAllAppsButtonContainer();
@@ -847,16 +870,20 @@
                                     : Interpolators.clampToProgress(LINEAR, 0.72f, 0.84f));
                 }
             }
-
             if (child == mTaskbarView.getQsb()) {
                 boolean isRtl = Utilities.isRtl(child.getResources());
                 float hotseatIconCenter = isRtl
                         ? launcherDp.widthPx - hotseatPadding.right + borderSpacing
                         + launcherDp.hotseatQsbWidth / 2f
                         : hotseatPadding.left - borderSpacing - launcherDp.hotseatQsbWidth / 2f;
+                if (taskbarDp.isQsbInline) {
+                    hotseatIconCenter += hotseatNavBarTranslationX;
+                }
                 float childCenter = (child.getLeft() + child.getRight()) / 2f;
-                childCenter += ((Reorderable) child).getTranslateDelegate().getTranslationX(
-                        INDEX_TASKBAR_PINNING_ANIM).getValue();
+                if (child instanceof Reorderable reorderableChild) {
+                    childCenter += reorderableChild.getTranslateDelegate().getTranslationX(
+                            INDEX_TASKBAR_PINNING_ANIM).getValue();
+                }
                 float halfQsbIconWidthDiff =
                         (launcherDp.hotseatQsbWidth - taskbarDp.taskbarIconSize) / 2f;
                 float scale = ((float) taskbarDp.taskbarIconSize)
@@ -865,8 +892,8 @@
 
                 float fromX = isRtl ? -halfQsbIconWidthDiff : halfQsbIconWidthDiff;
                 float toX = hotseatIconCenter - childCenter;
-                if (child instanceof Reorderable) {
-                    MultiTranslateDelegate mtd = ((Reorderable) child).getTranslateDelegate();
+                if (child instanceof Reorderable reorderableChild) {
+                    MultiTranslateDelegate mtd = reorderableChild.getTranslateDelegate();
 
                     setter.addFloat(mtd.getTranslationX(INDEX_TASKBAR_ALIGNMENT_ANIM),
                             MULTI_PROPERTY_VALUE, fromX, toX, interpolator);
@@ -901,10 +928,12 @@
                     mTaskbarView.isDividerForRecents(), recentTaskIndex);
             if (positionInHotseat == ERROR_POSITION_IN_HOTSEAT_NOT_FOUND) continue;
 
-            float hotseatAdjustedBorderSpace =
-                    launcherDp.getHotseatAdjustedBorderSpaceForBubbleBar(child.getContext());
+
             float hotseatIconCenter;
-            if (bubbleBarHasBubbles() && hotseatAdjustedBorderSpace != 0) {
+            if (launcherDp.shouldAdjustHotseatForBubbleBar(child.getContext(),
+                    bubbleBarHasBubbles())) {
+                float hotseatAdjustedBorderSpace =
+                        launcherDp.getHotseatAdjustedBorderSpaceForBubbleBar(child.getContext());
                 hotseatIconCenter = hotseatPadding.left + hotseatCellSize
                         + (hotseatCellSize + hotseatAdjustedBorderSpace) * positionInHotseat
                         + hotseatCellSize / 2f;
@@ -913,6 +942,7 @@
                         + (hotseatCellSize + borderSpacing) * positionInHotseat
                         + hotseatCellSize / 2f;
             }
+            hotseatIconCenter += hotseatNavBarTranslationX;
             float childCenter = (child.getLeft() + child.getRight()) / 2f;
             childCenter += ((Reorderable) child).getTranslateDelegate().getTranslationX(
                     INDEX_TASKBAR_PINNING_ANIM).getValue();
@@ -1053,6 +1083,8 @@
                 if (groupTask.containsTask(task.key.id)) {
                     mTaskbarView.applyGroupTaskToBubbleTextView(btv, groupTask);
                 }
+            } else if (view instanceof TaskbarOverflowView overflowButton) {
+                overflowButton.updateTaskIsShown(task);
             }
         }
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt
index f08318e..249773d 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt
@@ -43,9 +43,10 @@
     private val arrowTipRadius: Float
     private val arrowVisibleHeight: Float
 
-    private val shadowAlpha: Float
-    private var shadowBlur = 0f
-    private var keyShadowDistance = 0f
+    private val strokeAlpha: Int
+    private val shadowAlpha: Int
+    private val shadowBlur: Float
+    private val keyShadowDistance: Float
     private var arrowHeightFraction = 1f
 
     var arrowPositionX: Float = 0f
@@ -105,13 +106,13 @@
         strokePaint.strokeWidth = res.getDimension(R.dimen.transient_taskbar_stroke_width)
         // apply theme alpha attributes
         if (Utilities.isDarkTheme(context)) {
-            strokePaint.alpha = DARK_THEME_STROKE_ALPHA
+            strokeAlpha = DARK_THEME_STROKE_ALPHA
             shadowAlpha = DARK_THEME_SHADOW_ALPHA
         } else {
-            strokePaint.alpha = LIGHT_THEME_STROKE_ALPHA
+            strokeAlpha = LIGHT_THEME_STROKE_ALPHA
             shadowAlpha = LIGHT_THEME_SHADOW_ALPHA
         }
-
+        strokePaint.alpha = strokeAlpha
         shadowBlur = res.getDimension(R.dimen.transient_taskbar_shadow_blur)
         keyShadowDistance = res.getDimension(R.dimen.transient_taskbar_key_shadow_distance)
         arrowWidth = res.getDimension(R.dimen.bubblebar_pointer_width)
@@ -132,15 +133,14 @@
     override fun draw(canvas: Canvas) {
         canvas.save()
 
-        // TODO (b/277359345): Should animate the alpha similar to taskbar (see TaskbarDragLayer)
         // Draw shadows.
         val newShadowAlpha =
-            mapToRange(fillPaint.alpha.toFloat(), 0f, 255f, 0f, shadowAlpha, Interpolators.LINEAR)
+            mapToRange(fillPaint.alpha, 0, 255, 0, shadowAlpha, Interpolators.LINEAR)
         fillPaint.setShadowLayer(
             shadowBlur,
             0f,
             keyShadowDistance,
-            setColorAlphaBound(Color.BLACK, Math.round(newShadowAlpha))
+            setColorAlphaBound(Color.BLACK, newShadowAlpha),
         )
         // Create background path
         val backgroundPath = Path()
@@ -172,7 +172,7 @@
             arrowWidth,
             scaledHeight,
             arrowTipRadius,
-            arrowPath
+            arrowPath,
         )
         // flip it horizontally
         val pathTransform = Matrix()
@@ -196,6 +196,7 @@
 
     override fun setAlpha(alpha: Int) {
         fillPaint.alpha = alpha
+        strokePaint.alpha = mapToRange(alpha, 0, 255, 0, strokeAlpha, Interpolators.LINEAR)
         invalidateSelf()
     }
 
@@ -237,7 +238,7 @@
     companion object {
         private const val DARK_THEME_STROKE_ALPHA = 51
         private const val LIGHT_THEME_STROKE_ALPHA = 41
-        private const val DARK_THEME_SHADOW_ALPHA = 51f
-        private const val LIGHT_THEME_SHADOW_ALPHA = 25f
+        private const val DARK_THEME_SHADOW_ALPHA = 51
+        private const val LIGHT_THEME_SHADOW_ALPHA = 25
     }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
index 6860004..e0814d3 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
@@ -35,6 +35,7 @@
 import android.util.ArrayMap;
 import android.util.Log;
 
+import com.android.launcher3.taskbar.TaskbarSharedState;
 import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController;
 import com.android.launcher3.util.Executors.SimpleThreadFactory;
 import com.android.quickstep.SystemUiProxy;
@@ -47,6 +48,7 @@
 import com.android.wm.shell.shared.bubbles.RemovedBubble;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 import java.util.Objects;
 import java.util.Optional;
@@ -112,6 +114,7 @@
 
     private BubbleBarItem mSelectedBubble;
 
+    private TaskbarSharedState mSharedState;
     private ImeVisibilityChecker mImeVisibilityChecker;
     private BubbleBarViewController mBubbleBarViewController;
     private BubbleStashController mBubbleStashController;
@@ -173,12 +176,25 @@
 
     public void onDestroy() {
         mSystemUiProxy.setBubblesListener(null);
+        // Saves bubble bar state
+        BubbleInfo[] bubbleInfoItems = new BubbleInfo[mBubbles.size()];
+        mBubbles.values().forEach(bubbleBarBubble -> {
+            int index = mBubbleBarViewController.bubbleViewIndex(bubbleBarBubble.getView());
+            if (index < 0 || index >= bubbleInfoItems.length) {
+                Log.e(TAG, "Found improper index: " + index + " for " + bubbleBarBubble);
+            } else {
+                bubbleInfoItems[index] = bubbleBarBubble.getInfo();
+            }
+        });
+        mSharedState.bubbleInfoItems = Arrays.asList(bubbleInfoItems);
     }
 
     /** Initializes controllers. */
     public void init(BubbleControllers bubbleControllers,
             BubbleBarLocationListener bubbleBarLocationListener,
-            ImeVisibilityChecker imeVisibilityChecker) {
+            ImeVisibilityChecker imeVisibilityChecker,
+            TaskbarSharedState sharedState) {
+        mSharedState = sharedState;
         mImeVisibilityChecker = imeVisibilityChecker;
         mBubbleBarViewController = bubbleControllers.bubbleBarViewController;
         mBubbleStashController = bubbleControllers.bubbleStashController;
@@ -188,6 +204,7 @@
         mBubbleBarLocationListener = bubbleBarLocationListener;
 
         bubbleControllers.runAfterInit(() -> {
+            restoreSavedState(sharedState);
             mBubbleBarViewController.setHiddenForBubbles(
                     !sBubbleBarEnabled || mBubbles.isEmpty());
             mBubbleStashedHandleViewController.ifPresent(
@@ -196,7 +213,8 @@
             mBubbleBarViewController.setUpdateSelectedBubbleAfterCollapse(
                     key -> setSelectedBubbleInternal(mBubbles.get(key)));
             mBubbleBarViewController.setBoundsChangeListener(this::onBubbleBarBoundsChanged);
-
+            mBubbleBarLocationListener.onBubbleBarLocationUpdated(
+                    mBubbleBarViewController.getBubbleBarLocation());
             if (sBubbleBarEnabled) {
                 mSystemUiProxy.setBubblesListener(this);
             }
@@ -265,6 +283,24 @@
         }
     }
 
+    private void restoreSavedState(TaskbarSharedState sharedState) {
+        if (sharedState.bubbleBarLocation != null) {
+            updateBubbleBarLocationInternal(sharedState.bubbleBarLocation);
+        }
+        List<BubbleInfo> bubbleInfos = sharedState.bubbleInfoItems;
+        if (bubbleInfos == null || bubbleInfos.isEmpty()) return;
+        // Iterate in reverse because new bubbles are added in front and the list is in order.
+        for (int i = bubbleInfos.size() - 1; i >= 0; i--) {
+            BubbleBarBubble bubble = mBubbleCreator.populateBubble(mContext,
+                    bubbleInfos.get(i), mBarView, /* existingBubble = */ null);
+            if (bubble == null) {
+                Log.e(TAG, "Could not instantiate BubbleBarBubble for " + bubbleInfos.get(i));
+                continue;
+            }
+            addBubbleInternally(bubble, /* isExpanding= */ false, /* suppressAnimation= */ true);
+        }
+    }
+
     private void applyViewChanges(BubbleBarViewUpdate update) {
         final boolean isCollapsed = (update.expandedChanged && !update.expanded)
                 || (!update.expandedChanged && !mBubbleBarViewController.isExpanded());
@@ -276,6 +312,12 @@
                 update.initialState || mBubbleBarViewController.isHiddenForSysui()
                         || mImeVisibilityChecker.isImeVisible();
 
+        if (update.initialState && mSharedState.hasSavedBubbles()) {
+            // clear restored state
+            mBubbleBarViewController.removeAllBubbles();
+            mBubbles.clear();
+        }
+
         BubbleBarBubble bubbleToSelect = null;
 
         if (Flags.enableOptionalBubbleOverflow()
@@ -346,8 +388,7 @@
             for (int i = update.currentBubbles.size() - 1; i >= 0; i--) {
                 BubbleBarBubble bubble = update.currentBubbles.get(i);
                 if (bubble != null) {
-                    mBubbles.put(bubble.getKey(), bubble);
-                    mBubbleBarViewController.addBubble(bubble, isExpanding, suppressAnimation);
+                    addBubbleInternally(bubble, isExpanding, suppressAnimation);
                     if (isCollapsed) {
                         // If we're collapsed, the most recently added bubble will be selected.
                         bubbleToSelect = bubble;
@@ -376,10 +417,13 @@
             // Updates mean the dot state may have changed; any other changes were updated in
             // the populateBubble step.
             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, /* isUpdate= */ true);
+            if (suppressAnimation) {
+                // since we're not animating this update, we should update the dot visibility here.
+                bb.getView().updateDotVisibility(/* animate= */ false);
+            } else {
+                mBubbleBarViewController.animateBubbleNotification(
+                        bb, /* isExpanding= */ false, /* isUpdate= */ true);
+            }
         }
         if (update.bubbleKeysInOrder != null && !update.bubbleKeysInOrder.isEmpty()) {
             // Create the new list
@@ -421,6 +465,7 @@
             }
         }
         if (update.bubbleBarLocation != null) {
+            mSharedState.bubbleBarLocation = update.bubbleBarLocation;
             if (update.bubbleBarLocation != mBubbleBarViewController.getBubbleBarLocation()) {
                 updateBubbleBarLocationInternal(update.bubbleBarLocation);
             }
@@ -483,9 +528,10 @@
      * <p>
      * Updates the value locally in Launcher and in WMShell.
      */
-    public void updateBubbleBarLocation(BubbleBarLocation location) {
+    public void updateBubbleBarLocation(BubbleBarLocation location,
+            @BubbleBarLocation.UpdateSource int source) {
         updateBubbleBarLocationInternal(location);
-        mSystemUiProxy.setBubbleBarLocation(location);
+        mSystemUiProxy.setBubbleBarLocation(location, source);
     }
 
     private void updateBubbleBarLocationInternal(BubbleBarLocation location) {
@@ -520,6 +566,12 @@
         }
     }
 
+    private void addBubbleInternally(BubbleBarBubble bubble, boolean isExpanding,
+            boolean suppressAnimation) {
+        mBubbles.put(bubble.getKey(), bubble);
+        mBubbleBarViewController.addBubble(bubble, isExpanding, suppressAnimation);
+    }
+
     /** Interface for checking whether the IME is visible. */
     public interface ImeVisibilityChecker {
         /** Whether the IME is visible. */
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarItem.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarItem.kt
index 7a32ef1..680ffca 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarItem.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarItem.kt
@@ -17,10 +17,11 @@
 
 import android.graphics.Bitmap
 import android.graphics.Path
+import com.android.launcher3.taskbar.bubbles.flyout.BubbleBarFlyoutMessage
 import com.android.wm.shell.shared.bubbles.BubbleInfo
 
 /** An entity in the bubble bar. */
-sealed class BubbleBarItem(open var key: String, open var view: BubbleView)
+sealed class BubbleBarItem(open val key: String, open var view: BubbleView)
 
 /** Contains state info about a bubble in the bubble bar as well as presentation information. */
 data class BubbleBarBubble(
@@ -30,7 +31,8 @@
     var icon: Bitmap,
     var dotColor: Int,
     var dotPath: Path,
-    var appName: String
+    var appName: String,
+    var flyoutMessage: BubbleBarFlyoutMessage?,
 ) : BubbleBarItem(info.key, view)
 
 /** Represents the overflow bubble in the bubble bar. */
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarSwipeController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarSwipeController.kt
new file mode 100644
index 0000000..2d3642b
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarSwipeController.kt
@@ -0,0 +1,216 @@
+/*
+ * 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.bubbles
+
+import android.animation.ValueAnimator
+import android.content.Context
+import androidx.annotation.VisibleForTesting
+import androidx.core.animation.doOnEnd
+import androidx.dynamicanimation.animation.SpringForce
+import com.android.launcher3.anim.AnimatedFloat
+import com.android.launcher3.anim.SpringAnimationBuilder
+import com.android.launcher3.taskbar.TaskbarActivityContext
+import com.android.launcher3.taskbar.TaskbarThresholdUtils
+import com.android.launcher3.taskbar.bubbles.BubbleBarSwipeController.BarState.COLLAPSED
+import com.android.launcher3.taskbar.bubbles.BubbleBarSwipeController.BarState.EXPANDED
+import com.android.launcher3.taskbar.bubbles.BubbleBarSwipeController.BarState.STASHED
+import com.android.launcher3.taskbar.bubbles.BubbleBarSwipeController.BarState.UNKNOWN
+import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController
+import com.android.launcher3.touch.OverScroll
+
+/** Handle swipe events on the bubble bar and handle */
+class BubbleBarSwipeController {
+
+    private val context: Context
+
+    private var bubbleStashedHandleViewController: BubbleStashedHandleViewController? = null
+    private lateinit var bubbleBarViewController: BubbleBarViewController
+    private lateinit var bubbleStashController: BubbleStashController
+
+    private var springAnimation: ValueAnimator? = null
+    private val animatedSwipeTranslation = AnimatedFloat(this::onSwipeUpdate)
+
+    private val unstashThreshold: Int
+    private val maxOverscroll: Int
+
+    private var swipeState: SwipeState = SwipeState(startState = UNKNOWN)
+
+    constructor(tac: TaskbarActivityContext) : this(tac, DefaultDimensionProvider(tac))
+
+    @VisibleForTesting
+    constructor(context: Context, dimensionProvider: DimensionProvider) {
+        this.context = context
+        unstashThreshold = dimensionProvider.unstashThreshold
+        maxOverscroll = dimensionProvider.maxOverscroll
+    }
+
+    fun init(bubbleControllers: BubbleControllers) {
+        bubbleStashedHandleViewController =
+            bubbleControllers.bubbleStashedHandleViewController.orElse(null)
+        bubbleBarViewController = bubbleControllers.bubbleBarViewController
+        bubbleStashController = bubbleControllers.bubbleStashController
+    }
+
+    /** Start tracking a new swipe gesture */
+    fun start() {
+        if (springAnimation != null) reset()
+        val startState =
+            when {
+                bubbleStashController.isStashed -> STASHED
+                bubbleBarViewController.isExpanded -> EXPANDED
+                bubbleStashController.isBubbleBarVisible() -> COLLAPSED
+                else -> UNKNOWN
+            }
+        swipeState = SwipeState(startState = startState, currentState = startState)
+    }
+
+    /** Update swipe distance to [dy] */
+    fun swipeTo(dy: Float) {
+        if (!canHandleSwipe(dy)) {
+            return
+        }
+        animatedSwipeTranslation.updateValue(dy)
+
+        swipeState.passedUnstash = isUnstash(dy)
+        // Tracking swipe gesture if we pass unstash threshold at least once during gesture
+        swipeState.isSwipe = swipeState.isSwipe || swipeState.passedUnstash
+        when {
+            canUnstash() && swipeState.passedUnstash -> {
+                swipeState.currentState = COLLAPSED
+                bubbleStashController.showBubbleBar(expandBubbles = false)
+            }
+            canStash() && !swipeState.passedUnstash -> {
+                swipeState.currentState = STASHED
+                bubbleStashController.stashBubbleBar()
+            }
+        }
+    }
+
+    /** Finish tracking swipe gesture. Animate views back to resting state */
+    fun finish() {
+        if (swipeState.passedUnstash && swipeState.startState in setOf(STASHED, COLLAPSED)) {
+            bubbleStashController.showBubbleBar(expandBubbles = true)
+        }
+        if (animatedSwipeTranslation.value == 0f) {
+            reset()
+        } else {
+            springToRest()
+        }
+    }
+
+    /** Returns `true` if we are tracking a swipe gesture */
+    fun isSwipeGesture(): Boolean {
+        return swipeState.isSwipe
+    }
+
+    private fun canHandleSwipe(dy: Float): Boolean {
+        return when (swipeState.startState) {
+            STASHED -> {
+                if (swipeState.currentState == COLLAPSED) {
+                    // if we have unstashed the bar, allow swipe in both directions
+                    true
+                } else {
+                    // otherwise, only allow swipe up on stash handle
+                    dy < 0
+                }
+            }
+            COLLAPSED -> dy < 0 // collapsed bar can only be swiped up
+            UNKNOWN,
+            EXPANDED -> false // expanded bar can't be swiped
+        }
+    }
+
+    private fun isUnstash(dy: Float): Boolean {
+        return dy < -unstashThreshold
+    }
+
+    private fun canStash(): Boolean {
+        // Only allow stashing if we started from stashed state
+        return swipeState.startState == STASHED && swipeState.currentState == COLLAPSED
+    }
+
+    private fun canUnstash(): Boolean {
+        return swipeState.currentState == STASHED
+    }
+
+    private fun reset() {
+        springAnimation?.let {
+            if (it.isRunning) {
+                it.removeAllListeners()
+                it.cancel()
+                animatedSwipeTranslation.updateValue(0f)
+            }
+        }
+        springAnimation = null
+        swipeState = SwipeState(startState = UNKNOWN)
+    }
+
+    private fun onSwipeUpdate(value: Float) {
+        val dampedSwipe = -OverScroll.dampedScroll(-value, maxOverscroll).toFloat()
+        bubbleStashedHandleViewController?.setTranslationYForSwipe(dampedSwipe)
+        bubbleBarViewController.setTranslationYForSwipe(dampedSwipe)
+    }
+
+    private fun springToRest() {
+        springAnimation =
+            SpringAnimationBuilder(context)
+                .setStartValue(animatedSwipeTranslation.value)
+                .setEndValue(0f)
+                .setDampingRatio(SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY)
+                .setStiffness(SpringForce.STIFFNESS_LOW)
+                .build(animatedSwipeTranslation, AnimatedFloat.VALUE)
+                .also { it.doOnEnd { reset() } }
+        springAnimation?.start()
+    }
+
+    internal data class SwipeState(
+        val startState: BarState,
+        var currentState: BarState = UNKNOWN,
+        var passedUnstash: Boolean = false,
+        var isSwipe: Boolean = false,
+    )
+
+    internal enum class BarState {
+        UNKNOWN,
+        STASHED,
+        COLLAPSED,
+        EXPANDED,
+    }
+
+    /** Allows overriding the dimension provider for testing */
+    @VisibleForTesting
+    interface DimensionProvider {
+        val unstashThreshold: Int
+        val maxOverscroll: Int
+    }
+
+    private class DefaultDimensionProvider(taskbarActivityContext: TaskbarActivityContext) :
+        DimensionProvider {
+        override val unstashThreshold: Int
+        override val maxOverscroll: Int
+
+        init {
+            val resources = taskbarActivityContext.resources
+            unstashThreshold =
+                TaskbarThresholdUtils.getFromNavThreshold(
+                    resources,
+                    taskbarActivityContext.deviceProfile,
+                )
+            maxOverscroll = taskbarActivityContext.deviceProfile.heightPx - unstashThreshold
+        }
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
index d454fd7..c5c2d69 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -15,13 +15,9 @@
  */
 package com.android.launcher3.taskbar.bubbles;
 
-import static com.android.app.animation.Interpolators.EMPHASIZED_ACCELERATE;
 import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA;
-import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X;
 
 import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
 import android.animation.ValueAnimator;
 import android.annotation.NonNull;
@@ -42,10 +38,10 @@
 import android.view.accessibility.AccessibilityNodeInfo;
 import android.widget.FrameLayout;
 
-import androidx.dynamicanimation.animation.SpringForce;
-
+import com.android.app.animation.Interpolators;
 import com.android.launcher3.R;
-import com.android.launcher3.anim.SpringAnimationBuilder;
+import com.android.launcher3.anim.AnimatorListeners;
+import com.android.launcher3.taskbar.BarsLocationAnimatorHelper;
 import com.android.launcher3.taskbar.bubbles.animation.BubbleAnimator;
 import com.android.launcher3.util.DisplayController;
 import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
@@ -83,28 +79,17 @@
  */
 public class BubbleBarView extends FrameLayout {
 
+    public static final long FADE_OUT_ANIM_POSITION_DURATION_MS = 100L;
+    public static final long FADE_IN_ANIM_ALPHA_DURATION_MS = 100L;
     private static final String TAG = "BubbleBarView";
-
     // TODO: (b/273594744) calculate the amount of space we have and base the max on that
     //  if it's smaller than 5.
     private static final int MAX_BUBBLES = 5;
     private static final int MAX_VISIBLE_BUBBLES_COLLAPSED = 2;
     private static final int ARROW_POSITION_ANIMATION_DURATION_MS = 200;
-    private static final int WIDTH_ANIMATION_DURATION_MS = 200;
+    private static final int WIDTH_ANIMATION_DURATION_MS = 400;
     private static final int SCALE_ANIMATION_DURATION_MS = 200;
 
-    private static final long FADE_OUT_ANIM_ALPHA_DURATION_MS = 50L;
-    private static final long FADE_OUT_ANIM_ALPHA_DELAY_MS = 50L;
-    public static final long FADE_OUT_ANIM_POSITION_DURATION_MS = 100L;
-    // During fade out animation we shift the bubble bar 1/80th of the screen width
-    private static final float FADE_OUT_ANIM_POSITION_SHIFT = 1 / 80f;
-
-    public static final long FADE_IN_ANIM_ALPHA_DURATION_MS = 100L;
-    // Use STIFFNESS_MEDIUMLOW which is not defined in the API constants
-    private static final float FADE_IN_ANIM_POSITION_SPRING_STIFFNESS = 400f;
-    // During fade in animation we shift the bubble bar 1/60th of the screen width
-    private static final float FADE_IN_ANIM_POSITION_SHIFT = 1 / 60f;
-
     /**
      * Custom property to set alpha value for the bar view while a bubble is being dragged.
      * Skips applying alpha to the dragged bubble.
@@ -159,7 +144,7 @@
 
     // An animator that represents the expansion state of the bubble bar, where 0 corresponds to the
     // collapsed state and 1 to the fully expanded state.
-    private final ValueAnimator mWidthAnimator = ValueAnimator.ofFloat(0, 1);
+    private ValueAnimator mWidthAnimator = createExpansionAnimator(/* expanding = */ false);
 
     /** An animator used for animating individual bubbles in the bubble bar while expanded. */
     @Nullable
@@ -223,35 +208,6 @@
 
         mBubbleBarBackground = new BubbleBarBackground(context, getBubbleBarExpandedHeight());
         setBackgroundDrawable(mBubbleBarBackground);
-
-        mWidthAnimator.setDuration(WIDTH_ANIMATION_DURATION_MS);
-
-        addAnimationCallBacks(mWidthAnimator,
-                /* onStart= */ () -> mBubbleBarBackground.showArrow(true),
-                /* onEnd= */ () -> {
-                    mBubbleBarBackground.showArrow(mIsBarExpanded);
-                    if (!mIsBarExpanded && mReorderRunnable != null) {
-                        mReorderRunnable.run();
-                        mReorderRunnable = null;
-                    }
-                    // If the bar was just collapsed and the overflow was the last bubble that was
-                    // selected, set the first bubble as selected.
-                    if (!mIsBarExpanded && mUpdateSelectedBubbleAfterCollapse != null
-                            && mSelectedBubbleView != null
-                            && mSelectedBubbleView.getBubble() instanceof BubbleBarOverflow) {
-                        BubbleView firstBubble = (BubbleView) getChildAt(0);
-                        mUpdateSelectedBubbleAfterCollapse.accept(firstBubble.getBubble().getKey());
-                    }
-                    // If the bar was just expanded, remove the dot from the selected bubble.
-                    if (mIsBarExpanded && mSelectedBubbleView != null) {
-                        mSelectedBubbleView.markSeen();
-                    }
-                    updateLayoutParams();
-                },
-                /* onUpdate= */ animator -> {
-                    updateBubblesLayoutProperties(mBubbleBarLocation);
-                    invalidate();
-                });
     }
 
 
@@ -348,6 +304,17 @@
     }
 
     /**
+     * Set the bubble icons size and spacing between the bubbles and the borders of the bubble
+     * bar.
+     */
+    public void setIconSizeAndPaddingForPinning(float newIconSize, float newBubbleBarPadding) {
+        mBubbleBarPadding = newBubbleBarPadding;
+        mIconScale = newIconSize / mIconSize;
+        updateBubblesLayoutProperties(mBubbleBarLocation);
+        invalidate();
+    }
+
+    /**
      * Sets new icon sizes and newBubbleBarPadding between icons and bubble bar borders.
      *
      * @param newIconSize         new icon size
@@ -440,11 +407,13 @@
             return true;
         }
         if (action == R.id.action_move_left) {
-            mController.updateBubbleBarLocation(BubbleBarLocation.LEFT);
+            mController.updateBubbleBarLocation(BubbleBarLocation.LEFT,
+                    BubbleBarLocation.UpdateSource.A11Y_ACTION_BAR);
             return true;
         }
         if (action == R.id.action_move_right) {
-            mController.updateBubbleBarLocation(BubbleBarLocation.RIGHT);
+            mController.updateBubbleBarLocation(BubbleBarLocation.RIGHT,
+                    BubbleBarLocation.UpdateSource.A11Y_ACTION_BAR);
             return true;
         }
         return false;
@@ -578,77 +547,30 @@
         // First animator hides the bar.
         // After it completes, bubble positions in the bar and arrow position is updated.
         // Second animator is started to show the bar.
-        mBubbleBarLocationAnimator = getLocationUpdateFadeOutAnimator(bubbleBarLocation);
-        mBubbleBarLocationAnimator.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                updateBubblesLayoutProperties(bubbleBarLocation);
-                mBubbleBarBackground.setAnchorLeft(bubbleBarLocation.isOnLeft(isLayoutRtl()));
+        ObjectAnimator alphaOutAnim = ObjectAnimator.ofFloat(
+                this, getLocationAnimAlphaProperty(), 0f);
+        mBubbleBarLocationAnimator = BarsLocationAnimatorHelper.getBubbleBarLocationOutAnimator(
+                this,
+                bubbleBarLocation,
+                alphaOutAnim);
+        mBubbleBarLocationAnimator.addListener(AnimatorListeners.forEndCallback(() -> {
+            updateBubblesLayoutProperties(bubbleBarLocation);
+            mBubbleBarBackground.setAnchorLeft(bubbleBarLocation.isOnLeft(isLayoutRtl()));
+            ObjectAnimator alphaInAnim = ObjectAnimator.ofFloat(BubbleBarView.this,
+                    getLocationAnimAlphaProperty(), 1f);
 
-                // Animate it in
-                mBubbleBarLocationAnimator = getLocationUpdateFadeInAnimator(bubbleBarLocation);
-                mBubbleBarLocationAnimator.start();
-            }
-        });
+            // Animate it in
+            mBubbleBarLocationAnimator = BarsLocationAnimatorHelper.getBubbleBarLocationInAnimator(
+                    bubbleBarLocation,
+                    mBubbleBarLocation,
+                    getDistanceFromOtherSide(),
+                    alphaInAnim,
+                    BubbleBarView.this);
+            mBubbleBarLocationAnimator.start();
+        }));
         mBubbleBarLocationAnimator.start();
     }
 
-    private Animator getLocationUpdateFadeOutAnimator(BubbleBarLocation newLocation) {
-        final float shift =
-                getResources().getDisplayMetrics().widthPixels * FADE_OUT_ANIM_POSITION_SHIFT;
-        final boolean onLeft = newLocation.isOnLeft(isLayoutRtl());
-        final float tx = getTranslationX() + (onLeft ? -shift : shift);
-
-        ObjectAnimator positionAnim = ObjectAnimator.ofFloat(this, VIEW_TRANSLATE_X, tx)
-                .setDuration(FADE_OUT_ANIM_POSITION_DURATION_MS);
-        positionAnim.setInterpolator(EMPHASIZED_ACCELERATE);
-
-        ObjectAnimator alphaAnim = ObjectAnimator.ofFloat(this, getLocationAnimAlphaProperty(), 0f)
-                .setDuration(FADE_OUT_ANIM_ALPHA_DURATION_MS);
-        alphaAnim.setStartDelay(FADE_OUT_ANIM_ALPHA_DELAY_MS);
-
-        AnimatorSet animatorSet = new AnimatorSet();
-        animatorSet.playTogether(positionAnim, alphaAnim);
-        return animatorSet;
-    }
-
-    private Animator getLocationUpdateFadeInAnimator(BubbleBarLocation newLocation) {
-        final float shift =
-                getResources().getDisplayMetrics().widthPixels * FADE_IN_ANIM_POSITION_SHIFT;
-
-        final boolean onLeft = newLocation.isOnLeft(isLayoutRtl());
-        final float startTx;
-        final float finalTx;
-        if (newLocation == mBubbleBarLocation) {
-            // Animated location matches layout location.
-            finalTx = 0;
-        } else {
-            // We are animating in to a transient location, need to move the bar accordingly.
-            finalTx = getDistanceFromOtherSide() * (onLeft ? -1 : 1);
-        }
-        if (onLeft) {
-            // Bar will be shown on the left side. Start point is shifted right.
-            startTx = finalTx + shift;
-        } else {
-            // Bar will be shown on the right side. Start point is shifted left.
-            startTx = finalTx - shift;
-        }
-
-        ValueAnimator positionAnim = new SpringAnimationBuilder(getContext())
-                .setStartValue(startTx)
-                .setEndValue(finalTx)
-                .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
-                .setStiffness(FADE_IN_ANIM_POSITION_SPRING_STIFFNESS)
-                .build(this, VIEW_TRANSLATE_X);
-
-        ObjectAnimator alphaAnim = ObjectAnimator.ofFloat(this, getLocationAnimAlphaProperty(), 1f)
-                .setDuration(FADE_IN_ANIM_ALPHA_DURATION_MS);
-
-        AnimatorSet animatorSet = new AnimatorSet();
-        animatorSet.playTogether(positionAnim, alphaAnim);
-        return animatorSet;
-    }
-
     /**
      * Get property that can be used to animate the alpha value for the bar.
      * When a bubble is being dragged, uses {@link #BUBBLE_DRAG_ALPHA}.
@@ -871,6 +793,7 @@
         updateLayoutParams();
         updateBubbleAccessibilityStates();
         updateContentDescription();
+        updateDotsAndBadgesIfCollapsed();
     }
 
     /** Removes the given bubble from the bubble bar. */
@@ -936,7 +859,7 @@
         updateBubbleAccessibilityStates();
         updateContentDescription();
         mDismissedByDragBubbleView = null;
-        updateNotificationDotsIfCollapsed();
+        updateDotsAndBadgesIfCollapsed();
     }
 
     /**
@@ -966,17 +889,23 @@
         return childViews;
     }
 
-    private void updateNotificationDotsIfCollapsed() {
+    private void updateDotsAndBadgesIfCollapsed() {
         if (isExpanded()) {
             return;
         }
         for (int i = 0; i < getChildCount(); i++) {
             BubbleView bubbleView = (BubbleView) getChildAt(i);
-            // when we're collapsed, the first bubble should show the dot if it has it. the rest of
-            // the bubbles should hide their dots.
-            if (i == 0 && bubbleView.hasUnseenContent()) {
-                bubbleView.showDotIfNeeded(/* animate= */ true);
+            // when we're collapsed, the first bubble should show the badge and the dot if it has
+            // it. the rest of the bubbles should hide their badges and dots.
+            if (i == 0) {
+                bubbleView.showBadge();
+                if (bubbleView.hasUnseenContent()) {
+                    bubbleView.showDotIfNeeded(/* animate= */ true);
+                } else {
+                    bubbleView.hideDot();
+                }
             } else {
+                bubbleView.hideBadge();
                 bubbleView.hideDot();
             }
         }
@@ -1180,7 +1109,7 @@
             }
             updateBubblesLayoutProperties(mBubbleBarLocation);
             updateContentDescription();
-            updateNotificationDotsIfCollapsed();
+            updateDotsAndBadgesIfCollapsed();
         }
     }
 
@@ -1314,11 +1243,8 @@
             mIsBarExpanded = isBarExpanded;
             updateArrowForSelected(/* shouldAnimate= */ false);
             setOrUnsetClickListener();
-            if (isBarExpanded) {
-                mWidthAnimator.start();
-            } else {
-                mWidthAnimator.reverse();
-            }
+            mWidthAnimator = createExpansionAnimator(isBarExpanded);
+            mWidthAnimator.start();
             updateBubbleAccessibilityStates();
             announceExpandedStateChange();
         }
@@ -1364,10 +1290,14 @@
         // If there are more than 2 bubbles, the first 2 should be visible when collapsed,
         // excluding the overflow.
         return bubbleChildCount >= MAX_VISIBLE_BUBBLES_COLLAPSED
-                ? getScaledIconSize() + mIconOverlapAmount + horizontalPadding
+                ? getCollapsedWidthWithMaxVisibleBubbles()
                 : getScaledIconSize() + horizontalPadding;
     }
 
+    float getCollapsedWidthWithMaxVisibleBubbles()  {
+        return getScaledIconSize() + mIconOverlapAmount + 2 * mBubbleBarPadding;
+    }
+
     /** Returns the child count excluding the overflow if it's present. */
     int getBubbleChildCount() {
         return hasOverflow() ? getChildCount() - 1 : getChildCount();
@@ -1555,6 +1485,78 @@
         return bubbles;
     }
 
+    /** Creates an animator based on the expanding or collapsing action. */
+    private ValueAnimator createExpansionAnimator(boolean expanding) {
+        float startValue = expanding ? 0 : 1;
+        if ((mWidthAnimator != null && mWidthAnimator.isRunning())) {
+            startValue = (float) mWidthAnimator.getAnimatedValue();
+            mWidthAnimator.cancel();
+        }
+        float endValue = expanding ? 1 : 0;
+        ValueAnimator animator = ValueAnimator.ofFloat(startValue, endValue);
+        animator.setDuration(WIDTH_ANIMATION_DURATION_MS);
+        animator.setInterpolator(Interpolators.EMPHASIZED);
+        addAnimationCallBacks(animator,
+                /* onStart= */ () -> mBubbleBarBackground.showArrow(true),
+                /* onEnd= */ () -> {
+                    mBubbleBarBackground.showArrow(mIsBarExpanded);
+                    if (!mIsBarExpanded && mReorderRunnable != null) {
+                        mReorderRunnable.run();
+                        mReorderRunnable = null;
+                    }
+                    // If the bar was just collapsed and the overflow was the last bubble that was
+                    // selected, set the first bubble as selected.
+                    if (!mIsBarExpanded && mUpdateSelectedBubbleAfterCollapse != null
+                            && mSelectedBubbleView != null
+                            && mSelectedBubbleView.getBubble() instanceof BubbleBarOverflow) {
+                        BubbleView firstBubble = (BubbleView) getChildAt(0);
+                        mUpdateSelectedBubbleAfterCollapse.accept(firstBubble.getBubble().getKey());
+                    }
+                    // If the bar was just expanded, remove the dot from the selected bubble.
+                    if (mIsBarExpanded && mSelectedBubbleView != null) {
+                        mSelectedBubbleView.markSeen();
+                    }
+                    updateLayoutParams();
+                },
+                /* onUpdate= */ anim -> {
+                    updateBubblesLayoutProperties(mBubbleBarLocation);
+                    invalidate();
+                });
+        return animator;
+    }
+
+    /**
+     * Returns the distance between the top left corner of the bubble bar to the center of the dot
+     * of the selected bubble.
+     */
+    PointF getSelectedBubbleDotDistanceFromTopLeft() {
+        if (mSelectedBubbleView == null) {
+            return new PointF(0, 0);
+        }
+        final int indexOfSelectedBubble = indexOfChild(mSelectedBubbleView);
+        final boolean onLeft = mBubbleBarLocation.isOnLeft(isLayoutRtl());
+        final float selectedBubbleTx = isExpanded()
+                ? getExpandedBubbleTranslationX(indexOfSelectedBubble, getChildCount(), onLeft)
+                : getCollapsedBubbleTranslationX(indexOfSelectedBubble, getChildCount(), onLeft);
+        PointF selectedBubbleDotCenter = mSelectedBubbleView.getDotCenter();
+
+        return new PointF(
+                selectedBubbleTx + selectedBubbleDotCenter.x,
+                mBubbleBarPadding + mPointerSize + selectedBubbleDotCenter.y);
+    }
+
+    int getSelectedBubbleDotColor() {
+        return mSelectedBubbleView == null ? 0 : mSelectedBubbleView.getDotColor();
+    }
+
+    int getPointerSize() {
+        return mPointerSize;
+    }
+
+    float getBubbleElevation() {
+        return mBubbleElevation;
+    }
+
     /** Interface for BubbleBarView to communicate with its controller. */
     interface Controller {
 
@@ -1571,6 +1573,7 @@
         void dismissBubbleBar();
 
         /** Requests the controller to update bubble bar location to the given value */
-        void updateBubbleBarLocation(BubbleBarLocation location);
+        void updateBubbleBarLocation(BubbleBarLocation location,
+                @BubbleBarLocation.UpdateSource int source);
     }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index 025c038..d842138 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -18,6 +18,10 @@
 import static android.view.View.INVISIBLE;
 import static android.view.View.VISIBLE;
 
+import static com.android.launcher3.Utilities.mapRange;
+import static com.android.launcher3.taskbar.TaskbarPinningController.PINNING_PERSISTENT;
+import static com.android.launcher3.taskbar.TaskbarPinningController.PINNING_TRANSIENT;
+
 import android.animation.Animator;
 import android.animation.AnimatorSet;
 import android.content.res.Resources;
@@ -29,10 +33,12 @@
 import android.util.TypedValue;
 import android.view.MotionEvent;
 import android.view.View;
+import android.widget.FrameLayout;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.android.app.animation.Interpolators;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.R;
 import com.android.launcher3.anim.AnimatedFloat;
@@ -40,12 +46,18 @@
 import com.android.launcher3.taskbar.TaskbarActivityContext;
 import com.android.launcher3.taskbar.TaskbarControllers;
 import com.android.launcher3.taskbar.TaskbarInsetsController;
+import com.android.launcher3.taskbar.TaskbarSharedState;
 import com.android.launcher3.taskbar.TaskbarStashController;
 import com.android.launcher3.taskbar.bubbles.animation.BubbleBarViewAnimator;
+import com.android.launcher3.taskbar.bubbles.flyout.BubbleBarFlyoutController;
+import com.android.launcher3.taskbar.bubbles.flyout.BubbleBarFlyoutPositioner;
+import com.android.launcher3.taskbar.bubbles.flyout.FlyoutCallbacks;
 import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController;
+import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.MultiPropertyFactory;
 import com.android.launcher3.util.MultiValueAlpha;
 import com.android.quickstep.SystemUiProxy;
+import com.android.wm.shell.Flags;
 import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
 
 import java.io.PrintWriter;
@@ -63,6 +75,11 @@
     private static final float APP_ICON_SMALL_DP = 44f;
     private static final float APP_ICON_MEDIUM_DP = 48f;
     private static final float APP_ICON_LARGE_DP = 52f;
+    /** The dot size is defined as a percentage of the icon size. */
+    private static final float DOT_TO_BUBBLE_SIZE_RATIO = 0.228f;
+    public static final int TASKBAR_FADE_IN_DURATION_MS = 150;
+    public static final int TASKBAR_FADE_IN_DELAY_MS = 50;
+    public static final int TASKBAR_FADE_OUT_DURATION_MS = 100;
     private final SystemUiProxy mSystemUiProxy;
     private final TaskbarActivityContext mActivity;
     private final BubbleBarView mBarView;
@@ -96,6 +113,10 @@
             this::updateTranslationY);
     private final AnimatedFloat mBubbleOffsetY = new AnimatedFloat(
             this::updateBubbleOffsetY);
+    private final AnimatedFloat mBubbleBarPinning = new AnimatedFloat(pinningProgress -> {
+        updateTranslationY();
+        setBubbleBarScaleAndPadding(pinningProgress);
+    });
 
     // Modified when swipe up is happening on the bubble bar or task bar.
     private float mBubbleBarSwipeUpTranslationY;
@@ -106,36 +127,48 @@
     private boolean mHiddenForSysui;
     // Whether the bar is hidden because there are no bubbles.
     private boolean mHiddenForNoBubbles = true;
+    // Whether the bar is hidden when stashed
+    private boolean mHiddenForStashed;
     private boolean mShouldShowEducation;
 
     public boolean mOverflowAdded;
 
     private BubbleBarViewAnimator mBubbleBarViewAnimator;
-
+    private final FrameLayout mBubbleBarContainer;
+    private BubbleBarFlyoutController mBubbleBarFlyoutController;
+    private TaskbarSharedState mTaskbarSharedState;
     private final TimeSource mTimeSource = System::currentTimeMillis;
+    private final int mTaskbarTranslationDelta;
 
     @Nullable
     private BubbleBarBoundsChangeListener mBoundsChangeListener;
 
-    public BubbleBarViewController(TaskbarActivityContext activity, BubbleBarView barView) {
+    public BubbleBarViewController(TaskbarActivityContext activity, BubbleBarView barView,
+            FrameLayout bubbleBarContainer) {
         mActivity = activity;
         mBarView = barView;
+        mBubbleBarContainer = bubbleBarContainer;
         mSystemUiProxy = SystemUiProxy.INSTANCE.get(mActivity);
         mBubbleBarAlpha = new MultiValueAlpha(mBarView, 1 /* num alpha channels */);
         mIconSize = activity.getResources().getDimensionPixelSize(
                 R.dimen.bubblebar_icon_size);
+        mTaskbarTranslationDelta = getBubbleBarTranslationDeltaForTaskbar(activity);
     }
 
     /** Initializes controller. */
     public void init(TaskbarControllers controllers, BubbleControllers bubbleControllers,
             TaskbarViewPropertiesProvider taskbarViewPropertiesProvider) {
+        mTaskbarSharedState = controllers.getSharedState();
         mBubbleStashController = bubbleControllers.bubbleStashController;
         mBubbleBarController = bubbleControllers.bubbleBarController;
         mBubbleDragController = bubbleControllers.bubbleDragController;
         mTaskbarStashController = controllers.taskbarStashController;
         mTaskbarInsetsController = controllers.taskbarInsetsController;
+        mBubbleBarFlyoutController = new BubbleBarFlyoutController(
+                mBubbleBarContainer, createFlyoutPositioner(), createFlyoutCallbacks());
         mBubbleBarViewAnimator = new BubbleBarViewAnimator(
-                mBarView, mBubbleStashController, mBubbleBarController::showExpandedView);
+                mBarView, mBubbleStashController, mBubbleBarFlyoutController,
+                mBubbleBarController::showExpandedView);
         mTaskbarViewPropertiesProvider = taskbarViewPropertiesProvider;
         onBubbleBarConfigurationChanged(/* animate= */ false);
         mActivity.addOnDeviceProfileChangeListener(
@@ -145,6 +178,9 @@
         mBubbleBarClickListener = v -> expandBubbleBar();
         mBubbleDragController.setupBubbleBarView(mBarView);
         mOverflowBubble = bubbleControllers.bubbleCreator.createOverflow(mBarView);
+        if (!Flags.enableOptionalBubbleOverflow()) {
+            showOverflow(true);
+        }
         mBarView.setOnClickListener(mBubbleBarClickListener);
         mBarView.addOnLayoutChangeListener(
                 (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
@@ -153,6 +189,10 @@
                         mBoundsChangeListener.onBoundsChanged();
                     }
                 });
+        float pinningValue = DisplayController.isTransientTaskbar(mActivity)
+                ? PINNING_TRANSIENT
+                : PINNING_PERSISTENT;
+        mBubbleBarPinning.updateValue(pinningValue);
         mBarView.setController(new BubbleBarView.Controller() {
             @Override
             public float getBubbleBarTranslationY() {
@@ -161,7 +201,9 @@
 
             @Override
             public void onBubbleBarTouched() {
-                BubbleBarViewController.this.onBubbleBarTouched();
+                if (isAnimatingNewBubble()) {
+                    interruptAnimationForTouch();
+                }
             }
 
             @Override
@@ -175,8 +217,9 @@
             }
 
             @Override
-            public void updateBubbleBarLocation(BubbleBarLocation location) {
-                mBubbleBarController.updateBubbleBarLocation(location);
+            public void updateBubbleBarLocation(BubbleBarLocation location,
+                    @BubbleBarLocation.UpdateSource int source) {
+                mBubbleBarController.updateBubbleBarLocation(location, source);
             }
         });
 
@@ -200,13 +243,94 @@
             }
 
             @Override
-            public void updateBubbleBarLocation(BubbleBarLocation location) {
-                mBubbleBarController.updateBubbleBarLocation(location);
+            public void updateBubbleBarLocation(BubbleBarLocation location,
+                    @BubbleBarLocation.UpdateSource int source) {
+                mBubbleBarController.updateBubbleBarLocation(location, source);
+            }
+        };
+    }
+
+    /** Returns animated float property responsible for pinning transition animation. */
+    public AnimatedFloat getBubbleBarPinning() {
+        return mBubbleBarPinning;
+    }
+
+    private BubbleBarFlyoutPositioner createFlyoutPositioner() {
+        return new BubbleBarFlyoutPositioner() {
+
+            @Override
+            public boolean isOnLeft() {
+                return mBarView.getBubbleBarLocation().isOnLeft(mBarView.isLayoutRtl());
+            }
+
+            @Override
+            public float getTargetTy() {
+                return mBarView.getTranslationY() - mBarView.getHeight();
+            }
+
+            @Override
+            @NonNull
+            public PointF getDistanceToCollapsedPosition() {
+                // the flyout animates from the selected bubble dot. calculate the distance it needs
+                // to translate itself to its starting position.
+                PointF distanceToDotCenter = mBarView.getSelectedBubbleDotDistanceFromTopLeft();
+
+                // if we're gravitating left, return the distance between the top left corner of the
+                // bubble bar and the bottom left corner of the dot.
+                // if we're gravitating right, return the distance between the top right corner of
+                // the bubble bar and the bottom right corner of the dot.
+                float distanceX = isOnLeft()
+                        ? distanceToDotCenter.x - getCollapsedSize() / 2
+                        : mBarView.getWidth() - distanceToDotCenter.x - getCollapsedSize() / 2;
+                float distanceY = distanceToDotCenter.y + getCollapsedSize() / 2;
+                return new PointF(distanceX, distanceY);
+            }
+
+            @Override
+            public float getCollapsedSize() {
+                return mIconSize * DOT_TO_BUBBLE_SIZE_RATIO;
+            }
+
+            @Override
+            public int getCollapsedColor() {
+                return mBarView.getSelectedBubbleDotColor();
+            }
+
+            @Override
+            public float getCollapsedElevation() {
+                return mBarView.getBubbleElevation();
+            }
+
+            @Override
+            public float getDistanceToRevealTriangle() {
+                return getDistanceToCollapsedPosition().y - mBarView.getPointerSize();
+            }
+        };
+    }
+
+    private FlyoutCallbacks createFlyoutCallbacks() {
+        return new FlyoutCallbacks() {
+            @Override
+            public void extendTopBoundary(int space) {
+                int defaultSize = mActivity.getDefaultTaskbarWindowSize();
+                mActivity.setTaskbarWindowSize(defaultSize + space);
+            }
+
+            @Override
+            public void resetTopBoundary() {
+                mActivity.setTaskbarWindowSize(mActivity.getDefaultTaskbarWindowSize());
+            }
+
+            @Override
+            public void flyoutClicked() {
+                interruptAnimationForTouch();
+                expandBubbleBar();
             }
         };
     }
 
     private void onBubbleClicked(BubbleView bubbleView) {
+        if (mBubbleBarPinning.isAnimating()) return;
         bubbleView.markSeen();
         BubbleBarItem bubble = bubbleView.getBubble();
         if (bubble == null) {
@@ -222,12 +346,10 @@
         }
     }
 
-    private void onBubbleBarTouched() {
-        if (isAnimatingNewBubble()) {
-            mBubbleBarViewAnimator.onBubbleBarTouchedWhileAnimating();
-            mBubbleStashController.onNewBubbleAnimationInterrupted(false,
-                    mBarView.getTranslationY());
-        }
+    /** Interrupts the running animation for a touch event on the bubble bar or flyout. */
+    private void interruptAnimationForTouch() {
+        mBubbleBarViewAnimator.interruptForTouch();
+        mBubbleStashController.onNewBubbleAnimationInterrupted(false, mBarView.getTranslationY());
     }
 
     private void expandBubbleBar() {
@@ -351,6 +473,13 @@
     }
 
     /**
+     * @return the max collapsed width for the bubble bar.
+     */
+    public float getCollapsedWidthWithMaxVisibleBubbles() {
+        return mBarView.getCollapsedWidthWithMaxVisibleBubbles();
+    }
+
+    /**
      * @return {@code true} if bubble bar is on the left edge of the screen, {@code false} if on
      * the right
      */
@@ -381,6 +510,12 @@
         return mBarView.getBubbleBarBounds();
     }
 
+    /** Returns the bounds of the flyout view if it exists, or {@code null} otherwise. */
+    @Nullable
+    public Rect getFlyoutBounds() {
+        return mBubbleBarFlyoutController.getFlyoutBounds();
+    }
+
     /** Checks that bubble bar is visible and that the motion event is within bounds. */
     public boolean isEventOverBubbleBar(MotionEvent event) {
         if (!isBubbleBarVisible()) return false;
@@ -467,9 +602,17 @@
         }
     }
 
+    /** Sets whether the bubble bar should be hidden due to stashed state */
+    public void setHiddenForStashed(boolean hidden) {
+        if (mHiddenForStashed != hidden) {
+            mHiddenForStashed = hidden;
+            updateVisibilityForStateChange();
+        }
+    }
+
     // TODO: (b/273592694) animate it
     private void updateVisibilityForStateChange() {
-        if (!mHiddenForSysui && !mHiddenForNoBubbles) {
+        if (!mHiddenForSysui && !mHiddenForNoBubbles && !mHiddenForStashed) {
             mBarView.setVisibility(VISIBLE);
         } else {
             mBarView.setVisibility(INVISIBLE);
@@ -498,9 +641,11 @@
         updateBubbleBarIconSizeAndPadding(newIconSize, newPadding, animate);
     }
 
-
     private int getBubbleBarIconSizeFromDeviceProfile(Resources res) {
-        DeviceProfile deviceProfile = mActivity.getDeviceProfile();
+        return getBubbleBarIconSizeFromDeviceProfile(res, mActivity.getDeviceProfile());
+    }
+
+    private int getBubbleBarIconSizeFromDeviceProfile(Resources res, DeviceProfile deviceProfile) {
         DisplayMetrics dm = res.getDisplayMetrics();
         float smallIconSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
                 APP_ICON_SMALL_DP, dm);
@@ -515,7 +660,10 @@
     }
 
     private int getBubbleBarPaddingFromDeviceProfile(Resources res) {
-        DeviceProfile deviceProfile = mActivity.getDeviceProfile();
+        return getBubbleBarPaddingFromDeviceProfile(res, mActivity.getDeviceProfile());
+    }
+
+    private int getBubbleBarPaddingFromDeviceProfile(Resources res, DeviceProfile deviceProfile) {
         DisplayMetrics dm = res.getDisplayMetrics();
         float mediumIconSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
                 APP_ICON_MEDIUM_DP, dm);
@@ -556,7 +704,53 @@
 
     private void updateTranslationY() {
         mBarView.setTranslationY(mBubbleBarTranslationY.value + mBubbleBarSwipeUpTranslationY
-                + mBubbleBarStashTranslationY);
+                + mBubbleBarStashTranslationY + getBubbleBarTranslationYForTaskbarPinning());
+    }
+
+    /** Computes translation y for taskbar pinning. */
+    private float getBubbleBarTranslationYForTaskbarPinning() {
+        if (mTaskbarSharedState == null) return 0f;
+        float pinningProgress = mBubbleBarPinning.value;
+        if (mTaskbarSharedState.startTaskbarVariantIsTransient) {
+            return mapRange(pinningProgress, /* min = */ 0f, mTaskbarTranslationDelta);
+        } else {
+            return mapRange(pinningProgress, -mTaskbarTranslationDelta, /* max = */ 0f);
+        }
+    }
+
+    private void setBubbleBarScaleAndPadding(float pinningProgress) {
+        Resources res = mActivity.getResources();
+        // determine icon scale for pinning
+        int persistentIconSize = res.getDimensionPixelSize(
+                R.dimen.bubblebar_icon_size_persistent_taskbar);
+        int transientIconSize = getBubbleBarIconSizeFromDeviceProfile(res,
+                mActivity.getTransientTaskbarDeviceProfile());
+        float pinningIconSize = mapRange(pinningProgress, transientIconSize, persistentIconSize);
+
+        // determine bubble bar padding for pinning
+        int persistentPadding = res.getDimensionPixelSize(
+                R.dimen.bubblebar_icon_spacing_persistent_taskbar);
+        int transientPadding = getBubbleBarPaddingFromDeviceProfile(res,
+                mActivity.getTransientTaskbarDeviceProfile());
+        float pinningPadding = mapRange(pinningProgress, transientPadding, persistentPadding);
+        mBarView.setIconSizeAndPaddingForPinning(pinningIconSize, pinningPadding);
+    }
+
+    /**
+     * Calculates the vertical difference in the bubble bar positions for pinned and transient
+     * taskbar modes.
+     */
+    private int getBubbleBarTranslationDeltaForTaskbar(TaskbarActivityContext activity) {
+        Resources res = activity.getResources();
+        int persistentBubbleSize = res
+                .getDimensionPixelSize(R.dimen.bubblebar_icon_size_persistent_taskbar);
+        int persistentSpacingSize = res
+                .getDimensionPixelSize(R.dimen.bubblebar_icon_spacing_persistent_taskbar);
+        int persistentBubbleBarSize = persistentBubbleSize + persistentSpacingSize * 2;
+        int persistentTaskbarHeight = activity.getPersistentTaskbarDeviceProfile().taskbarHeight;
+        int persistentBubbleBarY = (persistentTaskbarHeight - persistentBubbleBarSize) / 2;
+        int transientBubbleBarY = activity.getTransientTaskbarDeviceProfile().taskbarBottomMargin;
+        return transientBubbleBarY - persistentBubbleBarY;
     }
 
     private void updateScaleX(float scale) {
@@ -675,7 +869,8 @@
                 // if the animation is suppressed, immediately stash or show the bubble bar to
                 // ensure they've been initialized.
                 if (mTaskbarStashController.isInApp()
-                        && mBubbleStashController.isTransientTaskBar()) {
+                        && mBubbleStashController.isTransientTaskBar()
+                        && mTaskbarStashController.isStashed()) {
                     mBubbleStashController.stashBubbleBarImmediate();
                 } else {
                     mBubbleStashController.showBubbleBarImmediate();
@@ -691,21 +886,31 @@
     /** Animates the bubble bar to notify the user about a bubble change. */
     public void animateBubbleNotification(BubbleBarBubble bubble, boolean isExpanding,
             boolean isUpdate) {
+        // if we're not already animating another bubble, update the dot visibility. otherwise the
+        // the dot will be handled as part of the animation.
+        if (!mBubbleBarViewAnimator.isAnimating()) {
+            bubble.getView().updateDotVisibility(
+                    /* animate= */ !mBubbleStashController.isStashed());
+        }
+        // if we're expanded, don't animate the bubble bar.
+        if (isExpanded()) {
+            return;
+        }
         boolean isInApp = mTaskbarStashController.isInApp();
         // if this is the first bubble, animate to the initial state.
         if (mBarView.getBubbleChildCount() == 1 && !isUpdate) {
             mBubbleBarViewAnimator.animateToInitialState(bubble, isInApp, isExpanding);
             return;
         }
-        boolean persistentTaskbarOrOnHome = mBubbleStashController.isBubblesShowingOnHome()
+        // if we're not stashed or we're in persistent taskbar, animate for collapsed state.
+        boolean animateForCollapsed = !mBubbleStashController.isStashed()
                 || !mBubbleStashController.isTransientTaskBar();
-        if (persistentTaskbarOrOnHome && !isExpanded()) {
+        if (animateForCollapsed) {
             mBubbleBarViewAnimator.animateBubbleBarForCollapsed(bubble, isExpanding);
             return;
         }
 
-        // only animate the new bubble if we're in an app, have handle view and not auto expanding
-        if (isInApp && mBubbleStashController.getHasHandleView() && !isExpanded()) {
+        if (isInApp && mBubbleStashController.getHasHandleView()) {
             mBubbleBarViewAnimator.animateBubbleInForStashed(bubble, isExpanding);
         }
     }
@@ -733,7 +938,7 @@
      * from Launcher.
      */
     public void setExpanded(boolean isExpanded) {
-        if (isExpanded != mBarView.isExpanded()) {
+        if (!mBubbleBarPinning.isAnimating() && isExpanded != mBarView.isExpanded()) {
             mBarView.setExpanded(isExpanded);
             adjustTaskbarAndHotseatToBubbleBarState(isExpanded);
             if (!isExpanded) {
@@ -748,22 +953,21 @@
 
     /**
      * Hides the persistent taskbar if it is going to intersect with the expanded bubble bar if in
-     * app or overview. Set the hotseat stashed state if on launcher home screen. If not on launcher
-     * home screen and hotseat is stashed immediately un-stashes the hotseat.
+     * app or overview.
      */
     private void adjustTaskbarAndHotseatToBubbleBarState(boolean isBubbleBarExpanded) {
-        if (mBubbleStashController.isBubblesShowingOnHome()) {
-            mTaskbarStashController.stashHotseat(isBubbleBarExpanded);
-        } else if (!mBubbleStashController.isTransientTaskBar()) {
-            boolean hideTaskbar = isBubbleBarExpanded && isIntersectingTaskbar();
-            mTaskbarViewPropertiesProvider
-                    .getIconsAlpha()
-                    .animateToValue(hideTaskbar ? 0 : 1)
-                    .start();
-        }
         if (!mBubbleStashController.isBubblesShowingOnHome()
-                && mTaskbarStashController.isHiddenForBubbles()) {
-            mTaskbarStashController.unStashHotseatInstantly();
+                && !mBubbleStashController.isTransientTaskBar()) {
+            boolean hideTaskbar = isBubbleBarExpanded && isIntersectingTaskbar();
+            Animator taskbarAlphaAnimator = mTaskbarViewPropertiesProvider.getIconsAlpha()
+                    .animateToValue(hideTaskbar ? 0 : 1);
+            taskbarAlphaAnimator.setDuration(hideTaskbar
+                    ? TASKBAR_FADE_OUT_DURATION_MS : TASKBAR_FADE_IN_DURATION_MS);
+            if (!hideTaskbar) {
+                taskbarAlphaAnimator.setStartDelay(TASKBAR_FADE_IN_DELAY_MS);
+            }
+            taskbarAlphaAnimator.setInterpolator(Interpolators.LINEAR);
+            taskbarAlphaAnimator.start();
         }
     }
 
@@ -876,6 +1080,16 @@
         mSystemUiProxy.removeAllBubbles();
     }
 
+    /** Removes all existing bubble views */
+    public void removeAllBubbles() {
+        mBarView.removeAllViews();
+    }
+
+    /** Returns the view index of the existing bubble */
+    public int bubbleViewIndex(View bubbleView) {
+        return mBarView.indexOfChild(bubbleView);
+    }
+
     /**
      * Set listener to be notified when bubble bar bounds have changed
      */
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java
index a66df4c..d993685 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java
@@ -21,7 +21,9 @@
 import android.view.View;
 
 import com.android.launcher3.taskbar.TaskbarControllers;
+import com.android.launcher3.taskbar.TaskbarSharedState;
 import com.android.launcher3.taskbar.bubbles.BubbleBarViewController.TaskbarViewPropertiesProvider;
+import com.android.launcher3.taskbar.bubbles.stashing.BubbleBarLocationOnDemandListener;
 import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController;
 import com.android.launcher3.util.MultiPropertyFactory;
 import com.android.launcher3.util.RunnableList;
@@ -40,6 +42,7 @@
     public final BubbleDismissController bubbleDismissController;
     public final BubbleBarPinController bubbleBarPinController;
     public final BubblePinController bubblePinController;
+    public final Optional<BubbleBarSwipeController> bubbleBarSwipeController;
     public final BubbleCreator bubbleCreator;
 
     private final RunnableList mPostInitRunnables = new RunnableList();
@@ -58,6 +61,7 @@
             BubbleDismissController bubbleDismissController,
             BubbleBarPinController bubbleBarPinController,
             BubblePinController bubblePinController,
+            Optional<BubbleBarSwipeController> bubbleBarSwipeController,
             BubbleCreator bubbleCreator) {
         this.bubbleBarController = bubbleBarController;
         this.bubbleBarViewController = bubbleBarViewController;
@@ -67,6 +71,7 @@
         this.bubbleDismissController = bubbleDismissController;
         this.bubbleBarPinController = bubbleBarPinController;
         this.bubblePinController = bubblePinController;
+        this.bubbleBarSwipeController = bubbleBarSwipeController;
         this.bubbleCreator = bubbleCreator;
     }
 
@@ -75,16 +80,17 @@
      * BubbleControllers instance, but should be careful to only access things that were created
      * in constructors for now, as some controllers may still be waiting for init().
      */
-    public void init(TaskbarControllers taskbarControllers) {
-        // TODO(b/346381754) add TaskbarLauncherStateController implementation to adjust the hotseat
+    public void init(TaskbarSharedState taskbarSharedState, TaskbarControllers taskbarControllers) {
         BubbleBarLocationCompositeListener bubbleBarLocationListeners =
                 new BubbleBarLocationCompositeListener(
                         taskbarControllers.navbarButtonsViewController,
-                        taskbarControllers.taskbarViewController
+                        taskbarControllers.taskbarViewController,
+                        new BubbleBarLocationOnDemandListener(() -> taskbarControllers.uiController)
                 );
         bubbleBarController.init(this,
                 bubbleBarLocationListeners,
-                taskbarControllers.navbarButtonsViewController::isImeVisible);
+                taskbarControllers.navbarButtonsViewController::isImeVisible,
+                taskbarSharedState);
         bubbleStashedHandleViewController.ifPresent(
                 controller -> controller.init(/* bubbleControllers = */ this));
         bubbleStashController.init(
@@ -111,6 +117,7 @@
         bubbleDismissController.init(/* bubbleControllers = */ this);
         bubbleBarPinController.init(this, bubbleBarLocationListeners);
         bubblePinController.init(this);
+        bubbleBarSwipeController.ifPresent(c -> c.init(this));
 
         mPostInitRunnables.executeAllAndDestroy();
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleCreator.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleCreator.java
index 12b1487..c5efe2f 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleCreator.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleCreator.java
@@ -22,6 +22,7 @@
 import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED_BY_ANY_LAUNCHER;
 
 import static com.android.launcher3.icons.FastBitmapDrawable.WHITE_SCRIM_ALPHA;
+import static com.android.wm.shell.shared.bubbles.FlyoutDrawableLoader.loadFlyoutDrawable;
 
 import android.annotation.Nullable;
 import android.content.Context;
@@ -44,14 +45,14 @@
 import android.view.LayoutInflater;
 import android.view.ViewGroup;
 
-import androidx.appcompat.content.res.AppCompatResources;
-
 import com.android.internal.graphics.ColorUtils;
 import com.android.launcher3.R;
 import com.android.launcher3.icons.BitmapInfo;
 import com.android.launcher3.icons.BubbleIconFactory;
 import com.android.launcher3.shortcuts.ShortcutRequest;
+import com.android.launcher3.taskbar.bubbles.flyout.BubbleBarFlyoutMessage;
 import com.android.wm.shell.shared.bubbles.BubbleInfo;
+import com.android.wm.shell.shared.bubbles.ParcelableFlyoutMessage;
 
 /**
  * Loads the necessary info to populate / present a bubble (name, icon, shortcut).
@@ -159,13 +160,16 @@
         dotColor = ColorUtils.blendARGB(badgeBitmapInfo.color,
                 Color.WHITE, WHITE_SCRIM_ALPHA / 255f);
 
+        final BubbleBarFlyoutMessage flyoutMessage =
+                getFlyoutMessage(info.getParcelableFlyoutMessage());
+
         if (existingBubble == null) {
             LayoutInflater inflater = LayoutInflater.from(context);
             BubbleView bubbleView = (BubbleView) inflater.inflate(
                     R.layout.bubblebar_item_view, barView, false /* attachToRoot */);
 
             BubbleBarBubble bubble = new BubbleBarBubble(info, bubbleView,
-                    badgeBitmap, bubbleBitmap, dotColor, dotPath, appName);
+                    badgeBitmap, bubbleBitmap, dotColor, dotPath, appName, flyoutMessage);
             bubbleView.setBubble(bubble);
             return bubble;
         } else {
@@ -176,10 +180,25 @@
             existingBubble.setDotColor(dotColor);
             existingBubble.setDotPath(dotPath);
             existingBubble.setAppName(appName);
+            existingBubble.setFlyoutMessage(flyoutMessage);
             return existingBubble;
         }
     }
 
+    @Nullable
+    private BubbleBarFlyoutMessage getFlyoutMessage(
+            @Nullable ParcelableFlyoutMessage parcelableFlyoutMessage) {
+        if (parcelableFlyoutMessage == null) {
+            return null;
+        }
+        String title = parcelableFlyoutMessage.getTitle();
+        String message = parcelableFlyoutMessage.getMessage();
+        return new BubbleBarFlyoutMessage(
+                loadFlyoutDrawable(parcelableFlyoutMessage.getIcon(), mContext),
+                title == null ? "" : title,
+                message == null ? "" : message);
+    }
+
     /**
      * Creates the overflow view shown in the bubble bar.
      *
@@ -196,8 +215,7 @@
     }
 
     private Bitmap createOverflowBitmap() {
-        Drawable iconDrawable = AppCompatResources.getDrawable(mContext,
-                R.drawable.bubble_ic_overflow_button);
+        Drawable iconDrawable = mContext.getDrawable(R.drawable.bubble_ic_overflow_button);
 
         final TypedArray ta = mContext.obtainStyledAttributes(
                 new int[]{
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java
index 42bd197..fd4cf0e 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java
@@ -161,7 +161,8 @@
 
             @Override
             void onDragEnd() {
-                mBubbleBarController.updateBubbleBarLocation(mReleasedLocation);
+                mBubbleBarController.updateBubbleBarLocation(mReleasedLocation,
+                        BubbleBarLocation.UpdateSource.DRAG_BUBBLE);
                 mBubbleBarViewController.onBubbleDragEnd();
                 mBubblePinController.setListener(null);
             }
@@ -226,7 +227,8 @@
             @Override
             void onDragEnd() {
                 // Make sure to update location as the first thing. Pivot update causes a relayout
-                mBubbleBarController.updateBubbleBarLocation(mReleasedLocation);
+                mBubbleBarController.updateBubbleBarLocation(mReleasedLocation,
+                        BubbleBarLocation.UpdateSource.DRAG_BAR);
                 bubbleBarView.setIsDragging(false);
                 // Restoring the initial pivot for the bubble bar view
                 bubbleBarView.setRelativePivot(initialRelativePivot.x, initialRelativePivot.y);
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
index 561df5c..c74fa9b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
@@ -22,6 +22,7 @@
 import android.graphics.Canvas;
 import android.graphics.Color;
 import android.graphics.Path;
+import android.graphics.PointF;
 import android.graphics.Rect;
 import android.graphics.drawable.BitmapDrawable;
 import android.os.Bundle;
@@ -48,6 +49,8 @@
 public class BubbleView extends ConstraintLayout {
 
     public static final int DEFAULT_PATH_SIZE = 100;
+    /** Duration for animating the scale of the dot and badge. */
+    private static final int SCALE_ANIMATION_DURATION_MS = 200;
 
     private final ImageView mBubbleIcon;
     private final ImageView mAppIcon;
@@ -67,8 +70,7 @@
     private float mAnimatingToDotScale;
     // The current scale value of the dot
     private float mDotScale;
-
-    private boolean mProvideShadowOutline = true;
+    private boolean mDotSuppressedForBubbleUpdate = false;
 
     // TODO: (b/273310265) handle RTL
     // Whether the bubbles are positioned on the left or right side of the screen
@@ -222,12 +224,14 @@
         }
         if (action == R.id.action_move_left) {
             if (mController != null) {
-                mController.updateBubbleBarLocation(BubbleBarLocation.LEFT);
+                mController.updateBubbleBarLocation(BubbleBarLocation.LEFT,
+                        BubbleBarLocation.UpdateSource.A11Y_ACTION_BUBBLE);
             }
         }
         if (action == R.id.action_move_right) {
             if (mController != null) {
-                mController.updateBubbleBarLocation(BubbleBarLocation.RIGHT);
+                mController.updateBubbleBarLocation(BubbleBarLocation.RIGHT,
+                        BubbleBarLocation.UpdateSource.A11Y_ACTION_BUBBLE);
             }
         }
         return false;
@@ -299,7 +303,12 @@
         return mBubble;
     }
 
-    void updateDotVisibility(boolean animate) {
+    /** Updates the dot visibility if it's not suppressed based on whether it has unseen content. */
+    public void updateDotVisibility(boolean animate) {
+        if (mDotSuppressedForBubbleUpdate) {
+            // if the dot is suppressed for an update, there's nothing to do
+            return;
+        }
         final float targetScale = hasUnseenContent() ? 1f : 0f;
         if (animate) {
             animateDotScale(targetScale);
@@ -311,12 +320,47 @@
     }
 
     void setBadgeScale(float fraction) {
-        if (mAppIcon.getVisibility() == VISIBLE) {
+        if (hasBadge()) {
             mAppIcon.setScaleX(fraction);
             mAppIcon.setScaleY(fraction);
         }
     }
 
+    void showBadge() {
+        animateBadgeScale(1);
+    }
+
+    void hideBadge() {
+        animateBadgeScale(0);
+    }
+
+    private boolean hasBadge() {
+        return mAppIcon.getVisibility() == VISIBLE;
+    }
+
+    private void animateBadgeScale(float scale) {
+        if (!hasBadge()) {
+            return;
+        }
+        mAppIcon.clearAnimation();
+        mAppIcon.animate()
+                .setDuration(SCALE_ANIMATION_DURATION_MS)
+                .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
+                .scaleX(scale)
+                .scaleY(scale)
+                .start();
+    }
+
+    /** Suppresses or un-suppresses drawing the dot due to an update for this bubble. */
+    public void suppressDotForBubbleUpdate(boolean suppress) {
+        mDotSuppressedForBubbleUpdate = suppress;
+        if (suppress) {
+            setDotScale(0);
+        } else {
+            showDotIfNeeded(/* animate= */ false);
+        }
+    }
+
     boolean hasUnseenContent() {
         return mBubble != null
                 && mBubble instanceof BubbleBarBubble
@@ -353,8 +397,8 @@
     }
 
     void showDotIfNeeded(boolean animate) {
-        // only show the dot if we have unseen content
-        if (!hasUnseenContent()) {
+        // only show the dot if we have unseen content and it's not suppressed
+        if (!hasUnseenContent() || mDotSuppressedForBubbleUpdate) {
             return;
         }
         if (animate) {
@@ -394,7 +438,7 @@
 
         clearAnimation();
         animate()
-                .setDuration(200)
+                .setDuration(SCALE_ANIMATION_DURATION_MS)
                 .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
                 .setUpdateListener((valueAnimator) -> {
                     float fraction = valueAnimator.getAnimatedFraction();
@@ -406,6 +450,23 @@
                 }).start();
     }
 
+    /**
+     * Returns the distance from the top left corner of this bubble view to the center of its dot.
+     */
+    public PointF getDotCenter() {
+        float[] dotPosition =
+                mOnLeft ? mDotRenderer.getLeftDotPosition() : mDotRenderer.getRightDotPosition();
+        getDrawingRect(mTempBounds);
+        float dotCenterX = mTempBounds.width() * dotPosition[0];
+        float dotCenterY = mTempBounds.height() * dotPosition[1];
+        return new PointF(dotCenterX, dotCenterY);
+    }
+
+    /** Returns the dot color. */
+    public int getDotColor() {
+        return mDotColor;
+    }
+
     @Override
     public String toString() {
         String toString = mBubble != null ? mBubble.getKey() : "null";
@@ -424,6 +485,7 @@
         void collapse();
 
         /** Request bubble bar location to be updated to the given location */
-        void updateBubbleBarLocation(BubbleBarLocation location);
+        void updateBubbleBarLocation(BubbleBarLocation location,
+                @BubbleBarLocation.UpdateSource int source);
     }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
index 6a955d9..6c354f3 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
@@ -27,6 +27,8 @@
 import com.android.launcher3.taskbar.bubbles.BubbleBarBubble
 import com.android.launcher3.taskbar.bubbles.BubbleBarView
 import com.android.launcher3.taskbar.bubbles.BubbleView
+import com.android.launcher3.taskbar.bubbles.flyout.BubbleBarFlyoutController
+import com.android.launcher3.taskbar.bubbles.flyout.BubbleBarFlyoutMessage
 import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController
 import com.android.wm.shell.shared.animation.PhysicsAnimator
 
@@ -36,8 +38,9 @@
 constructor(
     private val bubbleBarView: BubbleBarView,
     private val bubbleStashController: BubbleStashController,
+    private val bubbleBarFlyoutController: BubbleBarFlyoutController,
     private val onExpanded: Runnable,
-    private val scheduler: Scheduler = HandlerScheduler(bubbleBarView)
+    private val scheduler: Scheduler = HandlerScheduler(bubbleBarView),
 ) {
 
     private var animatingBubble: AnimatingBubble? = null
@@ -52,9 +55,11 @@
             return animatingBubble.state != AnimatingBubble.State.CREATED
         }
 
+    private var interceptedHandleAnimator = false
+
     private companion object {
         /** The time to show the flyout. */
-        const val FLYOUT_DELAY_MS: Long = 2500
+        const val FLYOUT_DELAY_MS: Long = 3000
         /** The initial scale Y value that the new bubble is set to before the animation starts. */
         const val BUBBLE_ANIMATION_INITIAL_SCALE_Y = 0.3f
         /** The minimum alpha value to make the bubble bar touchable. */
@@ -69,7 +74,7 @@
         val showAnimation: Runnable,
         val hideAnimation: Runnable,
         val expand: Boolean,
-        val state: State = State.CREATED
+        val state: State = State.CREATED,
     ) {
 
         /**
@@ -91,7 +96,7 @@
             /** The bubble notification is now fully showing and waiting to be hidden. */
             IN,
             /** The bubble notification is animating out. */
-            ANIMATING_OUT
+            ANIMATING_OUT,
         }
     }
 
@@ -127,18 +132,30 @@
     private val springConfig =
         PhysicsAnimator.SpringConfig(
             stiffness = SpringForce.STIFFNESS_LOW,
-            dampingRatio = SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY
+            dampingRatio = SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY,
         )
 
+    private fun cancelAnimationIfPending() {
+        val animatingBubble = animatingBubble ?: return
+        if (animatingBubble.state != AnimatingBubble.State.CREATED) return
+        scheduler.cancel(animatingBubble.showAnimation)
+        scheduler.cancel(animatingBubble.hideAnimation)
+    }
+
     /** Animates a bubble for the state where the bubble bar is stashed. */
     fun animateBubbleInForStashed(b: BubbleBarBubble, isExpanding: Boolean) {
-        // TODO b/346400677: handle animations for the same bubble interrupting each other
-        if (animatingBubble?.bubbleView?.bubble?.key == b.key) return
+        if (isAnimating) {
+            interruptAndUpdateAnimatingBubble(b.view, isExpanding)
+            return
+        }
+        cancelAnimationIfPending()
+
         val bubbleView = b.view
         val animator = PhysicsAnimator.getInstance(bubbleView)
         if (animator.isRunning()) animator.cancel()
-        // the animation of a new bubble is divided into 2 parts. The first part shows the bubble
-        // and the second part hides it after a delay.
+        // the animation of a new bubble is divided into 2 parts. The first part transforms the
+        // handle to the bubble bar and then shows the flyout. The second part hides the flyout and
+        // transforms the bubble bar back to the handle.
         val showAnimation = buildHandleToBubbleBarAnimation()
         val hideAnimation = if (isExpanding) Runnable {} else buildBubbleBarToHandleAnimation()
         animatingBubble =
@@ -161,17 +178,19 @@
      * 3. The third part is the overshoot of the spring animation, where we make the bubble fully
      *    visible which helps avoiding further updates when we re-enter the second part.
      */
-    private fun buildHandleToBubbleBarAnimation() = Runnable {
+    private fun buildHandleToBubbleBarAnimation(initialVelocity: Float? = null) = Runnable {
         moveToState(AnimatingBubble.State.ANIMATING_IN)
-        // prepare the bubble bar for the animation
-        bubbleBarView.visibility = VISIBLE
-        bubbleBarView.alpha = 0f
-        bubbleBarView.translationY = 0f
-        bubbleBarView.scaleX = 1f
-        bubbleBarView.scaleY = BUBBLE_ANIMATION_INITIAL_SCALE_Y
-        bubbleBarView.setBackgroundScaleX(1f)
-        bubbleBarView.setBackgroundScaleY(1f)
-        bubbleBarView.relativePivotY = 0.5f
+        // prepare the bubble bar for the animation if we're starting fresh
+        if (initialVelocity == null) {
+            bubbleBarView.visibility = VISIBLE
+            bubbleBarView.alpha = 0f
+            bubbleBarView.translationY = 0f
+            bubbleBarView.scaleX = 1f
+            bubbleBarView.scaleY = BUBBLE_ANIMATION_INITIAL_SCALE_Y
+            bubbleBarView.setBackgroundScaleX(1f)
+            bubbleBarView.setBackgroundScaleY(1f)
+            bubbleBarView.relativePivotY = 0.5f
+        }
 
         // this is the offset between the center of the bubble bar and the center of the stash
         // handle. when the handle becomes invisible and we start animating in the bubble bar,
@@ -190,7 +209,7 @@
         val totalTranslationY = bubbleStashController.bubbleBarTranslationYForTaskbar + offset
         val animator = bubbleStashController.getStashedHandlePhysicsAnimator() ?: return@Runnable
         animator.setDefaultSpringConfig(springConfig)
-        animator.spring(DynamicAnimation.TRANSLATION_Y, totalTranslationY)
+        animator.spring(DynamicAnimation.TRANSLATION_Y, totalTranslationY, initialVelocity ?: 0f)
         animator.addUpdateListener { handle, values ->
             val ty = values[DynamicAnimation.TRANSLATION_Y]?.value ?: return@addUpdateListener
             when {
@@ -243,7 +262,8 @@
                 cancelHideAnimation()
                 return@addEndListener
             }
-            moveToState(AnimatingBubble.State.IN)
+            setupAndShowFlyout()
+
             // the bubble bar is now fully settled in. update taskbar touch region so it's touchable
             bubbleStashController.updateTaskbarTouchRegion()
         }
@@ -309,34 +329,57 @@
                 }
             }
         }
-        animator.addEndListener { _, _, _, canceled, _, _, _ ->
+        animator.addEndListener { _, _, _, canceled, _, finalVelocity, _ ->
+            // PhysicsAnimator calls the end listeners when the animation is replaced with a new one
+            // if we're not in ANIMATING_OUT state, then this animation never started and we should
+            // return
+            if (animatingBubble?.state != AnimatingBubble.State.ANIMATING_OUT) return@addEndListener
+            if (interceptedHandleAnimator) {
+                interceptedHandleAnimator = false
+                // post this to give a PhysicsAnimator a chance to clean up its internal listeners.
+                // otherwise this end listener will be called as soon as we create a new spring
+                // animation
+                scheduler.post(buildHandleToBubbleBarAnimation(initialVelocity = finalVelocity))
+                return@addEndListener
+            }
             animatingBubble = null
             if (!canceled) bubbleStashController.stashBubbleBarImmediate()
             bubbleBarView.relativePivotY = 1f
             bubbleBarView.scaleY = 1f
             bubbleStashController.updateTaskbarTouchRegion()
         }
-        animator.start()
+
+        val bubble = animatingBubble?.bubbleView?.bubble as? BubbleBarBubble
+        val flyout = bubble?.flyoutMessage
+        if (flyout != null) {
+            bubbleBarFlyoutController.collapseFlyout {
+                onFlyoutRemoved()
+                animator.start()
+            }
+        } else {
+            animator.start()
+        }
     }
 
     /** Animates to the initial state of the bubble bar, when there are no previous bubbles. */
     fun animateToInitialState(b: BubbleBarBubble, isInApp: Boolean, isExpanding: Boolean) {
-        // TODO b/346400677: handle animations for the same bubble interrupting each other
-        if (animatingBubble?.bubbleView?.bubble?.key == b.key) return
         val bubbleView = b.view
         val animator = PhysicsAnimator.getInstance(bubbleView)
         if (animator.isRunning()) animator.cancel()
-        // the animation of a new bubble is divided into 2 parts. The first part shows the bubble
-        // and the second part hides it after a delay if we are in an app.
+        // the animation of a new bubble is divided into 2 parts. The first part slides in the
+        // bubble bar and shows the flyout. The second part hides the flyout and transforms the
+        // bubble bar to the handle if we're in an app.
         val showAnimation = buildBubbleBarSpringInAnimation()
         val hideAnimation =
             if (isInApp && !isExpanding) {
                 buildBubbleBarToHandleAnimation()
             } else {
-                // in this case the bubble bar remains visible so not much to do. once we implement
-                // the flyout we'll update this runnable to hide it.
                 Runnable {
-                    animatingBubble = null
+                    moveToState(AnimatingBubble.State.ANIMATING_OUT)
+                    bubbleBarFlyoutController.collapseFlyout {
+                        onFlyoutRemoved()
+                        animatingBubble = null
+                    }
                     bubbleStashController.showBubbleBarImmediate()
                     bubbleStashController.updateTaskbarTouchRegion()
                 }
@@ -370,7 +413,7 @@
             if (animatingBubble?.expand == true) {
                 cancelHideAnimation()
             } else {
-                moveToState(AnimatingBubble.State.IN)
+                setupAndShowFlyout()
             }
             // the bubble bar is now fully settled in. update taskbar touch region so it's touchable
             bubbleStashController.updateTaskbarTouchRegion()
@@ -379,14 +422,23 @@
     }
 
     fun animateBubbleBarForCollapsed(b: BubbleBarBubble, isExpanding: Boolean) {
-        // TODO b/346400677: handle animations for the same bubble interrupting each other
-        if (animatingBubble?.bubbleView?.bubble?.key == b.key) return
+        if (isAnimating) {
+            interruptAndUpdateAnimatingBubble(b.view, isExpanding)
+            return
+        }
+        cancelAnimationIfPending()
+
         val bubbleView = b.view
         val animator = PhysicsAnimator.getInstance(bubbleView)
         if (animator.isRunning()) animator.cancel()
+        // first bounce the bubble bar and show the flyout. Then hide the flyout.
         val showAnimation = buildBubbleBarBounceAnimation()
         val hideAnimation = Runnable {
-            animatingBubble = null
+            moveToState(AnimatingBubble.State.ANIMATING_OUT)
+            bubbleBarFlyoutController.collapseFlyout {
+                onFlyoutRemoved()
+                animatingBubble = null
+            }
             bubbleStashController.showBubbleBarImmediate()
             bubbleStashController.updateTaskbarTouchRegion()
         }
@@ -413,7 +465,7 @@
                 expandBubbleBar()
                 cancelHideAnimation()
             } else {
-                moveToState(AnimatingBubble.State.IN)
+                setupAndShowFlyout()
             }
         }
 
@@ -427,10 +479,38 @@
             .start()
     }
 
-    /** Handles touching the animating bubble bar. */
-    fun onBubbleBarTouchedWhileAnimating() {
+    private fun setupAndShowFlyout() {
+        val bubbleView = animatingBubble?.bubbleView
+        val bubble = bubbleView?.bubble as? BubbleBarBubble
+        val flyout = bubble?.flyoutMessage
+        if (flyout != null) {
+            bubbleBarFlyoutController.setUpAndShowFlyout(
+                BubbleBarFlyoutMessage(flyout.icon, flyout.title, flyout.message),
+                onInit = { bubbleView.suppressDotForBubbleUpdate(true) },
+                onEnd = {
+                    moveToState(AnimatingBubble.State.IN)
+                    bubbleStashController.updateTaskbarTouchRegion()
+                },
+            )
+        } else {
+            moveToState(AnimatingBubble.State.IN)
+        }
+    }
+
+    private fun cancelFlyout() {
+        bubbleBarFlyoutController.cancelFlyout { onFlyoutRemoved() }
+    }
+
+    private fun onFlyoutRemoved() {
+        animatingBubble?.bubbleView?.suppressDotForBubbleUpdate(false)
+        bubbleStashController.updateTaskbarTouchRegion()
+    }
+
+    /** Interrupts the animation due to touching the bubble bar or flyout. */
+    fun interruptForTouch() {
         PhysicsAnimator.getInstance(bubbleBarView).cancelIfRunning()
         bubbleStashController.getStashedHandlePhysicsAnimator().cancelIfRunning()
+        cancelFlyout()
         val hideAnimation = animatingBubble?.hideAnimation ?: return
         scheduler.cancel(hideAnimation)
         bubbleBarView.relativePivotY = 1f
@@ -439,6 +519,7 @@
 
     /** Notifies the animator that the taskbar area was touched during an animation. */
     fun onStashStateChangingWhileAnimating() {
+        cancelFlyout()
         val hideAnimation = animatingBubble?.hideAnimation ?: return
         scheduler.cancel(hideAnimation)
         animatingBubble = null
@@ -446,7 +527,7 @@
         bubbleBarView.relativePivotY = 1f
         bubbleStashController.onNewBubbleAnimationInterrupted(
             /* isStashed= */ bubbleBarView.alpha == 0f,
-            bubbleBarView.translationY
+            bubbleBarView.translationY,
         )
     }
 
@@ -455,11 +536,123 @@
         this.animatingBubble = animatingBubble.copy(expand = true)
         // if we're fully in and waiting to hide, cancel the hide animation and clean up
         if (animatingBubble.state == AnimatingBubble.State.IN) {
+            cancelFlyout()
             expandBubbleBar()
             cancelHideAnimation()
         }
     }
 
+    private fun interruptAndUpdateAnimatingBubble(bubbleView: BubbleView, isExpanding: Boolean) {
+        val animatingBubble = animatingBubble ?: return
+        when (animatingBubble.state) {
+            AnimatingBubble.State.CREATED -> {} // nothing to do since the animation hasn't started
+            AnimatingBubble.State.ANIMATING_IN ->
+                updateAnimationWhileAnimatingIn(animatingBubble, bubbleView, isExpanding)
+            AnimatingBubble.State.IN ->
+                updateAnimationWhileIn(animatingBubble, bubbleView, isExpanding)
+            AnimatingBubble.State.ANIMATING_OUT ->
+                updateAnimationWhileAnimatingOut(animatingBubble, bubbleView, isExpanding)
+        }
+    }
+
+    private fun updateAnimationWhileAnimatingIn(
+        animatingBubble: AnimatingBubble,
+        bubbleView: BubbleView,
+        isExpanding: Boolean,
+    ) {
+        this.animatingBubble = animatingBubble.copy(bubbleView = bubbleView, expand = isExpanding)
+        if (!bubbleBarFlyoutController.hasFlyout()) {
+            // if the flyout does not yet exist, then we're only animating the bubble bar.
+            // the animating bubble has been updated, so the when the flyout expands it will
+            // show the right message. we only need to update the dot visibility.
+            bubbleView.updateDotVisibility(/* animate= */ !bubbleStashController.isStashed)
+            return
+        }
+
+        val bubble = bubbleView.bubble as? BubbleBarBubble
+        val flyout = bubble?.flyoutMessage
+        if (flyout != null) {
+            // the flyout is currently expanding and we need to update it with new data
+            bubbleView.suppressDotForBubbleUpdate(true)
+            bubbleBarFlyoutController.updateFlyoutWhileExpanding(flyout)
+        } else {
+            // the flyout is expanding but we don't have new flyout data to update it with,
+            // so cancel the expanding flyout.
+            cancelFlyout()
+        }
+    }
+
+    private fun updateAnimationWhileIn(
+        animatingBubble: AnimatingBubble,
+        bubbleView: BubbleView,
+        isExpanding: Boolean,
+    ) {
+        // unsuppress the current bubble because we are about to hide its flyout
+        animatingBubble.bubbleView.suppressDotForBubbleUpdate(false)
+        this.animatingBubble = animatingBubble.copy(bubbleView = bubbleView, expand = isExpanding)
+
+        // we're currently idle, waiting for the hide animation to start. update the flyout
+        // data and reschedule the hide animation to run later to give the user a chance to
+        // see the new flyout.
+        val hideAnimation = animatingBubble.hideAnimation
+        scheduler.cancel(hideAnimation)
+        scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation)
+
+        val bubble = bubbleView.bubble as? BubbleBarBubble
+        val flyout = bubble?.flyoutMessage
+        if (flyout != null) {
+            bubbleView.suppressDotForBubbleUpdate(true)
+            bubbleBarFlyoutController.updateFlyoutFullyExpanded(flyout) {
+                bubbleStashController.updateTaskbarTouchRegion()
+            }
+        } else {
+            cancelFlyout()
+        }
+    }
+
+    private fun updateAnimationWhileAnimatingOut(
+        animatingBubble: AnimatingBubble,
+        bubbleView: BubbleView,
+        isExpanding: Boolean,
+    ) {
+        // unsuppress the current bubble because we are about to hide its flyout
+        animatingBubble.bubbleView.suppressDotForBubbleUpdate(false)
+        this.animatingBubble = animatingBubble.copy(bubbleView = bubbleView, expand = isExpanding)
+
+        // the hide animation already started so it can't be canceled, just post it again
+        val hideAnimation = animatingBubble.hideAnimation
+        scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation)
+
+        val bubble = bubbleView.bubble as? BubbleBarBubble
+        val flyout = bubble?.flyoutMessage
+        if (bubbleBarFlyoutController.hasFlyout()) {
+            // the flyout is collapsing. update it with the new flyout
+            if (flyout != null) {
+                moveToState(AnimatingBubble.State.ANIMATING_IN)
+                bubbleView.suppressDotForBubbleUpdate(true)
+                bubbleBarFlyoutController.updateFlyoutWhileCollapsing(flyout) {
+                    moveToState(AnimatingBubble.State.IN)
+                    bubbleStashController.updateTaskbarTouchRegion()
+                }
+            } else {
+                cancelFlyout()
+                moveToState(AnimatingBubble.State.IN)
+            }
+        } else {
+            // the flyout is already gone. if we're animating the handle cancel it. the
+            // animation itself can handle morphing back into the bubble bar and restarting
+            // and show the flyout.
+            val handleAnimator = bubbleStashController.getStashedHandlePhysicsAnimator()
+            if (handleAnimator != null && handleAnimator.isRunning()) {
+                interceptedHandleAnimator = true
+                handleAnimator.cancel()
+            }
+
+            // if we're not animating the handle, then the hide animation simply hides the
+            // flyout, but if the flyout is gone then the animation has ended.
+        }
+    }
+
     private fun cancelHideAnimation() {
         val hideAnimation = animatingBubble?.hideAnimation ?: return
         scheduler.cancel(hideAnimation)
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutController.kt
index 4939c99..908e97c 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutController.kt
@@ -16,24 +16,54 @@
 
 package com.android.launcher3.taskbar.bubbles.flyout
 
+import android.graphics.Rect
 import android.view.Gravity
 import android.view.ViewGroup
 import android.widget.FrameLayout
+import androidx.core.animation.ValueAnimator
+import com.android.app.animation.InterpolatorsAndroidX
 import com.android.launcher3.R
+import com.android.systemui.util.addListener
 
 /** Creates and manages the visibility of the [BubbleBarFlyoutView]. */
-class BubbleBarFlyoutController(
+class BubbleBarFlyoutController
+@JvmOverloads
+constructor(
     private val container: FrameLayout,
     private val positioner: BubbleBarFlyoutPositioner,
+    private val callbacks: FlyoutCallbacks,
+    private val flyoutScheduler: FlyoutScheduler = HandlerScheduler(container),
 ) {
 
+    private companion object {
+        const val EXPAND_ANIMATION_DURATION_MS = 400L
+        const val COLLAPSE_ANIMATION_DURATION_MS = 350L
+    }
+
     private var flyout: BubbleBarFlyoutView? = null
+    private var animator: ValueAnimator? = null
     private val horizontalMargin =
         container.context.resources.getDimensionPixelSize(R.dimen.transient_taskbar_bottom_margin)
 
-    fun setUpFlyout(message: BubbleBarFlyoutMessage) {
+    private enum class AnimationType {
+        /** Morphs the flyout between a dot and a rounded rectangle. */
+        MORPH,
+        /** Fades the flyout in or out. */
+        FADE,
+    }
+
+    /** The bounds of the flyout. */
+    val flyoutBounds: Rect?
+        get() {
+            val flyout = this.flyout ?: return null
+            val rect = Rect(flyout.bounds)
+            rect.offset(0, flyout.translationY.toInt())
+            return rect
+        }
+
+    fun setUpAndShowFlyout(message: BubbleBarFlyoutMessage, onInit: () -> Unit, onEnd: () -> Unit) {
         flyout?.let(container::removeView)
-        val flyout = BubbleBarFlyoutView(container.context, onLeft = positioner.isOnLeft)
+        val flyout = BubbleBarFlyoutView(container.context, positioner, flyoutScheduler)
 
         flyout.translationY = positioner.targetTy
 
@@ -47,13 +77,120 @@
         lp.marginEnd = horizontalMargin
         container.addView(flyout, lp)
 
-        flyout.setData(message)
         this.flyout = flyout
+        flyout.showFromCollapsed(message) {
+            flyout.updateExpansionProgress(0f)
+            onInit()
+            showFlyout(AnimationType.MORPH, onEnd)
+        }
     }
 
-    fun hideFlyout() {
+    private fun showFlyout(animationType: AnimationType, endAction: () -> Unit) {
         val flyout = this.flyout ?: return
+        val startValue = getCurrentAnimatedValueIfRunning() ?: 0f
+        val duration = (EXPAND_ANIMATION_DURATION_MS * (1f - startValue)).toLong()
+        animator?.cancel()
+        val animator = ValueAnimator.ofFloat(startValue, 1f).setDuration(duration)
+        animator.interpolator = InterpolatorsAndroidX.EMPHASIZED
+        this.animator = animator
+        when (animationType) {
+            AnimationType.FADE ->
+                animator.addUpdateListener { _ -> flyout.alpha = animator.animatedValue as Float }
+            AnimationType.MORPH ->
+                animator.addUpdateListener { _ ->
+                    flyout.updateExpansionProgress(animator.animatedValue as Float)
+                }
+        }
+        animator.addListener(
+            onStart = { extendTopBoundary() },
+            onEnd = {
+                endAction()
+                flyout.setOnClickListener { callbacks.flyoutClicked() }
+            },
+        )
+        animator.start()
+    }
+
+    fun updateFlyoutFullyExpanded(message: BubbleBarFlyoutMessage, onEnd: () -> Unit) {
+        val flyout = flyout ?: return
+        hideFlyout(AnimationType.FADE) {
+            callbacks.resetTopBoundary()
+            flyout.updateData(message) { showFlyout(AnimationType.FADE, onEnd) }
+        }
+    }
+
+    fun updateFlyoutWhileExpanding(message: BubbleBarFlyoutMessage) {
+        val flyout = flyout ?: return
+        flyout.updateData(message) { extendTopBoundary() }
+    }
+
+    fun updateFlyoutWhileCollapsing(message: BubbleBarFlyoutMessage, onEnd: () -> Unit) {
+        val flyout = flyout ?: return
+        animator?.pause()
+        animator?.removeAllListeners()
+        flyout.updateData(message) { showFlyout(AnimationType.MORPH, onEnd) }
+    }
+
+    private fun extendTopBoundary() {
+        val flyout = flyout ?: return
+        val flyoutTop = flyout.top + flyout.translationY
+        // If the top position of the flyout is negative, then it's bleeding over the
+        // top boundary of its parent view
+        if (flyoutTop < 0) callbacks.extendTopBoundary(space = -flyoutTop.toInt())
+    }
+
+    fun cancelFlyout(endAction: () -> Unit) {
+        hideFlyout(AnimationType.FADE) {
+            cleanupFlyoutView()
+            endAction()
+        }
+    }
+
+    fun collapseFlyout(endAction: () -> Unit) {
+        hideFlyout(AnimationType.MORPH) {
+            cleanupFlyoutView()
+            endAction()
+        }
+    }
+
+    private fun hideFlyout(animationType: AnimationType, endAction: () -> Unit) {
+        val flyout = this.flyout ?: return
+        val startValue = getCurrentAnimatedValueIfRunning() ?: 1f
+        val duration = (COLLAPSE_ANIMATION_DURATION_MS * startValue).toLong()
+        animator?.cancel()
+        val animator = ValueAnimator.ofFloat(startValue, 0f).setDuration(duration)
+        animator.interpolator = InterpolatorsAndroidX.EMPHASIZED
+        this.animator = animator
+        when (animationType) {
+            AnimationType.FADE ->
+                animator.addUpdateListener { _ -> flyout.alpha = animator.animatedValue as Float }
+            AnimationType.MORPH ->
+                animator.addUpdateListener { _ ->
+                    flyout.updateExpansionProgress(animator.animatedValue as Float)
+                }
+        }
+        animator.addListener(
+            onStart = {
+                flyout.setOnClickListener(null)
+                if (animationType == AnimationType.MORPH) {
+                    flyout.updateTranslationToCollapsedPosition()
+                }
+            },
+            onEnd = { endAction() },
+        )
+        animator.start()
+    }
+
+    private fun cleanupFlyoutView() {
         container.removeView(flyout)
-        this.flyout = null
+        this@BubbleBarFlyoutController.flyout = null
+        callbacks.resetTopBoundary()
+    }
+
+    fun hasFlyout() = flyout != null
+
+    private fun getCurrentAnimatedValueIfRunning(): Float? {
+        val animator = animator ?: return null
+        return if (animator.isRunning) animator.animatedValue as Float else null
     }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutMessage.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutMessage.kt
index 7298297..14b456c 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutMessage.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutMessage.kt
@@ -18,9 +18,4 @@
 
 import android.graphics.drawable.Drawable
 
-data class BubbleBarFlyoutMessage(
-    val senderAvatar: Drawable?,
-    val senderName: CharSequence,
-    val message: CharSequence,
-    val isGroupChat: Boolean,
-)
+data class BubbleBarFlyoutMessage(val icon: Drawable?, val title: String, val message: String)
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutPositioner.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutPositioner.kt
index deed1f5..aa2555e 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutPositioner.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutPositioner.kt
@@ -16,6 +16,8 @@
 
 package com.android.launcher3.taskbar.bubbles.flyout
 
+import android.graphics.PointF
+
 /** Provides positioning data to the flyout view. */
 interface BubbleBarFlyoutPositioner {
 
@@ -24,4 +26,26 @@
 
     /** The target translation Y that the flyout view should have when displayed. */
     val targetTy: Float
+
+    /**
+     * The distance between the expanded position of the flyout and the collapsed position.
+     *
+     * The distance is calculated between the bottom corner which is aligned with the bubble bar.
+     */
+    val distanceToCollapsedPosition: PointF
+
+    /** The size of the flyout when collapsed. */
+    val collapsedSize: Float
+
+    /** The color of the flyout when collapsed. */
+    val collapsedColor: Int
+
+    /** The elevation of the flyout when collapsed. */
+    val collapsedElevation: Float
+
+    /**
+     * The distance the flyout must pass from its collapsed position until it can start revealing
+     * the triangle.
+     */
+    val distanceToRevealTriangle: Float
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutView.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutView.kt
index 4b91f46..f9f5a15 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutView.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutView.kt
@@ -20,24 +20,43 @@
 import android.content.res.Configuration
 import android.graphics.Canvas
 import android.graphics.Color
+import android.graphics.Outline
 import android.graphics.Paint
 import android.graphics.Path
+import android.graphics.PointF
+import android.graphics.Rect
+import android.graphics.RectF
 import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewOutlineProvider
 import android.widget.ImageView
 import android.widget.TextView
 import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.core.animation.ArgbEvaluator
 import com.android.launcher3.R
 import com.android.launcher3.popup.RoundedArrowDrawable
+import kotlin.math.min
 
 /** The flyout view used to notify the user of a new bubble notification. */
-class BubbleBarFlyoutView(context: Context, private val onLeft: Boolean) :
-    ConstraintLayout(context) {
+class BubbleBarFlyoutView(
+    context: Context,
+    private val positioner: BubbleBarFlyoutPositioner,
+    scheduler: FlyoutScheduler? = null,
+) : ConstraintLayout(context) {
 
-    private val sender: TextView by
-        lazy(LazyThreadSafetyMode.NONE) { findViewById(R.id.bubble_flyout_name) }
+    private companion object {
+        // the minimum progress of the expansion animation before the content starts fading in.
+        const val MIN_EXPANSION_PROGRESS_FOR_CONTENT_ALPHA = 0.75f
+        // the rate multiple for the background color animation relative to the morph animation.
+        const val BACKGROUND_COLOR_CHANGE_RATE = 5
+    }
 
-    private val avatar: ImageView by
-        lazy(LazyThreadSafetyMode.NONE) { findViewById(R.id.bubble_flyout_avatar) }
+    private val scheduler: FlyoutScheduler = scheduler ?: HandlerScheduler(this)
+    private val title: TextView by
+        lazy(LazyThreadSafetyMode.NONE) { findViewById(R.id.bubble_flyout_title) }
+
+    private val icon: ImageView by
+        lazy(LazyThreadSafetyMode.NONE) { findViewById(R.id.bubble_flyout_icon) }
 
     private val message: TextView by
         lazy(LazyThreadSafetyMode.NONE) { findViewById(R.id.bubble_flyout_text) }
@@ -79,9 +98,43 @@
             context.resources.getDimensionPixelSize(R.dimen.bubblebar_flyout_max_width)
         }
 
+    private val flyoutElevation by
+        lazy(LazyThreadSafetyMode.NONE) {
+            context.resources.getDimensionPixelSize(R.dimen.bubblebar_flyout_elevation).toFloat()
+        }
+
+    /** The bounds of the background rect. */
+    private val backgroundRect = RectF()
     private val cornerRadius: Float
     private val triangle: Path = Path()
+    private val triangleOutline = Outline()
     private var backgroundColor = Color.BLACK
+    /** Represents the progress of the expansion animation. 0 when collapsed. 1 when expanded. */
+    private var expansionProgress = 0f
+    /** Translation x-y values to move the flyout to its collapsed position. */
+    private var translationToCollapsedPosition = PointF(0f, 0f)
+    /** The size of the flyout when it's collapsed. */
+    private var collapsedSize = 0f
+    /** The corner radius of the flyout when it's collapsed. */
+    private var collapsedCornerRadius = 0f
+    /** The color of the flyout when collapsed. */
+    private var collapsedColor = 0
+    /** The elevation of the flyout when collapsed. */
+    private var collapsedElevation = 0f
+    /** The minimum progress of the expansion animation before the triangle is made visible. */
+    private var minExpansionProgressForTriangle = 0f
+
+    /** The corner radius of the background according to the progress of the animation. */
+    private val currentCornerRadius
+        get() = collapsedCornerRadius + (cornerRadius - collapsedCornerRadius) * expansionProgress
+
+    /** Translation X of the background. */
+    private val backgroundRectTx
+        get() = translationToCollapsedPosition.x * (1 - expansionProgress)
+
+    /** Translation Y of the background. */
+    private val backgroundRectTy
+        get() = translationToCollapsedPosition.y * (1 - expansionProgress)
 
     /**
      * The paint used to draw the background, whose color changes as the flyout transitions to the
@@ -89,22 +142,25 @@
      */
     private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.FILTER_BITMAP_FLAG)
 
+    /** The bounds of the flyout relative to the parent view. */
+    val bounds = Rect()
+
     init {
         LayoutInflater.from(context).inflate(R.layout.bubblebar_flyout, this, true)
+        id = R.id.bubble_bar_flyout_view
 
         val ta = context.obtainStyledAttributes(intArrayOf(android.R.attr.dialogCornerRadius))
         cornerRadius = ta.getDimensionPixelSize(0, 0).toFloat()
         ta.recycle()
 
         setWillNotDraw(false)
-        clipChildren = false
+        clipChildren = true
         clipToPadding = false
 
         val padding = context.resources.getDimensionPixelSize(R.dimen.bubblebar_flyout_padding)
         // add extra padding to the bottom of the view to include the triangle
         setPadding(padding, padding, padding, padding + triangleHeight - triangleOverlap)
-        translationZ =
-            context.resources.getDimensionPixelSize(R.dimen.bubblebar_flyout_elevation).toFloat()
+        translationZ = flyoutElevation
 
         RoundedArrowDrawable.addDownPointingRoundedTriangleToPath(
             triangleWidth.toFloat(),
@@ -112,24 +168,71 @@
             triangleRadius.toFloat(),
             triangle,
         )
+        triangleOutline.setPath(triangle)
+
+        outlineProvider =
+            object : ViewOutlineProvider() {
+                override fun getOutline(view: View, outline: Outline) {
+                    this@BubbleBarFlyoutView.getOutline(outline)
+                }
+            }
+        clipToOutline = true
 
         applyConfigurationColors(resources.configuration)
     }
 
-    fun setData(flyoutMessage: BubbleBarFlyoutMessage) {
-        // the avatar is only displayed in group chat messages
-        if (flyoutMessage.senderAvatar != null && flyoutMessage.isGroupChat) {
-            avatar.visibility = VISIBLE
-            avatar.setImageDrawable(flyoutMessage.senderAvatar)
+    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
+        super.onLayout(changed, left, top, right, bottom)
+        bounds.left = left
+        bounds.top = top
+        bounds.right = right
+        bounds.bottom = bottom
+    }
+
+    /** Sets the data for the flyout and starts playing the expand animation. */
+    fun showFromCollapsed(flyoutMessage: BubbleBarFlyoutMessage, expandAnimation: () -> Unit) {
+        icon.alpha = 0f
+        title.alpha = 0f
+        message.alpha = 0f
+        setData(flyoutMessage)
+
+        updateTranslationToCollapsedPosition()
+        collapsedSize = positioner.collapsedSize
+        collapsedCornerRadius = collapsedSize / 2
+        collapsedColor = positioner.collapsedColor
+        collapsedElevation = positioner.collapsedElevation
+
+        // calculate the expansion progress required before we start showing the triangle as part of
+        // the expansion animation
+        minExpansionProgressForTriangle =
+            positioner.distanceToRevealTriangle / translationToCollapsedPosition.y
+
+        backgroundPaint.color = collapsedColor
+
+        // post the request to start the expand animation to the looper so the view can measure
+        // itself
+        scheduler.runAfterLayout(expandAnimation)
+    }
+
+    /** Updates the content of the flyout and schedules [afterLayout] to run after a layout pass. */
+    fun updateData(flyoutMessage: BubbleBarFlyoutMessage, afterLayout: () -> Unit) {
+        setData(flyoutMessage)
+        scheduler.runAfterLayout(afterLayout)
+    }
+
+    private fun setData(flyoutMessage: BubbleBarFlyoutMessage) {
+        if (flyoutMessage.icon != null) {
+            icon.visibility = VISIBLE
+            icon.setImageDrawable(flyoutMessage.icon)
         } else {
-            avatar.visibility = GONE
+            icon.visibility = GONE
         }
 
         val minTextViewWidth: Int
         val maxTextViewWidth: Int
-        if (avatar.visibility == VISIBLE) {
-            minTextViewWidth = minFlyoutWidth - avatar.width - flyoutPadding * 2
-            maxTextViewWidth = maxFlyoutWidth - avatar.width - flyoutPadding * 2
+        if (icon.visibility == VISIBLE) {
+            minTextViewWidth = minFlyoutWidth - icon.width - flyoutPadding * 2
+            maxTextViewWidth = maxFlyoutWidth - icon.width - flyoutPadding * 2
         } else {
             // when there's no avatar, the width of the text view is constant, so we're setting the
             // min and max to the same value
@@ -137,13 +240,13 @@
             maxTextViewWidth = minTextViewWidth
         }
 
-        if (flyoutMessage.senderName.isEmpty()) {
-            sender.visibility = GONE
+        if (flyoutMessage.title.isEmpty()) {
+            title.visibility = GONE
         } else {
-            sender.minWidth = minTextViewWidth
-            sender.maxWidth = maxTextViewWidth
-            sender.text = flyoutMessage.senderName
-            sender.visibility = VISIBLE
+            title.minWidth = minTextViewWidth
+            title.maxWidth = maxTextViewWidth
+            title.text = flyoutMessage.title
+            title.visibility = VISIBLE
         }
 
         message.minWidth = minTextViewWidth
@@ -151,28 +254,149 @@
         message.text = flyoutMessage.message
     }
 
+    /**
+     * This should be called to update [translationToCollapsedPosition] before we start expanding or
+     * collapsing to make sure that we're animating the flyout to and from the correct position.
+     */
+    fun updateTranslationToCollapsedPosition() {
+        val txToCollapsedPosition =
+            if (positioner.isOnLeft) {
+                positioner.distanceToCollapsedPosition.x
+            } else {
+                -positioner.distanceToCollapsedPosition.x
+            }
+        val tyToCollapsedPosition =
+            positioner.distanceToCollapsedPosition.y + triangleHeight - triangleOverlap
+        translationToCollapsedPosition = PointF(txToCollapsedPosition, tyToCollapsedPosition)
+    }
+
+    /** Updates the flyout view with the progress of the animation. */
+    fun updateExpansionProgress(fraction: Float) {
+        expansionProgress = fraction
+
+        updateTranslationForAnimation(message)
+        updateTranslationForAnimation(title)
+        updateTranslationForAnimation(icon)
+
+        // start fading in the content only after we're past the threshold
+        val alpha =
+            ((expansionProgress - MIN_EXPANSION_PROGRESS_FOR_CONTENT_ALPHA) /
+                    (1f - MIN_EXPANSION_PROGRESS_FOR_CONTENT_ALPHA))
+                .coerceIn(0f, 1f)
+        title.alpha = alpha
+        message.alpha = alpha
+        icon.alpha = alpha
+
+        translationZ =
+            collapsedElevation + (flyoutElevation - collapsedElevation) * expansionProgress
+
+        invalidate()
+    }
+
     override fun onDraw(canvas: Canvas) {
-        canvas.drawRoundRect(
-            0f,
-            0f,
-            width.toFloat(),
+        // interpolate the width, height, corner radius and translation based on the progress of the
+        // animation.
+        // the background is drawn from the bottom left corner to the top right corner if we're
+        // positioned on the left, and from the bottom right corner to the top left if we're
+        // positioned on the right.
+
+        // the current width of the background rect according to the progress of the animation
+        val currentWidth = collapsedSize + (width - collapsedSize) * expansionProgress
+        val rectBottom = height - triangleHeight + triangleOverlap
+        val currentHeight = collapsedSize + (rectBottom - collapsedSize) * expansionProgress
+
+        backgroundRect.set(
+            if (positioner.isOnLeft) 0f else width.toFloat() - currentWidth,
+            height.toFloat() - triangleHeight + triangleOverlap - currentHeight,
+            if (positioner.isOnLeft) currentWidth else width.toFloat(),
             height.toFloat() - triangleHeight + triangleOverlap,
-            cornerRadius,
-            cornerRadius,
+        )
+
+        // transform the flyout color between the collapsed and expanded states. the color
+        // transformation completes at a faster rate (BACKGROUND_COLOR_CHANGE_RATE) than the
+        // expansion animation. this helps make the color change smooth.
+        backgroundPaint.color =
+            ArgbEvaluator.getInstance()
+                .evaluate(
+                    min(expansionProgress * BACKGROUND_COLOR_CHANGE_RATE, 1f),
+                    collapsedColor,
+                    backgroundColor,
+                )
+
+        canvas.save()
+        canvas.translate(backgroundRectTx, backgroundRectTy)
+        // draw the background starting from the bottom left if we're positioned left, or the bottom
+        // right if we're positioned right.
+        canvas.drawRoundRect(
+            backgroundRect,
+            currentCornerRadius,
+            currentCornerRadius,
             backgroundPaint,
         )
-        drawTriangle(canvas)
+        if (expansionProgress >= minExpansionProgressForTriangle) {
+            drawTriangle(canvas)
+        }
+        canvas.restore()
+        invalidateOutline()
         super.onDraw(canvas)
     }
 
     private fun drawTriangle(canvas: Canvas) {
         canvas.save()
-        val triangleX = if (onLeft) cornerRadius else width - cornerRadius - triangleWidth
-        canvas.translate(triangleX, (height - triangleHeight).toFloat())
+        val triangleX =
+            if (positioner.isOnLeft) {
+                currentCornerRadius
+            } else {
+                width - currentCornerRadius - triangleWidth
+            }
+        // instead of scaling the triangle, increasingly reveal it from the background. this has the
+        // effect of the triangle scaling.
+
+        // the translation y of the triangle before we start revealing it. align its bottom with the
+        // bottom of the rect
+        val triangleYCollapsed = height - triangleHeight - (triangleHeight - triangleOverlap)
+        // the translation y of the triangle when it's fully revealed
+        val triangleYExpanded = height - triangleHeight
+        val interpolatedExpansion =
+            ((expansionProgress - minExpansionProgressForTriangle) /
+                    (1 - minExpansionProgressForTriangle))
+                .coerceIn(0f, 1f)
+        val triangleY =
+            triangleYCollapsed + (triangleYExpanded - triangleYCollapsed) * interpolatedExpansion
+        canvas.translate(triangleX, triangleY)
         canvas.drawPath(triangle, backgroundPaint)
+        triangleOutline.setPath(triangle)
+        triangleOutline.offset(triangleX.toInt(), triangleY.toInt())
         canvas.restore()
     }
 
+    private fun getOutline(outline: Outline) {
+        val path = Path()
+        path.addRoundRect(
+            backgroundRect,
+            currentCornerRadius,
+            currentCornerRadius,
+            Path.Direction.CW,
+        )
+        if (expansionProgress >= minExpansionProgressForTriangle) {
+            path.addPath(triangleOutline.mPath)
+        }
+        outline.setPath(path)
+        outline.offset(backgroundRectTx.toInt(), backgroundRectTy.toInt())
+    }
+
+    private fun updateTranslationForAnimation(view: View) {
+        val tx =
+            if (positioner.isOnLeft) {
+                translationToCollapsedPosition.x - view.left
+            } else {
+                width - view.left - translationToCollapsedPosition.x
+            }
+        val ty = height - view.top + translationToCollapsedPosition.y
+        view.translationX = tx * (1f - expansionProgress)
+        view.translationY = ty * (1f - expansionProgress)
+    }
+
     private fun applyConfigurationColors(configuration: Configuration) {
         val nightModeFlags = configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
         val isNightModeOn = nightModeFlags == Configuration.UI_MODE_NIGHT_YES
@@ -187,7 +411,7 @@
                 )
             )
         backgroundColor = ta.getColor(0, defaultBackgroundColor)
-        sender.setTextColor(ta.getColor(1, defaultTextColor))
+        title.setTextColor(ta.getColor(1, defaultTextColor))
         message.setTextColor(ta.getColor(2, defaultTextColor))
         ta.recycle()
         backgroundPaint.color = backgroundColor
diff --git a/tests/multivalentTests/src/com/android/launcher3/model/ModelTestRule.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/FlyoutCallbacks.kt
similarity index 60%
copy from tests/multivalentTests/src/com/android/launcher3/model/ModelTestRule.kt
copy to quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/FlyoutCallbacks.kt
index ad2c2a4..e2f010a 100644
--- a/tests/multivalentTests/src/com/android/launcher3/model/ModelTestRule.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/FlyoutCallbacks.kt
@@ -14,14 +14,16 @@
  * limitations under the License.
  */
 
-package com.android.launcher3.model
+package com.android.launcher3.taskbar.bubbles.flyout
 
-import com.android.launcher3.util.RoboApiWrapper
-import org.junit.rules.TestWatcher
-import org.junit.runner.Description
+/** Callbacks that the flyout uses to notify of events. */
+interface FlyoutCallbacks {
+    /** Requests to extend the top boundary of the parent to fully include the flyout. */
+    fun extendTopBoundary(space: Int)
 
-class ModelTestRule : TestWatcher() {
-    override fun starting(description: Description?) {
-        RoboApiWrapper.initialize()
-    }
+    /** Resets the top boundary of the parent. */
+    fun resetTopBoundary()
+
+    /** The flyout was clicked. */
+    fun flyoutClicked()
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/FlyoutScheduler.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/FlyoutScheduler.kt
new file mode 100644
index 0000000..6f5d700
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/flyout/FlyoutScheduler.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.bubbles.flyout
+
+import android.view.View
+
+/** Interface for scheduling jobs by flyout. */
+fun interface FlyoutScheduler {
+    /** Runs the given [block] after layout. */
+    fun runAfterLayout(block: () -> Unit)
+}
+
+/** A [FlyoutScheduler] that uses a Handler to schedule jobs. */
+class HandlerScheduler(val view: View) : FlyoutScheduler {
+    override fun runAfterLayout(block: () -> Unit) {
+        view.post(block)
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/BubbleBarLocationOnDemandListener.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/BubbleBarLocationOnDemandListener.kt
new file mode 100644
index 0000000..ffe7c44
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/BubbleBarLocationOnDemandListener.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.bubbles.stashing
+
+import com.android.launcher3.taskbar.bubbles.BubbleBarController.BubbleBarLocationListener
+import com.android.wm.shell.shared.bubbles.BubbleBarLocation
+
+/** On demand implementation of [BubbleBarLocationListener]. */
+class BubbleBarLocationOnDemandListener(
+    private val listenerProvider: () -> BubbleBarLocationListener
+) : BubbleBarLocationListener {
+
+    override fun onBubbleBarLocationAnimated(location: BubbleBarLocation) {
+        listenerProvider().onBubbleBarLocationAnimated(location)
+    }
+
+    override fun onBubbleBarLocationUpdated(location: BubbleBarLocation) {
+        listenerProvider().onBubbleBarLocationUpdated(location)
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/BubbleStashController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/BubbleStashController.kt
index 9721792..d9589bb 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/BubbleStashController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/BubbleStashController.kt
@@ -42,12 +42,6 @@
 
         /** Provides taskbar height in pixels. */
         fun getTaskbarHeight(): Int
-
-        /** Provides hotseat bottom space in pixels. */
-        fun getHotseatBottomSpace(): Int
-
-        /** Provides hotseat height in pixels. */
-        fun getHotseatHeight(): Int
     }
 
     /** Execute passed action only after controllers are initiated. */
@@ -56,14 +50,29 @@
         fun runAfterInit(action: Runnable)
     }
 
+    /** Launcher states bubbles cares about */
+    enum class BubbleLauncherState {
+        /* When launcher is in overview */
+        OVERVIEW,
+        /* When launcher is on home */
+        HOME,
+        /* We're in an app */
+        IN_APP,
+    }
+
+    /** The current launcher state */
+    var launcherState: BubbleLauncherState
+
     /** Whether bubble bar is currently stashed */
     val isStashed: Boolean
 
     /** Whether launcher enters or exits the home page. */
-    var isBubblesShowingOnHome: Boolean
+    val isBubblesShowingOnHome: Boolean
+        get() = launcherState == BubbleLauncherState.HOME
 
     /** Whether launcher enters or exits the overview page. */
-    var isBubblesShowingOnOverview: Boolean
+    val isBubblesShowingOnOverview: Boolean
+        get() = launcherState == BubbleLauncherState.OVERVIEW
 
     /** Updated when sysui locked state changes, when locked, bubble bar is not shown. */
     var isSysuiLocked: Boolean
@@ -79,7 +88,7 @@
         taskbarInsetsController: TaskbarInsetsController,
         bubbleBarViewController: BubbleBarViewController,
         bubbleStashedHandleViewController: BubbleStashedHandleViewController?,
-        controllersAfterInitAction: ControllersAfterInitAction
+        controllersAfterInitAction: ControllersAfterInitAction,
     )
 
     /** Shows the bubble bar at [bubbleBarTranslationY] position immediately without animation. */
@@ -112,6 +121,9 @@
     /** Set a bubble bar location */
     fun setBubbleBarLocation(bubbleBarLocation: BubbleBarLocation)
 
+    /** Set the hotseat vertical center that bubble bar will align with. */
+    fun setHotseatVerticalCenter(hotseatVerticalCenter: Int)
+
     /**
      * Stashes the bubble bar (transform to the handle view), or just shrink width of the expanded
      * bubble bar based on the controller implementation.
@@ -164,12 +176,18 @@
                 bubbleBarTranslationYForTaskbar
             }
 
-    /** Translation Y to align the bubble bar with the hotseat. */
+    /** Translation Y to align the bubble bar with the taskbar. */
     val bubbleBarTranslationYForTaskbar: Float
 
-    /** Return translation Y to align the bubble bar with the taskbar. */
+    /** Return translation Y to align the bubble bar with the hotseat. */
     val bubbleBarTranslationYForHotseat: Float
 
+    /**
+     * Show bubble bar is if it were in-app while launcher state is still on home. Set as a progress
+     * value between 0 and 1: 0 - use home layout, 1 - use in-app layout.
+     */
+    var inAppDisplayOverrideProgress: Float
+
     /** Dumps the state of BubbleStashController. */
     fun dump(pw: PrintWriter) {
         pw.println("Bubble stash controller state:")
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/DeviceProfileDimensionsProviderAdapter.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/DeviceProfileDimensionsProviderAdapter.kt
index a55763b..886b9f0 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/DeviceProfileDimensionsProviderAdapter.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/DeviceProfileDimensionsProviderAdapter.kt
@@ -27,13 +27,9 @@
 class DeviceProfileDimensionsProviderAdapter(
     private val taskbarActivityContext: TaskbarActivityContext
 ) : TaskbarHotseatDimensionsProvider {
-    override fun getTaskbarBottomSpace(): Int = deviceProfile().taskbarBottomMargin
+    override fun getTaskbarBottomSpace(): Int = taskbarDp().taskbarBottomMargin
 
-    override fun getTaskbarHeight(): Int = deviceProfile().taskbarHeight
+    override fun getTaskbarHeight(): Int = taskbarDp().taskbarHeight
 
-    override fun getHotseatBottomSpace(): Int = deviceProfile().hotseatBarBottomSpacePx
-
-    override fun getHotseatHeight(): Int = deviceProfile().hotseatCellHeightPx
-
-    private fun deviceProfile(): DeviceProfile = taskbarActivityContext.deviceProfile
+    private fun taskbarDp(): DeviceProfile = taskbarActivityContext.deviceProfile
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashController.kt
index 7d6f7ad..45f5568 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashController.kt
@@ -22,10 +22,13 @@
 import android.graphics.Rect
 import android.view.MotionEvent
 import android.view.View
+import com.android.app.animation.Interpolators
+import com.android.launcher3.Utilities
 import com.android.launcher3.anim.AnimatedFloat
 import com.android.launcher3.taskbar.TaskbarInsetsController
 import com.android.launcher3.taskbar.bubbles.BubbleBarViewController
 import com.android.launcher3.taskbar.bubbles.BubbleStashedHandleViewController
+import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.BubbleLauncherState
 import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.Companion.BAR_STASH_DURATION
 import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.Companion.BAR_TRANSLATION_DURATION
 import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.ControllersAfterInitAction
@@ -35,7 +38,7 @@
 import com.android.wm.shell.shared.bubbles.BubbleBarLocation
 
 class PersistentBubbleStashController(
-    private val taskbarHotseatDimensionsProvider: TaskbarHotseatDimensionsProvider,
+    private val taskbarHotseatDimensionsProvider: TaskbarHotseatDimensionsProvider
 ) : BubbleStashController {
 
     private lateinit var taskbarInsetsController: TaskbarInsetsController
@@ -44,32 +47,26 @@
     private lateinit var bubbleBarAlphaAnimator: MultiPropertyFactory<View>.MultiProperty
     private lateinit var bubbleBarScaleAnimator: AnimatedFloat
     private lateinit var controllersAfterInitAction: ControllersAfterInitAction
+    private var hotseatVerticalCenter: Int = 0
 
-    override var isBubblesShowingOnHome: Boolean = false
-        set(onHome) {
-            if (field == onHome) return
-            field = onHome
-            if (!bubbleBarViewController.hasBubbles()) {
+    override var launcherState: BubbleLauncherState = BubbleLauncherState.IN_APP
+        set(state) {
+            if (field == state) return
+            val transitionFromHome = field == BubbleLauncherState.HOME
+            field = state
+            val hasBubbles = bubbleBarViewController.hasBubbles()
+            bubbleBarViewController.onBubbleBarConfigurationChanged(hasBubbles)
+            if (!hasBubbles) {
                 // if there are no bubbles, there's nothing to show, so just return.
                 return
             }
-            if (onHome) {
-                // When transition to home we should show collapse the bubble bar
-                updateExpandedState(expand = false)
+            // If we're transitioning anywhere, bubble bar should be collapsed
+            updateExpandedState(expand = false)
+            if (transitionFromHome || field == BubbleLauncherState.HOME) {
+                // If we're transitioning to or from home, animate the Y because we're in hotseat
+                // on home but in persistent taskbar elsewhere so the position is different.
+                animateBubbleBarY()
             }
-            animateBubbleBarY()
-            bubbleBarViewController.onBubbleBarConfigurationChanged(/* animate= */ true)
-        }
-
-    override var isBubblesShowingOnOverview: Boolean = false
-        set(onOverview) {
-            if (field == onOverview) return
-            field = onOverview
-            if (!onOverview) {
-                // When transition from overview we should show collapse the bubble bar
-                updateExpandedState(expand = false)
-            }
-            bubbleBarViewController.onBubbleBarConfigurationChanged(/* animate= */ true)
         }
 
     override var isSysuiLocked: Boolean = false
@@ -99,17 +96,46 @@
 
     override val bubbleBarTranslationYForHotseat: Float
         get() {
-            val hotseatBottomSpace = taskbarHotseatDimensionsProvider.getHotseatBottomSpace()
-            val hotseatCellHeight = taskbarHotseatDimensionsProvider.getHotseatHeight()
-            val bubbleBarHeight: Float = bubbleBarViewController.bubbleBarCollapsedHeight
-            return -hotseatBottomSpace - (hotseatCellHeight - bubbleBarHeight) / 2
+            val bubbleBarHeight = bubbleBarViewController.bubbleBarCollapsedHeight
+            return -hotseatVerticalCenter + bubbleBarHeight / 2
+        }
+
+    override val bubbleBarTranslationY: Float
+        get() =
+            if (inAppDisplayOverrideProgress > 0f && launcherState == BubbleLauncherState.HOME) {
+                Utilities.mapToRange(
+                    inAppDisplayOverrideProgress,
+                    /* fromMin = */ 0f,
+                    /* fromMax = */ 1f,
+                    bubbleBarTranslationYForHotseat,
+                    bubbleBarTranslationYForTaskbar,
+                    Interpolators.LINEAR,
+                )
+            } else {
+                super.bubbleBarTranslationY
+            }
+
+    override var inAppDisplayOverrideProgress: Float = 0f
+        set(value) {
+            if (field == value) return
+            field = value
+            if (launcherState == BubbleLauncherState.HOME) {
+                if (bubbleBarTranslationYAnimator.isAnimating) {
+                    bubbleBarTranslationYAnimator.cancelAnimation()
+                }
+                bubbleBarTranslationYAnimator.updateValue(bubbleBarTranslationY)
+                if (value == 0f || value == 1f) {
+                    // Update insets only when we reach the end values
+                    taskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
+                }
+            }
         }
 
     override fun init(
         taskbarInsetsController: TaskbarInsetsController,
         bubbleBarViewController: BubbleBarViewController,
         bubbleStashedHandleViewController: BubbleStashedHandleViewController?,
-        controllersAfterInitAction: ControllersAfterInitAction
+        controllersAfterInitAction: ControllersAfterInitAction,
     ) {
         this.taskbarInsetsController = taskbarInsetsController
         this.bubbleBarViewController = bubbleBarViewController
@@ -126,13 +152,17 @@
             animatorSet.playTogether(
                 bubbleBarScaleAnimator.animateToValue(1f),
                 bubbleBarTranslationYAnimator.animateToValue(bubbleBarTranslationY),
-                bubbleBarAlphaAnimator.animateToValue(1f)
+                bubbleBarAlphaAnimator.animateToValue(1f),
             )
         }
         updateTouchRegionOnAnimationEnd(animatorSet)
         animatorSet.setDuration(BAR_STASH_DURATION).start()
     }
 
+    override fun setHotseatVerticalCenter(hotseatVerticalCenter: Int) {
+        this.hotseatVerticalCenter = hotseatVerticalCenter
+    }
+
     override fun showBubbleBarImmediate() = showBubbleBarImmediate(bubbleBarTranslationY)
 
     override fun showBubbleBarImmediate(bubbleBarTranslationY: Float) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashController.kt
index 4f0337d..e62c0d4 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashController.kt
@@ -32,10 +32,11 @@
 import com.android.launcher3.anim.AnimatedFloat
 import com.android.launcher3.anim.SpringAnimationBuilder
 import com.android.launcher3.taskbar.TaskbarInsetsController
-import com.android.launcher3.taskbar.TaskbarStashController.TASKBAR_STASH_ALPHA_DURATION
 import com.android.launcher3.taskbar.TaskbarStashController.TASKBAR_STASH_ALPHA_START_DELAY
+import com.android.launcher3.taskbar.TaskbarStashController.TRANSIENT_TASKBAR_STASH_ALPHA_DURATION
 import com.android.launcher3.taskbar.bubbles.BubbleBarViewController
 import com.android.launcher3.taskbar.bubbles.BubbleStashedHandleViewController
+import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.BubbleLauncherState
 import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.Companion.BAR_STASH_DURATION
 import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.Companion.BAR_TRANSLATION_DURATION
 import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.ControllersAfterInitAction
@@ -77,41 +78,33 @@
         context.resources.getDimensionPixelSize(R.dimen.bubblebar_stashed_size) / 2f
 
     private var animator: AnimatorSet? = null
+    private var hotseatVerticalCenter: Int = 0
 
     override var isStashed: Boolean = false
         @VisibleForTesting set
 
-    override var isBubblesShowingOnHome: Boolean = false
-        set(onHome) {
-            if (field == onHome) return
-            field = onHome
-            if (!bubbleBarViewController.hasBubbles()) {
+    override var launcherState: BubbleLauncherState = BubbleLauncherState.IN_APP
+        set(state) {
+            if (field == state) return
+            field = state
+            val hasBubbles = bubbleBarViewController.hasBubbles()
+            bubbleBarViewController.onBubbleBarConfigurationChanged(hasBubbles)
+            if (!hasBubbles) {
                 // if there are no bubbles, there's nothing to show, so just return.
                 return
             }
-            if (onHome) {
-                updateStashedAndExpandedState(stash = false, expand = false)
-                // When transitioning from app to home we need to animate the bubble bar
+            if (field == BubbleLauncherState.HOME) {
+                // When to home we need to animate the bubble bar
                 // here to align with hotseat center.
                 animateBubbleBarYToHotseat()
-            } else if (!bubbleBarViewController.isExpanded) {
-                updateStashedAndExpandedState(stash = true, expand = false)
-            }
-            bubbleBarViewController.onBubbleBarConfigurationChanged(/* animate= */ true)
-        }
-
-    override var isBubblesShowingOnOverview: Boolean = false
-        set(onOverview) {
-            if (field == onOverview) return
-            field = onOverview
-            if (onOverview) {
+            } else if (field == BubbleLauncherState.OVERVIEW) {
                 // When transitioning to overview we need to animate the bubble bar to align with
                 // the taskbar bottom.
                 animateBubbleBarYToTaskbar()
-            } else {
-                updateStashedAndExpandedState(stash = true, expand = false)
             }
-            bubbleBarViewController.onBubbleBarConfigurationChanged(/* animate= */ true)
+            // Only stash if we're in an app, otherwise we're in home or overview where we should
+            // be un-stashed
+            updateStashedAndExpandedState(field == BubbleLauncherState.IN_APP, expand = false)
         }
 
     override var isSysuiLocked: Boolean = false
@@ -127,15 +120,16 @@
 
     override val bubbleBarTranslationYForHotseat: Float
         get() {
-            val hotseatBottomSpace = taskbarHotseatDimensionsProvider.getHotseatBottomSpace()
-            val hotseatCellHeight = taskbarHotseatDimensionsProvider.getHotseatHeight()
-            val bubbleBarHeight: Float = bubbleBarViewController.bubbleBarCollapsedHeight
-            return -hotseatBottomSpace - (hotseatCellHeight - bubbleBarHeight) / 2
+            val bubbleBarHeight = bubbleBarViewController.bubbleBarCollapsedHeight
+            return -hotseatVerticalCenter + bubbleBarHeight / 2
         }
 
     override val bubbleBarTranslationYForTaskbar: Float =
         -taskbarHotseatDimensionsProvider.getTaskbarBottomSpace().toFloat()
 
+    /** Not supported in transient mode */
+    override var inAppDisplayOverrideProgress: Float = 0f
+
     /** Check if we have handle view controller */
     override val hasHandleView: Boolean
         get() = bubbleStashedHandleViewController != null
@@ -178,7 +172,15 @@
             isStashed = true
             stashHandleViewAlpha?.let { animatorSet.playTogether(it.animateToValue(1f)) }
         }
-        animatorSet.updateTouchRegionOnAnimationEnd().setDuration(BAR_STASH_DURATION).start()
+        animatorSet
+            .updateBarVisibility(isStashed)
+            .updateTouchRegionOnAnimationEnd()
+            .setDuration(BAR_STASH_DURATION)
+            .start()
+    }
+
+    override fun setHotseatVerticalCenter(hotseatVerticalCenter: Int) {
+        this.hotseatVerticalCenter = hotseatVerticalCenter
     }
 
     override fun showBubbleBarImmediate() {
@@ -195,6 +197,7 @@
         bubbleBarBackgroundScaleX.updateValue(1f)
         bubbleBarBackgroundScaleY.updateValue(1f)
         isStashed = false
+        bubbleBarViewController.setHiddenForStashed(false)
         onIsStashedChanged()
     }
 
@@ -209,6 +212,7 @@
         bubbleBarBackgroundScaleX.updateValue(getStashScaleX())
         bubbleBarBackgroundScaleY.updateValue(getStashScaleY())
         isStashed = true
+        bubbleBarViewController.setHiddenForStashed(true)
         onIsStashedChanged()
     }
 
@@ -305,7 +309,8 @@
 
         animatorSet.play(
             createBackgroundAlphaAnimator(isStashed).apply {
-                val alphaDuration = if (isStashed) duration else TASKBAR_STASH_ALPHA_DURATION
+                val alphaDuration =
+                    if (isStashed) duration else TRANSIENT_TASKBAR_STASH_ALPHA_DURATION
                 val alphaDelay = if (isStashed) TASKBAR_STASH_ALPHA_START_DELAY else 0L
                 this.duration = max(0L, alphaDuration - alphaDelay)
                 this.startDelay = alphaDelay
@@ -317,7 +322,7 @@
             bubbleBarBubbleAlpha
                 .animateToValue(getBarAlphaStart(isStashed), getBarAlphaEnd(isStashed))
                 .apply {
-                    this.duration = TASKBAR_STASH_ALPHA_DURATION
+                    this.duration = TRANSIENT_TASKBAR_STASH_ALPHA_DURATION
                     this.startDelay = TASKBAR_STASH_ALPHA_START_DELAY
                     this.interpolator = LINEAR
                 }
@@ -489,6 +494,7 @@
             animator?.cancel()
             animator =
                 createStashAnimator(isStashed, BAR_STASH_DURATION).apply {
+                    updateBarVisibility(isStashed)
                     updateTouchRegionOnAnimationEnd()
                     start()
                 }
@@ -503,6 +509,15 @@
         return this
     }
 
+    private fun <T : Animator> T.updateBarVisibility(stashed: Boolean): T {
+        if (stashed) {
+            doOnEnd { bubbleBarViewController.setHiddenForStashed(true) }
+        } else {
+            doOnStart { bubbleBarViewController.setHiddenForStashed(false) }
+        }
+        return this
+    }
+
     private fun Animator.setBubbleBarPivotDuringAnim(pivotX: Float, pivotY: Float): Animator {
         var initialPivotX = Float.NaN
         var initialPivotY = Float.NaN
diff --git a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarAllAppsButtonContainer.kt b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarAllAppsButtonContainer.kt
index e6c0b2f..c5f8aa0 100644
--- a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarAllAppsButtonContainer.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarAllAppsButtonContainer.kt
@@ -37,7 +37,7 @@
 import com.android.launcher3.views.ActivityContext
 import com.android.launcher3.views.IconButtonView
 import com.android.quickstep.DeviceConfigWrapper
-import com.android.quickstep.util.AssistStateManager
+import com.android.quickstep.util.ContextualSearchStateManager
 
 /** Taskbar all apps button container for customizable taskbar. */
 class TaskbarAllAppsButtonContainer
@@ -79,17 +79,18 @@
         setOnClickListener(this::onAllAppsButtonClick)
         setOnLongClickListener(this::onAllAppsButtonLongClick)
         setOnTouchListener(this::onAllAppsButtonTouch)
-        isHapticFeedbackEnabled = taskbarViewCallbacks.isAllAppsButtonHapticFeedbackEnabled()
+        isHapticFeedbackEnabled =
+            taskbarViewCallbacks.isAllAppsButtonHapticFeedbackEnabled(mContext)
         allAppsTouchRunnable = Runnable {
             taskbarViewCallbacks.triggerAllAppsButtonLongClick()
             allAppsTouchTriggered = true
         }
-        val assistStateManager = AssistStateManager.INSTANCE[mContext]
+        val contextualSearchStateManager = ContextualSearchStateManager.INSTANCE[mContext]
         if (
             DeviceConfigWrapper.get().customLpaaThresholds &&
-                assistStateManager.lpnhDurationMillis.isPresent
+                contextualSearchStateManager.lpnhDurationMillis.isPresent
         ) {
-            allAppsButtonTouchDelayMs = assistStateManager.lpnhDurationMillis.get()
+            allAppsButtonTouchDelayMs = contextualSearchStateManager.lpnhDurationMillis.get()
         }
     }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarFeatureEvaluator.kt b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarFeatureEvaluator.kt
index 7739a0e..f130d29 100644
--- a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarFeatureEvaluator.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarFeatureEvaluator.kt
@@ -23,9 +23,7 @@
 
 /** Evaluates all the features taskbar can have. */
 class TaskbarFeatureEvaluator
-private constructor(
-    private val taskbarActivityContext: TaskbarActivityContext,
-) {
+private constructor(private val taskbarActivityContext: TaskbarActivityContext) {
     val hasAllApps = true
     val hasAppIcons = true
     val hasBubbles = false
@@ -43,6 +41,9 @@
     val isLandscape: Boolean
         get() = taskbarActivityContext.deviceProfile.isLandscape
 
+    val supportsPinningPopup: Boolean
+        get() = !hasNavButtons
+
     fun onDestroy() {
         taskbarFeatureEvaluator = null
     }
@@ -51,9 +52,7 @@
         @Volatile private var taskbarFeatureEvaluator: TaskbarFeatureEvaluator? = null
 
         @JvmStatic
-        fun getInstance(
-            taskbarActivityContext: TaskbarActivityContext,
-        ): TaskbarFeatureEvaluator {
+        fun getInstance(taskbarActivityContext: TaskbarActivityContext): TaskbarFeatureEvaluator {
             synchronized(this) {
                 if (taskbarFeatureEvaluator == null) {
                     taskbarFeatureEvaluator = TaskbarFeatureEvaluator(taskbarActivityContext)
diff --git a/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayController.java b/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayController.java
index 7eb34a5..79cb748 100644
--- a/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayController.java
@@ -35,6 +35,7 @@
 
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Flags;
 import com.android.launcher3.taskbar.TaskbarActivityContext;
 import com.android.launcher3.taskbar.TaskbarControllers;
 import com.android.systemui.shared.system.TaskStackChangeListener;
@@ -216,6 +217,13 @@
         @Override
         protected void handleClose(boolean animate) {
             if (!mIsOpen) return;
+            if (Flags.taskbarOverflow()) {
+                // Mark the view closed before attempting to remove it, so the drag layer does not
+                // schedule another call to close. Needed for taskbar overflow in case the KQS
+                // view shown for taskbar overflow needs to be reshown - delayed close call would
+                // would result in reshown KQS view getting hidden.
+                mIsOpen = false;
+            }
             mTaskbarContext.getDragLayer().removeView(this);
             Optional.ofNullable(mOverlayContext).ifPresent(c -> {
                 if (canCloseWindow()) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayDragLayer.java b/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayDragLayer.java
index 773b0b9..669850c 100644
--- a/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayDragLayer.java
+++ b/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayDragLayer.java
@@ -73,7 +73,7 @@
     @Override
     public void recreateControllers() {
         List<TouchController> controllers = new ArrayList<>();
-        controllers.add(mActivity.getDragController());
+        controllers.add(mContainer.getDragController());
         controllers.addAll(mTouchControllers);
         mControllers = controllers.toArray(new TouchController[0]);
     }
@@ -87,7 +87,7 @@
     @Override
     public boolean dispatchKeyEvent(KeyEvent event) {
         if (event.getAction() == ACTION_UP && event.getKeyCode() == KEYCODE_BACK) {
-            AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(mActivity);
+            AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(mContainer);
             if (topView != null && topView.canHandleBack()) {
                 topView.onBackInvoked();
                 return true;
@@ -96,7 +96,7 @@
                 && event.getKeyCode() == KeyEvent.KEYCODE_ESCAPE && event.hasNoModifiers()) {
             // Ignore escape if pressed in conjunction with any modifier keys. Close each
             // floating view one at a time for each key press.
-            AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(mActivity);
+            AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(mContainer);
             if (topView != null) {
                 topView.close(/* animate= */ true);
                 return true;
@@ -107,7 +107,7 @@
 
     @Override
     public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) {
-        if (mActivity.isAnySystemDragInProgress()) {
+        if (mContainer.isAnySystemDragInProgress()) {
             inoutInfo.touchableRegion.setEmpty();
             inoutInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION);
         }
@@ -123,7 +123,7 @@
     @Override
     public void onViewRemoved(View child) {
         super.onViewRemoved(child);
-        mActivity.getOverlayController().maybeCloseWindow();
+        mContainer.getOverlayController().maybeCloseWindow();
     }
 
     /** Adds a {@link TouchController} to this drag layer. */
@@ -147,14 +147,14 @@
      * 2) Sets tappableInsets bottom inset to 0.
      */
     private WindowInsets updateInsetsDueToStashing(WindowInsets oldInsets) {
-        if (!DisplayController.isTransientTaskbar(mActivity)) {
+        if (!DisplayController.isTransientTaskbar(mContainer)) {
             return oldInsets;
         }
         WindowInsets.Builder updatedInsetsBuilder = new WindowInsets.Builder(oldInsets);
 
         Insets oldNavInsets = oldInsets.getInsets(WindowInsets.Type.navigationBars());
         Insets newNavInsets = Insets.of(oldNavInsets.left, oldNavInsets.top, oldNavInsets.right,
-                mActivity.getStashedTaskbarHeight());
+                mContainer.getStashedTaskbarHeight());
         updatedInsetsBuilder.setInsets(WindowInsets.Type.navigationBars(), newNavInsets);
 
         Insets oldTappableInsets = oldInsets.getInsets(WindowInsets.Type.tappableElement());
diff --git a/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java b/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
index 14d391b..721c831 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
@@ -21,6 +21,7 @@
 import static com.android.app.animation.Interpolators.FINAL_FRAME;
 import static com.android.app.animation.Interpolators.INSTANT;
 import static com.android.app.animation.Interpolators.LINEAR;
+import static com.android.launcher3.Flags.enableLargeDesktopWindowingTile;
 import static com.android.launcher3.LauncherState.QUICK_SWITCH_FROM_HOME;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTION_EXIT_HOME;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_FADE;
@@ -31,6 +32,7 @@
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_TRANSLATE_Y;
 import static com.android.launcher3.states.StateAnimationConfig.SKIP_OVERVIEW;
 import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_HORIZONTAL_OFFSET;
+import static com.android.quickstep.views.RecentsView.DESKTOP_CAROUSEL_DETACH_PROGRESS;
 import static com.android.quickstep.views.RecentsView.RECENTS_GRID_PROGRESS;
 import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY;
 import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_TRANSLATION;
@@ -76,6 +78,10 @@
         RECENTS_GRID_PROGRESS.set(mRecentsView,
                 state.displayOverviewTasksAsGrid(mLauncher.getDeviceProfile()) ? 1f : 0f);
         TASK_THUMBNAIL_SPLASH_ALPHA.set(mRecentsView, state.showTaskThumbnailSplash() ? 1f : 0f);
+        if (enableLargeDesktopWindowingTile()) {
+            DESKTOP_CAROUSEL_DETACH_PROGRESS.set(mRecentsView,
+                    state.detachDesktopCarousel() ? 1f : 0f);
+        }
     }
 
     @Override
@@ -86,7 +92,7 @@
         }
         setStateWithAnimationInternal(toState, config, builder);
         builder.addEndListener(success -> {
-            if (!success) {
+            if (!success && !toState.isRecentsViewVisible) {
                 mRecentsView.reset();
             }
         });
@@ -142,6 +148,12 @@
         setter.setFloat(mRecentsView, RECENTS_GRID_PROGRESS,
                 toState.displayOverviewTasksAsGrid(mLauncher.getDeviceProfile()) ? 1f : 0f,
                 getOverviewInterpolator(fromState, toState));
+
+        if (enableLargeDesktopWindowingTile()) {
+            setter.setFloat(mRecentsView, DESKTOP_CAROUSEL_DETACH_PROGRESS,
+                    toState.detachDesktopCarousel() ? 1f : 0f,
+                    getOverviewInterpolator(fromState, toState));
+        }
     }
 
     private Interpolator getOverviewInterpolator(LauncherState fromState, LauncherState toState) {
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index e80e838..34d9a68 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -19,7 +19,7 @@
 import static android.os.Trace.TRACE_TAG_APP;
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_OPTIMIZE_MEASURE;
 import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_FOCUSED;
-import static android.window.flags.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY;
+import static android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY;
 
 import static com.android.app.animation.Interpolators.EMPHASIZED;
 import static com.android.internal.jank.Cuj.CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE;
@@ -37,6 +37,7 @@
 import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.LauncherState.OVERVIEW_MODAL_TASK;
 import static com.android.launcher3.LauncherState.OVERVIEW_SPLIT_SELECT;
+import static com.android.launcher3.Utilities.isRtl;
 import static com.android.launcher3.compat.AccessibilityManagerCompat.sendCustomAccessibilityEvent;
 import static com.android.launcher3.config.FeatureFlags.enableSplitContextually;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_LAUNCH_TAP;
@@ -60,12 +61,12 @@
 import static com.android.launcher3.util.DisplayController.CHANGE_ACTIVE_SCREEN;
 import static com.android.launcher3.util.DisplayController.CHANGE_NAVIGATION_MODE;
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
-import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.QUICK_SWITCH_FROM_HOME_FAILED;
-import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.QUICK_SWITCH_FROM_HOME_FALLBACK;
 import static com.android.quickstep.util.AnimUtils.completeRunnableListCallback;
 import static com.android.quickstep.util.SplitAnimationTimings.TABLET_HOME_TO_SPLIT;
 import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_HOME_KEY;
-import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_50_50;
+import static com.android.wm.shell.Flags.enableBubbleAnything;
+import static com.android.wm.shell.Flags.enableBubbleBarInPersistentTaskBar;
+import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -138,6 +139,7 @@
 import com.android.launcher3.statemanager.StateManager.StateHandler;
 import com.android.launcher3.taskbar.LauncherTaskbarUIController;
 import com.android.launcher3.taskbar.TaskbarManager;
+import com.android.launcher3.taskbar.TaskbarUIController;
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.shared.TestProtocol;
 import com.android.launcher3.uioverrides.QuickstepWidgetHolder.QuickstepHolderFactory;
@@ -172,7 +174,7 @@
 import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.TaskUtils;
 import com.android.quickstep.TouchInteractionService.TISBinder;
-import com.android.quickstep.util.ActiveGestureLog;
+import com.android.quickstep.util.ActiveGestureProtoLogProxy;
 import com.android.quickstep.util.AsyncClockEventDelegate;
 import com.android.quickstep.util.GroupTask;
 import com.android.quickstep.util.LauncherUnfoldAnimationController;
@@ -188,6 +190,7 @@
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.RecentsViewContainer;
 import com.android.quickstep.views.TaskView;
+import com.android.systemui.animation.back.FlingOnBackAnimationCallback;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.unfold.RemoteUnfoldSharedComponent;
@@ -198,6 +201,7 @@
 import com.android.systemui.unfold.dagger.UnfoldMain;
 import com.android.systemui.unfold.progress.RemoteUnfoldTransitionReceiver;
 import com.android.systemui.unfold.updates.RotationChangeProvider;
+import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
 
 import kotlin.Unit;
@@ -239,6 +243,7 @@
     private SplitSelectStateController mSplitSelectStateController;
     private SplitWithKeyboardShortcutController mSplitWithKeyboardShortcutController;
     private SplitToWorkspaceController mSplitToWorkspaceController;
+    private BubbleBarLocation mBubbleBarLocation;
 
     /**
      * If Launcher restarted while in the middle of an Overview split select, it needs this data to
@@ -271,7 +276,7 @@
         RecentsView<?,?> overviewPanel = getOverviewPanel();
         SystemUiProxy systemUiProxy = SystemUiProxy.INSTANCE.get(this);
         mSplitSelectStateController =
-                new SplitSelectStateController(this, mHandler, getStateManager(),
+                new SplitSelectStateController(this, getStateManager(),
                         getDepthController(), getStatsLogManager(),
                         systemUiProxy, RecentsModel.INSTANCE.get(this),
                         () -> onStateBack());
@@ -463,7 +468,7 @@
         if (Flags.enablePrivateSpace()) {
             shortcuts.add(UNINSTALL_APP);
         }
-        if (com.android.wm.shell.Flags.enableBubbleAnything()) {
+        if (enableBubbleAnything()) {
             shortcuts.add(BUBBLE_SHORTCUT);
         }
         return shortcuts.stream();
@@ -595,11 +600,8 @@
                     TaskView taskToLaunch = currentPageTask;
                     if (currentPageTask == null) {
                         taskToLaunch = fallbackTask;
-                        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
-                                "Quick switch from home fallback case: The TaskView at index ")
-                                        .append(rv.getCurrentPage())
-                                        .append(" is missing."),
-                                QUICK_SWITCH_FROM_HOME_FALLBACK);
+                        ActiveGestureProtoLogProxy.logQuickSwitchFromHomeFallback(
+                                rv.getCurrentPage());
                     }
                     taskToLaunch.launchWithoutAnimation(success -> {
                         if (!success) {
@@ -610,11 +612,7 @@
                         return Unit.INSTANCE;
                     });
                 } else {
-                    ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
-                            "Quick switch from home failed: TaskViews at indices ")
-                                    .append(rv.getCurrentPage())
-                                    .append(" and 0 are missing."),
-                            QUICK_SWITCH_FROM_HOME_FAILED);
+                    ActiveGestureProtoLogProxy.logQuickSwitchFromHomeFailed(rv.getCurrentPage());
                     getStateManager().goToState(NORMAL);
                 }
                 break;
@@ -681,10 +679,6 @@
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
-        // Back dispatcher is registered in {@link BaseActivity#onCreate}. For predictive back to
-        // work, we must opt-in BEFORE registering back dispatcher. So we need to call
-        // setEnableOnBackInvokedCallback() before super.onCreate()
-        getApplicationInfo().setEnableOnBackInvokedCallback(true);
         super.onCreate(savedInstanceState);
         if (savedInstanceState != null) {
             mPendingSplitSelectInfo = ObjectWrapper.unwrap(
@@ -907,12 +901,12 @@
     protected void registerBackDispatcher() {
         getOnBackInvokedDispatcher().registerOnBackInvokedCallback(
                 OnBackInvokedDispatcher.PRIORITY_DEFAULT,
-                new OnBackAnimationCallback() {
+                new FlingOnBackAnimationCallback() {
 
                     @Nullable OnBackAnimationCallback mActiveOnBackAnimationCallback;
 
                     @Override
-                    public void onBackStarted(@NonNull BackEvent backEvent) {
+                    public void onBackStartedCompat(@NonNull BackEvent backEvent) {
                         if (mActiveOnBackAnimationCallback != null) {
                             mActiveOnBackAnimationCallback.onBackCancelled();
                         }
@@ -922,7 +916,7 @@
 
                     @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
                     @Override
-                    public void onBackInvoked() {
+                    public void onBackInvokedCompat() {
                         // Recreate mActiveOnBackAnimationCallback if necessary to avoid NPE
                         // because:
                         // 1. b/260636433: In 3-button-navigation mode, onBackStarted() is not
@@ -938,7 +932,7 @@
                     }
 
                     @Override
-                    public void onBackProgressed(@NonNull BackEvent backEvent) {
+                    public void onBackProgressedCompat(@NonNull BackEvent backEvent) {
                         if (!FeatureFlags.IS_STUDIO_BUILD
                                 && mActiveOnBackAnimationCallback == null) {
                             return;
@@ -947,7 +941,7 @@
                     }
 
                     @Override
-                    public void onBackCancelled() {
+                    public void onBackCancelledCompat() {
                         if (!FeatureFlags.IS_STUDIO_BUILD
                                 && mActiveOnBackAnimationCallback == null) {
                             return;
@@ -1097,14 +1091,34 @@
         );
     }
 
-    public void setTaskbarUIController(LauncherTaskbarUIController taskbarUIController) {
-        mTaskbarUIController = taskbarUIController;
+    @Override
+    public void setTaskbarUIController(@Nullable TaskbarUIController taskbarUIController) {
+        mTaskbarUIController = (LauncherTaskbarUIController) taskbarUIController;
     }
 
+    @Override
     public @Nullable LauncherTaskbarUIController getTaskbarUIController() {
         return mTaskbarUIController;
     }
 
+    /** Provides the translation X for the hotseat item. */
+    public int getHotseatItemTranslationX(ItemInfo itemInfo) {
+        int translationX = 0;
+        if (isBubbleBarEnabled()
+                && enableBubbleBarInPersistentTaskBar()
+                && mBubbleBarLocation != null) {
+            boolean isBubblesOnLeft = mBubbleBarLocation.isOnLeft(isRtl(getResources()));
+            translationX += mDeviceProfile
+                    .getHotseatTranslationXForNavBar(this, isBubblesOnLeft);
+        }
+        if (isBubbleBarEnabled()
+                && mDeviceProfile.shouldAdjustHotseatForBubbleBar(getContext(), hasBubbles())) {
+            translationX += (int) mDeviceProfile
+                    .getHotseatAdjustedTranslation(getContext(), itemInfo.cellX);
+        }
+        return translationX;
+    }
+
     public SplitToWorkspaceController getSplitToWorkspaceController() {
         return mSplitToWorkspaceController;
     }
@@ -1300,6 +1314,10 @@
         mTISBindHelper.setPredictiveBackToHomeInProgress(isInProgress);
     }
 
+    public boolean getPredictiveBackToHomeInProgress() {
+        return mIsPredictiveBackToHomeInProgress;
+    }
+
     @Override
     public boolean areDesktopTasksVisible() {
         DesktopVisibilityController desktopVisibilityController = getDesktopVisibilityController();
@@ -1345,7 +1363,7 @@
                 /* callback= */ success -> mSplitSelectStateController.resetState(),
                 /* freezeTaskList= */ false,
                 groupTask.mSplitBounds == null
-                        ? SNAP_TO_50_50
+                        ? SNAP_TO_2_50_50
                         : groupTask.mSplitBounds.snapPosition,
                 remoteTransition);
     }
@@ -1385,6 +1403,7 @@
     }
 
     @NonNull
+    @Override
     public TISBindHelper getTISBindHelper() {
         return mTISBindHelper;
     }
@@ -1410,6 +1429,11 @@
         SystemUiProxy.INSTANCE.get(this).showAppBubble(intent);
     }
 
+    /** Sets the location of the bubble bar */
+    public void setBubbleBarLocation(BubbleBarLocation bubbleBarLocation) {
+        mBubbleBarLocation = bubbleBarLocation;
+    }
+
     private static final class LauncherTaskViewController extends
             TaskViewTouchController<QuickstepLauncher> {
 
diff --git a/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
index 235ec7b..111069f 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
@@ -155,6 +155,9 @@
                 0,
                 timings.getGridSlideSecondaryInterpolator());
 
+        mRecentsView.handleDesktopTaskInSplitSelectState(builder,
+                timings.getDesktopTaskFadeInterpolator());
+
         if (!animate) {
             AnimatorSet as = builder.buildAnim();
             as.start();
diff --git a/quickstep/src/com/android/launcher3/uioverrides/SystemApiWrapper.kt b/quickstep/src/com/android/launcher3/uioverrides/SystemApiWrapper.kt
index 0469636..2946242 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/SystemApiWrapper.kt
+++ b/quickstep/src/com/android/launcher3/uioverrides/SystemApiWrapper.kt
@@ -23,9 +23,11 @@
 import android.content.IIntentSender
 import android.content.Intent
 import android.content.pm.ActivityInfo
+import android.content.pm.ApplicationInfo
 import android.content.pm.LauncherActivityInfo
 import android.content.pm.LauncherApps
 import android.content.pm.ShortcutInfo
+import android.multiuser.Flags.addLauncherUserConfig
 import android.os.Bundle
 import android.os.Flags.allowPrivateProfile
 import android.os.IBinder
@@ -40,15 +42,20 @@
 import com.android.launcher3.Flags.privateSpaceSysAppsSeparation
 import com.android.launcher3.R
 import com.android.launcher3.Utilities
+import com.android.launcher3.dagger.ApplicationContext
+import com.android.launcher3.dagger.LauncherAppSingleton
 import com.android.launcher3.proxy.ProxyActivityStarter
 import com.android.launcher3.util.ApiWrapper
 import com.android.launcher3.util.Executors
 import com.android.launcher3.util.StartActivityParams
 import com.android.launcher3.util.UserIconInfo
 import com.android.quickstep.util.FadeOutRemoteTransition
+import javax.inject.Inject
 
 /** A wrapper for the hidden API calls */
-open class SystemApiWrapper(context: Context?) : ApiWrapper(context) {
+@LauncherAppSingleton
+open class SystemApiWrapper @Inject constructor(@ApplicationContext context: Context?) :
+    ApiWrapper(context) {
 
     override fun getPersons(si: ShortcutInfo) = si.persons ?: Utilities.EMPTY_PERSON_ARRAY
 
@@ -68,16 +75,14 @@
         mContext.getSystemService(UserManager::class.java)!!.userProfiles?.forEach { user ->
             mContext.getSystemService(LauncherApps::class.java)!!.getLauncherUserInfo(user)?.apply {
                 users[user] =
-                    UserIconInfo(
-                        user,
-                        when (userType) {
-                            UserManager.USER_TYPE_PROFILE_MANAGED -> UserIconInfo.TYPE_WORK
-                            UserManager.USER_TYPE_PROFILE_CLONE -> UserIconInfo.TYPE_CLONED
-                            UserManager.USER_TYPE_PROFILE_PRIVATE -> UserIconInfo.TYPE_PRIVATE
-                            else -> UserIconInfo.TYPE_MAIN
-                        },
-                        userSerialNumber.toLong()
-                    )
+                    if (addLauncherUserConfig())
+                        UserIconInfo(
+                            user,
+                            getUserIconType(userType),
+                            userSerialNumber.toLong(),
+                            userConfig,
+                        )
+                    else UserIconInfo(user, getUserIconType(userType), userSerialNumber.toLong())
             }
         }
         return users
@@ -110,7 +115,7 @@
                             )
                             .toBundle()
                     requireActivityResult = false
-                }
+                },
             )
         else super.getAppMarketActivityIntent(packageName, user)
 
@@ -131,7 +136,7 @@
                             )
                             .toBundle()
                     requireActivityResult = false
-                }
+                },
             )
         else null
 
@@ -160,7 +165,7 @@
                             allowlistToken: IBinder?,
                             finishedReceiver: IIntentReceiver?,
                             requiredPermission: String?,
-                            options: Bundle?
+                            options: Bundle?,
                         ) {
                             if (code != -1) {
                                 Executors.MAIN_EXECUTOR.execute {
@@ -168,9 +173,9 @@
                                             context,
                                             context.getString(
                                                 R.string.set_default_home_app,
-                                                context.getString(R.string.derived_app_name)
+                                                context.getString(R.string.derived_app_name),
                                             ),
-                                            Toast.LENGTH_LONG
+                                            Toast.LENGTH_LONG,
                                         )
                                         .show()
                                 }
@@ -183,4 +188,16 @@
             context.startActivity(ProxyActivityStarter.getLaunchIntent(context, params))
         }
     }
+
+    override fun getApplicationInfoHash(appInfo: ApplicationInfo): String =
+        (appInfo.sourceDir?.hashCode() ?: 0).toString() + " " + appInfo.longVersionCode
+
+    fun getUserIconType(userType: String): Int {
+        return when (userType) {
+            UserManager.USER_TYPE_PROFILE_MANAGED -> UserIconInfo.TYPE_WORK
+            UserManager.USER_TYPE_PROFILE_CLONE -> UserIconInfo.TYPE_CLONED
+            UserManager.USER_TYPE_PROFILE_PRIVATE -> UserIconInfo.TYPE_PRIVATE
+            else -> UserIconInfo.TYPE_MAIN
+        }
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt b/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt
index 181cba0..417bb74 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt
+++ b/quickstep/src/com/android/launcher3/uioverrides/flags/DevOptionsUiHelper.kt
@@ -35,9 +35,6 @@
 import android.provider.Settings.Secure
 import android.text.Html
 import android.util.AttributeSet
-import android.util.Base64
-import android.util.Base64.NO_PADDING
-import android.util.Base64.NO_WRAP
 import android.view.inputmethod.EditorInfo
 import android.widget.TextView
 import android.widget.Toast
@@ -57,9 +54,10 @@
 import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET
 import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
 import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_FOLDER
-import com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_KEY
 import com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_LABEL
 import com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_TAG
+import com.android.launcher3.LauncherSettings.Settings.LAYOUT_PROVIDER_KEY
+import com.android.launcher3.LauncherSettings.Settings.createBlobProviderKey
 import com.android.launcher3.R
 import com.android.launcher3.model.data.FolderInfo
 import com.android.launcher3.model.data.ItemInfo
@@ -241,7 +239,7 @@
     private fun DebugInfo<Boolean>.getBoolValue() =
         DeviceConfigHelper.prefs.getBoolean(
             this.key,
-            DeviceConfig.getBoolean(NAMESPACE_LAUNCHER, this.key, this.valueInCode)
+            DeviceConfig.getBoolean(NAMESPACE_LAUNCHER, this.key, this.valueInCode),
         )
 
     private fun DebugInfo<Int>.getIntValueAsString() =
@@ -265,7 +263,7 @@
         val pluginPermissionApps =
             pm.getPackagesHoldingPermissions(
                     arrayOf(PLUGIN_PERMISSION),
-                    PackageManager.MATCH_DISABLED_COMPONENTS
+                    PackageManager.MATCH_DISABLED_COMPONENTS,
                 )
                 .map { it.packageName }
 
@@ -274,7 +272,7 @@
                 pm.queryIntentServices(
                         Intent(action),
                         PackageManager.MATCH_DISABLED_COMPONENTS or
-                            PackageManager.GET_RESOLVED_FILTER
+                            PackageManager.GET_RESOLVED_FILTER,
                     )
                     .filter { pluginPermissionApps.contains(it.serviceInfo.packageName) }
             }
@@ -316,7 +314,7 @@
                             infoList.forEach {
                                 manager.pluginEnabler.setDisabled(
                                     it.serviceInfo.componentName,
-                                    disabledState
+                                    disabledState,
                                 )
                             }
                             manager.notifyChange(Intent(Intent.ACTION_PACKAGE_CHANGED, pluginUri))
@@ -387,12 +385,12 @@
             addOnboardPref(
                 "All Apps Bounce",
                 HOME_BOUNCE_SEEN.sharedPrefKey,
-                HOME_BOUNCE_COUNT.sharedPrefKey
+                HOME_BOUNCE_COUNT.sharedPrefKey,
             )
             addOnboardPref(
                 "Hybrid Hotseat Education",
                 HOTSEAT_DISCOVERY_TIP_COUNT.sharedPrefKey,
-                HOTSEAT_LONGPRESS_TIP_SEEN.sharedPrefKey
+                HOTSEAT_LONGPRESS_TIP_SEEN.sharedPrefKey,
             )
             addOnboardPref("Taskbar Education", TASKBAR_EDU_TOOLTIP_STEP.sharedPrefKey)
             addOnboardPref("Taskbar Search Education", TASKBAR_SEARCH_EDU_SEEN.sharedPrefKey)
@@ -470,13 +468,16 @@
                         session.allowPublicAccess()
 
                         session.commit(ORDERED_BG_EXECUTOR) {
-                            val key = Base64.encodeToString(digest, NO_WRAP or NO_PADDING)
-                            Secure.putString(resolver, LAYOUT_DIGEST_KEY, key)
+                            Secure.putString(
+                                resolver,
+                                LAYOUT_PROVIDER_KEY,
+                                createBlobProviderKey(digest),
+                            )
 
                             MODEL_EXECUTOR.submit { model.modelDbController.createEmptyDB() }.get()
                             MAIN_EXECUTOR.submit { model.forceReload() }.get()
                             MODEL_EXECUTOR.submit {}.get()
-                            Secure.putString(resolver, LAYOUT_DIGEST_KEY, null)
+                            Secure.putString(resolver, LAYOUT_PROVIDER_KEY, null)
                         }
                     }
                 }
@@ -512,7 +513,7 @@
                     info.providerName.className,
                     info.spanX,
                     info.spanY,
-                    userType
+                    userType,
                 )
         }
     }
@@ -520,7 +521,7 @@
     private fun createUriPickerIntent(
         action: String,
         executor: Executor,
-        callback: (uri: Uri) -> Unit
+        callback: (uri: Uri) -> Unit,
     ): Intent {
         val pendingIntent =
             PendingIntent(
@@ -532,7 +533,7 @@
                         allowlistToken: IBinder?,
                         finishedReceiver: IIntentReceiver?,
                         requiredPermission: String?,
-                        options: Bundle?
+                        options: Bundle?,
                     ) {
                         intent.data?.let { uri -> executor.execute { callback(uri) } }
                     }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginManagerWrapperImpl.java b/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginManagerWrapperImpl.java
index 74572c4..3aa1963 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginManagerWrapperImpl.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginManagerWrapperImpl.java
@@ -27,6 +27,8 @@
 import android.content.pm.ResolveInfo;
 
 import com.android.launcher3.BuildConfig;
+import com.android.launcher3.dagger.ApplicationContext;
+import com.android.launcher3.dagger.LauncherAppSingleton;
 import com.android.launcher3.util.PluginManagerWrapper;
 import com.android.systemui.plugins.Plugin;
 import com.android.systemui.plugins.PluginListener;
@@ -34,7 +36,6 @@
 import com.android.systemui.shared.plugins.PluginInstance;
 import com.android.systemui.shared.plugins.PluginManagerImpl;
 import com.android.systemui.shared.plugins.PluginPrefs;
-import com.android.systemui.shared.system.UncaughtExceptionPreHandlerManager;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -42,16 +43,17 @@
 import java.util.List;
 import java.util.Set;
 
-public class PluginManagerWrapperImpl extends PluginManagerWrapper {
+import javax.inject.Inject;
 
-    private static final UncaughtExceptionPreHandlerManager UNCAUGHT_EXCEPTION_PRE_HANDLER_MANAGER =
-            new UncaughtExceptionPreHandlerManager();
+@LauncherAppSingleton
+public class PluginManagerWrapperImpl extends PluginManagerWrapper {
 
     private final Context mContext;
     private final PluginManagerImpl mPluginManager;
     private final PluginEnablerImpl mPluginEnabler;
 
-    public PluginManagerWrapperImpl(Context c) {
+    @Inject
+    public PluginManagerWrapperImpl(@ApplicationContext Context c) {
         mContext = c;
         mPluginEnabler = new PluginEnablerImpl(c);
         List<String> privilegedPlugins = Collections.emptyList();
@@ -64,9 +66,11 @@
                 c.getSystemService(NotificationManager.class), mPluginEnabler,
                 privilegedPlugins, instanceFactory);
 
+        // Use null preHandlerManager, as the handler is never unregistered which can cause leaks
+        // when using multiple dagger graphs.
         mPluginManager = new PluginManagerImpl(c, instanceManagerFactory,
                 BuildConfig.IS_DEBUG_DEVICE,
-                UNCAUGHT_EXCEPTION_PRE_HANDLER_MANAGER, mPluginEnabler,
+                null /* preHandlerManager */, mPluginEnabler,
                 new PluginPrefs(c), privilegedPlugins);
     }
 
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java b/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
index 030a7ac..d387794 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
@@ -118,7 +118,7 @@
 
     @Override
     public ScaleAndTranslation getHotseatScaleAndTranslation(Launcher launcher) {
-        if (launcher.getDeviceProfile().isTablet) {
+        if (launcher.getDeviceProfile().shouldShowAllAppsOnSheet()) {
             return getWorkspaceScaleAndTranslation(launcher);
         } else {
             ScaleAndTranslation overviewScaleAndTranslation = LauncherState.OVERVIEW
@@ -133,7 +133,7 @@
     @Override
     protected <DEVICE_PROFILE_CONTEXT extends Context & ActivityContext>
             float getDepthUnchecked(DEVICE_PROFILE_CONTEXT context) {
-        if (context.getDeviceProfile().isTablet) {
+        if (context.getDeviceProfile().shouldShowAllAppsOnSheet()) {
             return context.getDeviceProfile().bottomSheetDepth;
         } else {
             // The scrim fades in at approximately 50% of the swipe gesture.
@@ -154,7 +154,7 @@
         return new PageAlphaProvider(DECELERATE_2) {
             @Override
             public float getPageAlpha(int pageIndex) {
-                return launcher.getDeviceProfile().isTablet
+                return launcher.getDeviceProfile().shouldShowAllAppsOnSheet()
                         ? superPageAlphaProvider.getPageAlpha(pageIndex)
                         : 0;
             }
@@ -164,8 +164,8 @@
     @Override
     public int getVisibleElements(Launcher launcher) {
         int elements = ALL_APPS_CONTENT | FLOATING_SEARCH_BAR;
-        // Only add HOTSEAT_ICONS for tablets in ALL_APPS state.
-        if (launcher.getDeviceProfile().isTablet) {
+        // When All Apps is presented on a bottom sheet, HOTSEAT_ICONS are visible.
+        if (launcher.getDeviceProfile().shouldShowAllAppsOnSheet()) {
             elements |= HOTSEAT_ICONS;
         }
         return elements;
@@ -202,7 +202,7 @@
 
     @Override
     public int getWorkspaceScrimColor(Launcher launcher) {
-        return launcher.getDeviceProfile().isTablet
+        return launcher.getDeviceProfile().shouldShowAllAppsOnSheet()
                 ? launcher.getResources().getColor(R.color.widgets_picker_scrim)
                 : Themes.getAttrColor(launcher, R.attr.allAppsScrimColor);
     }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java b/quickstep/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
index 18d717f..ca388c6 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
@@ -15,6 +15,7 @@
  */
 package com.android.launcher3.uioverrides.states;
 
+import static com.android.launcher3.Flags.enableDesktopWindowingCarouselDetach;
 import static com.android.launcher3.Flags.enableScalingRevealHomeAnimation;
 import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_BACKGROUND;
 
@@ -90,6 +91,11 @@
     }
 
     @Override
+    public boolean detachDesktopCarousel() {
+        return enableDesktopWindowingCarouselDetach();
+    }
+
+    @Override
     protected float getDepthUnchecked(Context context) {
         if (Launcher.getLauncher(context).areDesktopTasksVisible()) {
             // Don't blur the background while desktop tasks are visible
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
index b165cdd..c48ba4f 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
@@ -166,6 +166,11 @@
     }
 
     @Override
+    public boolean detachDesktopCarousel() {
+        return false;
+    }
+
+    @Override
     public boolean disallowTaskbarGlobalDrag() {
         // Disable global drag in overview
         return true;
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index a6d651c..31aa489 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -45,6 +45,7 @@
 import static com.android.launcher3.util.SystemUiController.UI_STATE_FULLSCREEN_TASK;
 import static com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC;
 import static com.android.launcher3.util.window.RefreshRateTracker.getSingleFrameMs;
+import static com.android.quickstep.BaseContainerInterface.AnimationFactory;
 import static com.android.quickstep.GestureState.GestureEndTarget.HOME;
 import static com.android.quickstep.GestureState.GestureEndTarget.LAST_TASK;
 import static com.android.quickstep.GestureState.GestureEndTarget.NEW_TASK;
@@ -55,11 +56,7 @@
 import static com.android.quickstep.GestureState.STATE_RECENTS_ANIMATION_STARTED;
 import static com.android.quickstep.GestureState.STATE_RECENTS_SCROLLING_FINISHED;
 import static com.android.quickstep.MultiStateCallback.DEBUG_STATES;
-import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.CANCEL_RECENTS_ANIMATION;
 import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.EXPECTING_TASK_APPEARED;
-import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.INVALID_VELOCITY_ON_SWIPE_UP;
-import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.LAUNCHER_DESTROYED;
-import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.ON_SETTLED_ON_END_TARGET;
 import static com.android.quickstep.views.RecentsView.UPDATE_SYSUI_FLAGS_THRESHOLD;
 import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
 
@@ -91,8 +88,8 @@
 import android.view.WindowInsets;
 import android.view.animation.Interpolator;
 import android.widget.Toast;
+import android.window.DesktopModeFlags;
 import android.window.PictureInPictureSurfaceTransaction;
-import android.window.flags.DesktopModeFlags;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -112,6 +109,7 @@
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.logging.StatsLogManager.StatsLogger;
 import com.android.launcher3.statemanager.BaseState;
+import com.android.launcher3.statemanager.StatefulContainer;
 import com.android.launcher3.taskbar.TaskbarThresholdUtils;
 import com.android.launcher3.taskbar.TaskbarUIController;
 import com.android.launcher3.uioverrides.QuickstepLauncher;
@@ -120,13 +118,14 @@
 import com.android.launcher3.util.TraceHelper;
 import com.android.launcher3.util.VibratorWrapper;
 import com.android.launcher3.util.WindowBounds;
-import com.android.quickstep.BaseActivityInterface.AnimationFactory;
 import com.android.quickstep.GestureState.GestureEndTarget;
 import com.android.quickstep.RemoteTargetGluer.RemoteTargetHandle;
+import com.android.quickstep.fallback.window.RecentsWindowManager;
 import com.android.quickstep.util.ActiveGestureErrorDetector;
 import com.android.quickstep.util.ActiveGestureLog;
-import com.android.quickstep.util.ActivityInitListener;
+import com.android.quickstep.util.ActiveGestureProtoLogProxy;
 import com.android.quickstep.util.AnimatorControllerWithResistance;
+import com.android.quickstep.util.ContextInitListener;
 import com.android.quickstep.util.InputConsumerProxy;
 import com.android.quickstep.util.InputProxyHandlerFactory;
 import com.android.quickstep.util.MotionPauseDetector;
@@ -156,8 +155,6 @@
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
 import com.android.wm.shell.shared.startingsurface.SplashScreenExitAnimationUtils;
 
-import kotlin.Unit;
-
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
@@ -167,15 +164,16 @@
 import java.util.OptionalInt;
 import java.util.function.Consumer;
 
+import kotlin.Unit;
+
 /**
  * Handles the navigation gestures when Launcher is the default home activity.
  */
 public abstract class AbsSwipeUpHandler<
-        RECENTS_CONTAINER extends Context & RecentsViewContainer,
-        RECENTS_VIEW extends RecentsView<RECENTS_CONTAINER, STATE>,
-        STATE extends BaseState<STATE>>
-        extends SwipeUpAnimationLogic
-        implements OnApplyWindowInsetsListener, RecentsAnimationCallbacks.RecentsAnimationListener {
+        RECENTS_CONTAINER extends Context & RecentsViewContainer & StatefulContainer<STATE>,
+        RECENTS_VIEW extends RecentsView<RECENTS_CONTAINER, STATE>, STATE extends BaseState<STATE>>
+        extends SwipeUpAnimationLogic implements OnApplyWindowInsetsListener,
+        RecentsAnimationCallbacks.RecentsAnimationListener {
     private static final String TAG = "AbsSwipeUpHandler";
 
     private static final ArrayList<String> STATE_NAMES = new ArrayList<>();
@@ -185,7 +183,7 @@
 
     protected final BaseContainerInterface<STATE, RECENTS_CONTAINER> mContainerInterface;
     protected final InputConsumerProxy mInputConsumerProxy;
-    protected final ActivityInitListener mActivityInitListener;
+    protected final ContextInitListener mContextInitListener;
     // Callbacks to be made once the recents animation starts
     private final ArrayList<Runnable> mRecentsAnimationStartCallbacks = new ArrayList<>();
     private final OnScrollChangedListener mOnRecentsScrollListener = this::onRecentsViewScroll;
@@ -201,7 +199,7 @@
     private boolean mRecentsViewScrollLinked = false;
 
     private final Runnable mLauncherOnDestroyCallback = () -> {
-        ActiveGestureLog.INSTANCE.addLog("Launcher destroyed", LAUNCHER_DESTROYED);
+        ActiveGestureProtoLogProxy.logLauncherDestroyed();
         mRecentsView = null;
         mContainer = null;
         mStateCallback.clearState(STATE_LAUNCHER_PRESENT);
@@ -238,6 +236,8 @@
             getNextStateFlag("STATE_SCALED_CONTROLLER_HOME");
     private static final int STATE_SCALED_CONTROLLER_RECENTS =
             getNextStateFlag("STATE_SCALED_CONTROLLER_RECENTS");
+    private static final int STATE_PARALLEL_ANIM_FINISHED =
+            getNextStateFlag("STATE_PARALLEL_ANIM_FINISHED");
 
     protected static final int STATE_HANDLER_INVALIDATED =
             getNextStateFlag("STATE_HANDLER_INVALIDATED");
@@ -292,7 +292,7 @@
     private static final int LOG_NO_OP_PAGE_INDEX = -1;
 
     protected final TaskAnimationManager mTaskAnimationManager;
-
+    protected final RecentsWindowManager mRecentsWindowManager;
     // Either RectFSpringAnim (if animating home) or ObjectAnimator (from mCurrentShift) otherwise
     private RunningWindowAnim[] mRunningWindowAnim;
     // Possible second animation running at the same time as mRunningWindowAnim
@@ -353,10 +353,10 @@
     public AbsSwipeUpHandler(Context context, RecentsAnimationDeviceState deviceState,
             TaskAnimationManager taskAnimationManager, GestureState gestureState,
             long touchTimeMs, boolean continuingLastGesture,
-            InputConsumerController inputConsumer) {
+            InputConsumerController inputConsumer, RecentsWindowManager recentsWindowManager) {
         super(context, deviceState, gestureState);
         mContainerInterface = gestureState.getContainerInterface();
-        mActivityInitListener =
+        mContextInitListener =
                 mContainerInterface.createActivityInitListener(this::onActivityInit);
         mInputConsumerProxy =
                 new InputConsumerProxy(context, /* rotationSupplier = */ () -> {
@@ -369,6 +369,7 @@
                     endLauncherTransitionController();
                 }, new InputProxyHandlerFactory(mContainerInterface, mGestureState));
         mTaskAnimationManager = taskAnimationManager;
+        mRecentsWindowManager = recentsWindowManager;
         mTouchTimeMs = touchTimeMs;
         mContinuingLastGesture = continuingLastGesture;
 
@@ -450,7 +451,8 @@
         mStateCallback.runOnceAtState(STATE_SCREENSHOT_CAPTURED | STATE_GESTURE_COMPLETED
                         | STATE_SCALED_CONTROLLER_HOME,
                 this::finishCurrentTransitionToHome);
-        mStateCallback.runOnceAtState(STATE_SCALED_CONTROLLER_HOME | STATE_CURRENT_TASK_FINISHED,
+        mStateCallback.runOnceAtState(STATE_SCALED_CONTROLLER_HOME | STATE_CURRENT_TASK_FINISHED
+                        | STATE_PARALLEL_ANIM_FINISHED,
                 this::reset);
 
         mStateCallback.runOnceAtState(STATE_LAUNCHER_PRESENT | STATE_APP_CONTROLLER_RECEIVED
@@ -476,7 +478,7 @@
                 this::resetStateForAnimationCancel);
     }
 
-    protected boolean onActivityInit(Boolean alreadyOnHome) {
+    protected boolean onActivityInit(Boolean isHomeStarted) {
         if (mStateCallback.hasStates(STATE_HANDLER_INVALIDATED)) {
             return false;
         }
@@ -504,11 +506,11 @@
             initStateCallbacks();
             mStateCallback.setState(oldState);
         }
-        mWasLauncherAlreadyVisible = alreadyOnHome;
+        mWasLauncherAlreadyVisible = isHomeStarted;
         mContainer = container;
         // Override the visibility of the activity until the gesture actually starts and we swipe
         // up, or until we transition home and the home animation is composed
-        if (alreadyOnHome) {
+        if (isHomeStarted) {
             mContainer.clearForceInvisibleFlag(STATE_HANDLER_INVISIBILITY_FLAGS);
         } else {
             mContainer.addForceInvisibleFlag(STATE_HANDLER_INVISIBILITY_FLAGS);
@@ -518,7 +520,7 @@
         mRecentsView.setOnPageTransitionEndCallback(null);
 
         mStateCallback.setState(STATE_LAUNCHER_PRESENT);
-        if (alreadyOnHome) {
+        if (isHomeStarted) {
             onLauncherStart();
         } else {
             container.addEventCallback(EVENT_STARTED, mLauncherOnStartCallback);
@@ -665,7 +667,7 @@
         TopTaskTracker.CachedTaskInfo cachedTaskInfo = mGestureState.getRunningTask();
         if (mIsSwipeForSplit) {
             int[] splitTaskIds = TopTaskTracker.INSTANCE.get(mContext).getRunningSplitTaskIds();
-            runningTasks = cachedTaskInfo.getPlaceholderTasks(splitTaskIds);
+            runningTasks = cachedTaskInfo.getSplitPlaceholderTasks(splitTaskIds);
         } else {
             runningTasks = cachedTaskInfo.getPlaceholderTasks();
         }
@@ -819,7 +821,7 @@
             return;
         }
         initTransitionEndpoints(mContainer.getDeviceProfile());
-        mAnimationFactory.createActivityInterface(mTransitionDragLength);
+        mAnimationFactory.createContainerInterface(mTransitionDragLength);
     }
 
     /**
@@ -863,10 +865,13 @@
         }
     }
 
+    public Intent getHomeIntent() {
+        return mGestureState.getHomeIntent();
+    }
+
     public Intent getLaunchIntent() {
         return mGestureState.getOverviewIntent();
     }
-
     /**
      * Called when the value of {@link #mCurrentShift} changes
      */
@@ -975,10 +980,8 @@
 
     @Override
     public void onRecentsAnimationCanceled(HashMap<Integer, ThumbnailData> thumbnailDatas) {
-        ActiveGestureLog.INSTANCE.addLog(
-                /* event= */ "cancelRecentsAnimation",
-                /* gestureEvent= */ CANCEL_RECENTS_ANIMATION);
-        mActivityInitListener.unregister("AbsSwipeUpHandler.onRecentsAnimationCanceled");
+        ActiveGestureProtoLogProxy.logAbsSwipeUpHandlerOnRecentsAnimationCanceled();
+        mContextInitListener.unregister("AbsSwipeUpHandler.onRecentsAnimationCanceled");
         mStateCallback.setStateOnUiThread(STATE_GESTURE_CANCELLED | STATE_HANDLER_INVALIDATED);
         // Defer clearing the controller and the targets until after we've updated the state
         mRecentsAnimationController = null;
@@ -1145,12 +1148,18 @@
 
         if (endTarget != NEW_TASK) {
             InteractionJankMonitorWrapper.cancel(Cuj.CUJ_LAUNCHER_QUICK_SWITCH);
+        } else {
+            InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_QUICK_SWITCH);
         }
         if (endTarget != HOME) {
             InteractionJankMonitorWrapper.cancel(Cuj.CUJ_LAUNCHER_APP_CLOSE_TO_HOME);
+        } else {
+            InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_APP_CLOSE_TO_HOME);
         }
         if (endTarget != RECENTS) {
             InteractionJankMonitorWrapper.cancel(Cuj.CUJ_LAUNCHER_APP_SWIPE_TO_RECENTS);
+        } else {
+            InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_APP_SWIPE_TO_RECENTS);
         }
 
         switch (endTarget) {
@@ -1181,10 +1190,7 @@
             // Resets this value as the gesture is now complete.
             mContainerInterface.getTaskbarController().setUserIsNotGoingHome(false);
         }
-        ActiveGestureLog.INSTANCE.addLog(
-                new ActiveGestureLog.CompoundString("onSettledOnEndTarget ")
-                        .append(endTarget.name()),
-                /* gestureEvent= */ ON_SETTLED_ON_END_TARGET);
+        ActiveGestureProtoLogProxy.logOnSettledOnEndTarget(endTarget.name());
     }
 
     /** @return Whether this was the task we were waiting to appear, and thus handled it. */
@@ -1200,11 +1206,9 @@
                 failureReason.append("STATE_START_NEW_TASK was never set");
             } else {
                 TaskInfo taskInfo = appearedTaskTargets[0].taskInfo;
-                failureReason.append("Unexpected task appeared")
-                                .append(" id=")
-                                .append(taskInfo.taskId)
-                                .append(" pkg=")
-                                .append(taskInfo.baseIntent.getComponent().getPackageName());
+                failureReason.append("Unexpected task appeared id=%d, pkg=%s",
+                        taskInfo.taskId,
+                        taskInfo.baseIntent.getComponent().getPackageName());
             }
             return false;
         }
@@ -1218,19 +1222,10 @@
 
     private GestureEndTarget calculateEndTarget(
             PointF velocityPxPerMs, float endVelocityPxPerMs, boolean isFlingY, boolean isCancel) {
-
-        ActiveGestureErrorDetector.GestureEvent gestureEvent =
-                velocityPxPerMs.x == 0 && velocityPxPerMs.y == 0
-                        ? INVALID_VELOCITY_ON_SWIPE_UP
-                        : null;
-        ActiveGestureLog.INSTANCE.addLog(
-                new ActiveGestureLog.CompoundString("calculateEndTarget: velocities=(x=")
-                        .append(dpiFromPx(velocityPxPerMs.x))
-                        .append("dp/ms, y=")
-                        .append(dpiFromPx(velocityPxPerMs.y))
-                        .append("dp/ms), angle=")
-                        .append(Math.toDegrees(Math.atan2(
-                                -velocityPxPerMs.y, velocityPxPerMs.x))), gestureEvent);
+        ActiveGestureProtoLogProxy.logOnCalculateEndTarget(
+                dpiFromPx(velocityPxPerMs.x),
+                dpiFromPx(velocityPxPerMs.y),
+                Math.toDegrees(Math.atan2(-velocityPxPerMs.y, velocityPxPerMs.x)));
 
         if (mGestureState.isHandlingAtomicEvent()) {
             // Button mode, this is only used to go to recents.
@@ -1548,9 +1543,12 @@
                     @Override
                     public void onAnimationEnd(Animator animation) {
                         mParallelRunningAnim = null;
+                        mStateCallback.setStateOnUiThread(STATE_PARALLEL_ANIM_FINISHED);
                     }
                 });
                 mParallelRunningAnim.start();
+            } else {
+                mStateCallback.setStateOnUiThread(STATE_PARALLEL_ANIM_FINISHED);
             }
         }
 
@@ -1981,15 +1979,13 @@
      * handler (in case of quick switch).
      */
     private void cancelCurrentAnimation() {
-        ActiveGestureLog.INSTANCE.addLog(
-                "AbsSwipeUpHandler.cancelCurrentAnimation",
-                ActiveGestureErrorDetector.GestureEvent.CANCEL_CURRENT_ANIMATION);
+        ActiveGestureProtoLogProxy.logAbsSwipeUpHandlerCancelCurrentAnimation();
         mCanceled = true;
         mCurrentShift.cancelAnimation();
 
         // Cleanup when switching handlers
         mInputConsumerProxy.unregisterOnTouchDownCallback();
-        mActivityInitListener.unregister("AbsSwipeUpHandler.cancelCurrentAnimation");
+        mContextInitListener.unregister("AbsSwipeUpHandler.cancelCurrentAnimation");
         TaskStackChangeListeners.getInstance().unregisterTaskStackListener(
                 mActivityRestartListener);
         mTaskSnapshotCache.clear();
@@ -2007,7 +2003,7 @@
             mGestureEndCallback.run();
         }
 
-        mActivityInitListener.unregister("AbsSwipeUpHandler.invalidateHandler");
+        mContextInitListener.unregister("AbsSwipeUpHandler.invalidateHandler");
         TaskStackChangeListeners.getInstance().unregisterTaskStackListener(
                 mActivityRestartListener);
         mTaskSnapshotCache.clear();
@@ -2284,18 +2280,15 @@
             TaskView nextTask = mRecentsView == null ? null : mRecentsView.getNextPageTaskView();
             if (nextTask != null) {
                 int[] taskIds = nextTask.getTaskIds();
-                ActiveGestureLog.CompoundString nextTaskLog = new ActiveGestureLog.CompoundString(
-                        "Launching task: ");
+                ActiveGestureLog.CompoundString nextTaskLog =
+                        ActiveGestureLog.CompoundString.newEmptyString();
                 for (TaskContainer container : nextTask.getTaskContainers()) {
                     if (container == null) {
                         continue;
                     }
-                    nextTaskLog
-                            .append("[id: ")
-                            .append(container.getTask().key.id)
-                            .append(", pkg: ")
-                            .append(container.getTask().key.getPackageName())
-                            .append("] | ");
+                    nextTaskLog.append("[id: %d, pkg: %s] | ",
+                            container.getTask().key.id,
+                            container.getTask().key.getPackageName());
                 }
                 mGestureState.updateLastStartedTaskIds(taskIds);
                 boolean hasTaskPreviouslyAppeared = Arrays.stream(taskIds).anyMatch(
@@ -2304,7 +2297,7 @@
                 if (!hasTaskPreviouslyAppeared) {
                     ActiveGestureLog.INSTANCE.trackEvent(EXPECTING_TASK_APPEARED);
                 }
-                ActiveGestureLog.INSTANCE.addLog(nextTaskLog);
+                ActiveGestureProtoLogProxy.logStartNewTask(nextTaskLog);
                 nextTask.launchWithoutAnimation(true, success -> {
                     resultCallback.accept(success);
                     if (success) {
@@ -2382,31 +2375,28 @@
             return;
         }
         final Runnable onFinishComplete = () -> {
-            ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
-                    "AbsSwipeUpHandler.onTasksAppeared: ")
-                    .append("force finish recents animation complete; clearing state callback."));
+            ActiveGestureProtoLogProxy.logAbsSwipeUpHandlerOnTasksAppeared();
             mStateCallback.setStateOnUiThread(STATE_GESTURE_CANCELLED | STATE_HANDLER_INVALIDATED);
         };
-        ActiveGestureLog.CompoundString forceFinishReason = new ActiveGestureLog.CompoundString(
-                "Forcefully finishing recents animation: ");
+        ActiveGestureLog.CompoundString forceFinishReason =
+                ActiveGestureLog.CompoundString.newEmptyString();
         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(forceFinishReason
-                            .append("Unexpected task appeared id=")
-                            .append(taskInfo.taskId)
-                            .append(" pkg=")
-                            .append(taskInfo.baseIntent.getComponent().getPackageName()));
+            ActiveGestureProtoLogProxy.logUnexpectedTaskAppeared(
+                    taskInfo.taskId,
+                    taskInfo.baseIntent.getComponent().getPackageName());
             finishRecentsAnimationOnTasksAppeared(onFinishComplete);
             return;
         }
         ActiveGestureLog.CompoundString handleTaskFailureReason =
-                new ActiveGestureLog.CompoundString("handleTaskAppeared check failed: ");
+                ActiveGestureLog.CompoundString.newEmptyString();
         if (!handleTaskAppeared(appearedTaskTargets, handleTaskFailureReason)) {
-            ActiveGestureLog.INSTANCE.addLog(forceFinishReason.append(handleTaskFailureReason));
+            forceFinishReason.append(handleTaskFailureReason);
+            ActiveGestureProtoLogProxy.logHandleTaskAppearedFailed(forceFinishReason);
             finishRecentsAnimationOnTasksAppeared(onFinishComplete);
             return;
         }
@@ -2414,8 +2404,8 @@
                 .filter(mGestureState.mLastStartedTaskIdPredicate)
                 .toArray(RemoteAnimationTarget[]::new);
         if (taskTargets.length == 0) {
-            ActiveGestureLog.INSTANCE.addLog(
-                    forceFinishReason.append("No appeared task matching started task id"));
+            forceFinishReason.append("No appeared task matching started task id");
+            ActiveGestureProtoLogProxy.logHandleTaskAppearedFailed(forceFinishReason);
             finishRecentsAnimationOnTasksAppeared(onFinishComplete);
             return;
         }
@@ -2424,12 +2414,14 @@
                 ? null : mRecentsView.getTaskViewByTaskId(taskTarget.taskId);
         if (taskView == null || taskView.getTaskContainers().stream().noneMatch(
                 TaskContainer::getShouldShowSplashView)) {
-            ActiveGestureLog.INSTANCE.addLog(forceFinishReason.append("Splash not needed"));
+            forceFinishReason.append("Splash not needed");
+            ActiveGestureProtoLogProxy.logHandleTaskAppearedFailed(forceFinishReason);
             finishRecentsAnimationOnTasksAppeared(onFinishComplete);
             return;
         }
         if (mContainer == null) {
-            ActiveGestureLog.INSTANCE.addLog(forceFinishReason.append("Activity destroyed"));
+            forceFinishReason.append("Activity destroyed");
+            ActiveGestureProtoLogProxy.logHandleTaskAppearedFailed(forceFinishReason);
             finishRecentsAnimationOnTasksAppeared(onFinishComplete);
             return;
         }
@@ -2485,7 +2477,7 @@
         if (mRecentsAnimationController != null) {
             mRecentsAnimationController.finish(false /* toRecents */, onFinishComplete);
         }
-        ActiveGestureLog.INSTANCE.addLog("finishRecentsAnimationOnTasksAppeared");
+        ActiveGestureProtoLogProxy.logFinishRecentsAnimationOnTasksAppeared();
     }
 
     /**
@@ -2520,7 +2512,7 @@
         // Preload the plan
         RecentsModel.INSTANCE.get(mContext).getTasks(null);
 
-        mActivityInitListener.register(reasonString);
+        mContextInitListener.register(reasonString);
     }
 
     private boolean shouldFadeOutTargetsForKeyboardQuickSwitch(
diff --git a/quickstep/src/com/android/quickstep/BaseActivityInterface.java b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
index 8703843..1e755eb 100644
--- a/quickstep/src/com/android/quickstep/BaseActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
@@ -36,7 +36,6 @@
 import android.view.MotionEvent;
 
 import androidx.annotation.Nullable;
-import androidx.annotation.UiThread;
 
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.PendingAnimation;
@@ -62,17 +61,14 @@
 public abstract class BaseActivityInterface<STATE_TYPE extends BaseState<STATE_TYPE>,
         ACTIVITY_TYPE extends StatefulActivity<STATE_TYPE> & RecentsViewContainer> extends
         BaseContainerInterface<STATE_TYPE, ACTIVITY_TYPE> {
-    private final STATE_TYPE mBackgroundState;
 
     private STATE_TYPE mTargetState;
 
-    @Nullable private Runnable mOnInitBackgroundStateUICallback = null;
-
     protected BaseActivityInterface(boolean rotationSupportedByActivity,
             STATE_TYPE overviewState, STATE_TYPE backgroundState) {
+        super(backgroundState);
         this.rotationSupportedByActivity = rotationSupportedByActivity;
         mTargetState = overviewState;
-        mBackgroundState = backgroundState;
     }
 
     /**
@@ -124,13 +120,6 @@
         return activity != null && activity.isStarted();
     }
 
-    @UiThread
-    @Nullable
-    public abstract <T extends RecentsView> T getVisibleRecentsView();
-
-    @UiThread
-    public abstract boolean switchToRecentsIfVisible(Animator.AnimatorListener animatorListener);
-
     public boolean deferStartingActivity(RecentsAnimationDeviceState deviceState, MotionEvent ev) {
         TaskbarUIController controller = getTaskbarController();
         boolean isEventOverBubbleBarStashHandle =
@@ -163,49 +152,6 @@
         recentsView.switchToScreenshot(thumbnailDatas, runnable);
     }
 
-
-    protected void runOnInitBackgroundStateUI(Runnable callback) {
-        ACTIVITY_TYPE activity = getCreatedContainer();
-        if (activity != null && activity.getStateManager().getState() == mBackgroundState) {
-            callback.run();
-            onInitBackgroundStateUI();
-            return;
-        }
-        mOnInitBackgroundStateUICallback = callback;
-    }
-
-    private void onInitBackgroundStateUI() {
-        if (mOnInitBackgroundStateUICallback != null) {
-            mOnInitBackgroundStateUICallback.run();
-            mOnInitBackgroundStateUICallback = null;
-        }
-    }
-
-    public interface AnimationFactory {
-
-        void createActivityInterface(long transitionLength);
-
-        /**
-         * @param attached Whether to show RecentsView alongside the app window. If false, recents
-         *                 will be hidden by some property we can animate, e.g. alpha.
-         * @param animate Whether to animate recents to/from its new attached state.
-         * @param updateRunningTaskAlpha Whether to update the running task's attached alpha
-         */
-        default void setRecentsAttachedToAppWindow(
-                boolean attached, boolean animate, boolean updateRunningTaskAlpha) { }
-
-        default boolean isRecentsAttachedToAppWindow() {
-            return false;
-        }
-
-        default boolean hasRecentsEverAttachedToAppWindow() {
-            return false;
-        }
-
-        /** Called when the gesture ends and we know what state it is going towards */
-        default void setEndTarget(GestureState.GestureEndTarget endTarget) { }
-    }
-
     class DefaultAnimationFactory implements AnimationFactory {
 
         protected final ACTIVITY_TYPE mActivity;
@@ -234,7 +180,7 @@
         }
 
         @Override
-        public void createActivityInterface(long transitionLength) {
+        public void createContainerInterface(long transitionLength) {
             PendingAnimation pa = new PendingAnimation(transitionLength * 2);
             createBackgroundToOverviewAnim(mActivity, pa);
             AnimatorPlaybackController controller = pa.createPlaybackController();
diff --git a/quickstep/src/com/android/quickstep/BaseContainerInterface.java b/quickstep/src/com/android/quickstep/BaseContainerInterface.java
index bf3a662..2164bc2 100644
--- a/quickstep/src/com/android/quickstep/BaseContainerInterface.java
+++ b/quickstep/src/com/android/quickstep/BaseContainerInterface.java
@@ -34,19 +34,21 @@
 import android.view.View;
 
 import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Flags;
 import com.android.launcher3.R;
 import com.android.launcher3.statehandlers.DesktopVisibilityController;
 import com.android.launcher3.statemanager.BaseState;
+import com.android.launcher3.statemanager.StatefulContainer;
 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;
 import com.android.quickstep.util.AnimatorControllerWithResistance;
+import com.android.quickstep.util.ContextInitListener;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.RecentsViewContainer;
 import com.android.systemui.shared.recents.model.ThumbnailData;
@@ -57,13 +59,28 @@
 import java.util.function.Predicate;
 
 public abstract class BaseContainerInterface<STATE_TYPE extends BaseState<STATE_TYPE>,
-        CONTAINER_TYPE extends RecentsViewContainer> {
+        CONTAINER_TYPE extends RecentsViewContainer & StatefulContainer<STATE_TYPE>> {
 
     public boolean rotationSupportedByActivity = false;
+    protected final STATE_TYPE mBackgroundState;
+
+    protected BaseContainerInterface(STATE_TYPE backgroundState) {
+        mBackgroundState = backgroundState;
+    }
+
+    @UiThread
+    @Nullable
+    public abstract <T extends RecentsView<?,?>> T getVisibleRecentsView();
+
+    @UiThread
+    public abstract boolean switchToRecentsIfVisible(Animator.AnimatorListener animatorListener);
 
     @Nullable
     public abstract CONTAINER_TYPE getCreatedContainer();
 
+    @Nullable
+    protected Runnable mOnInitBackgroundStateUICallback = null;
+
     public abstract boolean isInLiveTileMode();
 
     public abstract void onAssistantVisibilityChanged(float assistantVisibility);
@@ -88,11 +105,36 @@
     @Nullable
     public abstract TaskbarUIController getTaskbarController();
 
-    public abstract BaseActivityInterface.AnimationFactory prepareRecentsUI(
+    public interface AnimationFactory {
+
+        void createContainerInterface(long transitionLength);
+
+        /**
+         * @param attached Whether to show RecentsView alongside the app window. If false, recents
+         *                 will be hidden by some property we can animate, e.g. alpha.
+         * @param animate Whether to animate recents to/from its new attached state.
+         * @param updateRunningTaskAlpha Whether to update the running task's attached alpha
+         */
+        default void setRecentsAttachedToAppWindow(
+                boolean attached, boolean animate, boolean updateRunningTaskAlpha) { }
+
+        default boolean isRecentsAttachedToAppWindow() {
+            return false;
+        }
+
+        default boolean hasRecentsEverAttachedToAppWindow() {
+            return false;
+        }
+
+        /** Called when the gesture ends and we know what state it is going towards */
+        default void setEndTarget(GestureState.GestureEndTarget endTarget) { }
+    }
+
+    public abstract BaseContainerInterface.AnimationFactory prepareRecentsUI(
             RecentsAnimationDeviceState deviceState, boolean activityVisible,
             Consumer<AnimatorControllerWithResistance> callback);
 
-    public abstract ActivityInitListener createActivityInitListener(
+    public abstract ContextInitListener createActivityInitListener(
             Predicate<Boolean> onInitListener);
     /**
      * Returns the expected STATE_TYPE from the provided GestureEndTarget.
@@ -126,6 +168,17 @@
         return false;
     }
 
+    public void runOnInitBackgroundStateUI(Runnable callback) {
+        StatefulContainer container = getCreatedContainer();
+        if (container != null
+                && container.getStateManager().getState() == mBackgroundState) {
+            callback.run();
+            onInitBackgroundStateUI();
+            return;
+        }
+        mOnInitBackgroundStateUICallback = callback;
+    }
+
     @Nullable
     public DesktopVisibilityController getDesktopVisibilityController() {
         CONTAINER_TYPE container = getCreatedContainer();
@@ -403,4 +456,11 @@
                 outRect,
                 orientationHandler);
     }
+
+    protected void onInitBackgroundStateUI() {
+        if (mOnInitBackgroundStateUICallback != null) {
+            mOnInitBackgroundStateUICallback.run();
+            mOnInitBackgroundStateUICallback = null;
+        }
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/BaseWindowInterface.java b/quickstep/src/com/android/quickstep/BaseWindowInterface.java
new file mode 100644
index 0000000..9d6e61d
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/BaseWindowInterface.java
@@ -0,0 +1,259 @@
+/*
+ * 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;
+
+import static com.android.app.animation.Interpolators.ACCELERATE_2;
+import static com.android.app.animation.Interpolators.INSTANT;
+import static com.android.app.animation.Interpolators.LINEAR;
+import static com.android.launcher3.MotionEventsUtils.isTrackpadMultiFingerSwipe;
+import static com.android.quickstep.AbsSwipeUpHandler.RECENTS_ATTACH_DURATION;
+import static com.android.quickstep.util.RecentsAtomicAnimationFactory.INDEX_RECENTS_FADE_ANIM;
+import static com.android.quickstep.util.RecentsAtomicAnimationFactory.INDEX_RECENTS_TRANSLATE_X_ANIM;
+import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_HORIZONTAL_OFFSET;
+import static com.android.quickstep.views.RecentsView.FULLSCREEN_PROGRESS;
+import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY;
+import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_TRANSLATION;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.util.Log;
+import android.view.MotionEvent;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.statehandlers.DepthController;
+import com.android.launcher3.taskbar.TaskbarUIController;
+import com.android.launcher3.util.DisplayController;
+import com.android.launcher3.util.NavigationMode;
+import com.android.quickstep.fallback.RecentsState;
+import com.android.quickstep.fallback.window.RecentsWindowManager;
+import com.android.quickstep.util.AnimatorControllerWithResistance;
+import com.android.quickstep.views.RecentsView;
+import com.android.systemui.shared.recents.model.ThumbnailData;
+
+import java.util.HashMap;
+import java.util.Optional;
+import java.util.function.Consumer;
+
+/**
+ * Temporary utility class in place for differences needed between
+ * Recents in Window in Launcher vs Fallback
+ */
+public abstract class BaseWindowInterface extends
+        BaseContainerInterface<RecentsState, RecentsWindowManager> {
+
+    final String TAG = "BaseWindowInterface";
+    private RecentsState mTargetState;
+
+
+    protected BaseWindowInterface(RecentsState overviewState, RecentsState backgroundState) {
+        super(backgroundState);
+        mTargetState = overviewState;
+    }
+
+    @Nullable
+    public abstract RecentsWindowManager getCreatedContainer();
+
+    @Nullable
+    public DepthController getDepthController() {
+        return null;
+    }
+
+    public final boolean isResumed() {
+        return isStarted();
+    }
+
+    public final boolean isStarted() {
+        RecentsWindowManager windowManager = getCreatedContainer();
+        return windowManager != null && windowManager.isStarted();
+    }
+
+    public boolean deferStartingActivity(RecentsAnimationDeviceState deviceState, MotionEvent ev) {
+        TaskbarUIController controller = getTaskbarController();
+        boolean isEventOverBubbleBarStashHandle =
+                controller != null && controller.isEventOverBubbleBarViews(ev);
+        return deviceState.isInDeferredGestureRegion(ev) || deviceState.isImeRenderingNavButtons()
+                || isTrackpadMultiFingerSwipe(ev) || isEventOverBubbleBarStashHandle;
+    }
+
+    /**
+     * Closes any overlays.
+     */
+    public void closeOverlay() {
+        Optional.ofNullable(getTaskbarController()).ifPresent(
+                TaskbarUIController::hideOverlayWindow);
+    }
+
+    public void switchRunningTaskViewToScreenshot(HashMap<Integer, ThumbnailData> thumbnailDatas,
+            Runnable runnable) {
+        RecentsWindowManager windowManager = getCreatedContainer();
+        if (windowManager == null) {
+            return;
+        }
+        RecentsView recentsView = windowManager.getOverviewPanel();
+        if (recentsView == null) {
+            if (runnable != null) {
+                runnable.run();
+            }
+            return;
+        }
+        recentsView.switchToScreenshot(thumbnailDatas, runnable);
+    }
+
+    /**
+     * todo: Create an abstract animation factory to handle both activity and window implementations
+     * todo: move new factory into BaseContainerInterface and cleanup.
+      */
+
+    class DefaultAnimationFactory implements AnimationFactory {
+
+        protected final RecentsWindowManager mRecentsWindowManager;
+        private final RecentsState mStartState;
+        private final Consumer<AnimatorControllerWithResistance> mCallback;
+
+        private boolean mIsAttachedToWindow;
+        private boolean mHasEverAttachedToWindow;
+
+        DefaultAnimationFactory(Consumer<AnimatorControllerWithResistance> callback) {
+            mCallback = callback;
+
+            mRecentsWindowManager = getCreatedContainer();
+            mStartState = mRecentsWindowManager.getStateManager().getState();
+        }
+
+        protected RecentsWindowManager initBackgroundStateUI() {
+            RecentsState resetState = mStartState;
+            if (mStartState.shouldDisableRestore()) {
+                resetState = mRecentsWindowManager.getStateManager().getRestState();
+            }
+            mRecentsWindowManager.getStateManager().setRestState(resetState);
+            mRecentsWindowManager.getStateManager().goToState(mBackgroundState, false);
+            onInitBackgroundStateUI();
+            return mRecentsWindowManager;
+        }
+
+        @Override
+        public void createContainerInterface(long transitionLength) {
+            PendingAnimation pa = new PendingAnimation(transitionLength * 2);
+            createBackgroundToOverviewAnim(mRecentsWindowManager, pa);
+            AnimatorPlaybackController controller = pa.createPlaybackController();
+            mRecentsWindowManager.getStateManager().setCurrentUserControlledAnimation(controller);
+
+            // Since we are changing the start position of the UI, reapply the state, at the end
+            controller.setEndAction(() -> {
+                mRecentsWindowManager.getStateManager().goToState(
+                        controller.getInterpolatedProgress() > 0.5 ? mTargetState
+                                : mBackgroundState,
+                        /* animated= */ false);
+            });
+
+            RecentsView recentsView = mRecentsWindowManager.getOverviewPanel();
+            AnimatorControllerWithResistance controllerWithResistance =
+                    AnimatorControllerWithResistance.createForRecents(controller,
+                            mRecentsWindowManager, recentsView.getPagedViewOrientedState(),
+                            mRecentsWindowManager.getDeviceProfile(), recentsView,
+                            RECENTS_SCALE_PROPERTY, recentsView, TASK_SECONDARY_TRANSLATION);
+            mCallback.accept(controllerWithResistance);
+
+            // Creating the activity controller animation sometimes reapplies the launcher state
+            // (because we set the animation as the current state animation), so we reapply the
+            // attached state here as well to ensure recents is shown/hidden appropriately.
+            if (DisplayController.getNavigationMode(mRecentsWindowManager)
+                    == NavigationMode.NO_BUTTON) {
+                setRecentsAttachedToAppWindow(mIsAttachedToWindow, false, false);
+            }
+        }
+
+        @Override
+        public void setRecentsAttachedToAppWindow(boolean attached, boolean animate,
+                boolean updateRunningTaskAlpha) {
+
+            if (mIsAttachedToWindow == attached && animate) {
+                return;
+            }
+            mRecentsWindowManager.getStateManager()
+                    .cancelStateElementAnimation(INDEX_RECENTS_FADE_ANIM);
+            mRecentsWindowManager.getStateManager()
+                    .cancelStateElementAnimation(INDEX_RECENTS_TRANSLATE_X_ANIM);
+
+            AnimatorSet animatorSet = new AnimatorSet();
+            animatorSet.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationStart(Animator animation) {
+                    super.onAnimationStart(animation);
+                    mIsAttachedToWindow = attached;
+                    if (attached) {
+                        mHasEverAttachedToWindow = true;
+                    }
+                }});
+
+            long animationDuration = animate ? RECENTS_ATTACH_DURATION : 0;
+            Animator fadeAnim = mRecentsWindowManager.getStateManager()
+                    .createStateElementAnimation(INDEX_RECENTS_FADE_ANIM, attached ? 1 : 0);
+            fadeAnim.setInterpolator(attached ? INSTANT : ACCELERATE_2);
+            fadeAnim.setDuration(animationDuration);
+            animatorSet.play(fadeAnim);
+
+            float fromTranslation = ADJACENT_PAGE_HORIZONTAL_OFFSET.get(
+                    mRecentsWindowManager.getOverviewPanel());
+            float toTranslation = attached ? 0 : 1;
+
+            Animator translationAnimator =
+                    mRecentsWindowManager.getStateManager().createStateElementAnimation(
+                            INDEX_RECENTS_TRANSLATE_X_ANIM, fromTranslation, toTranslation);
+            translationAnimator.setDuration(animationDuration);
+            animatorSet.play(translationAnimator);
+            animatorSet.start();
+        }
+
+        @Override
+        public boolean isRecentsAttachedToAppWindow() {
+            return mIsAttachedToWindow;
+        }
+
+        @Override
+        public boolean hasRecentsEverAttachedToAppWindow() {
+            return mHasEverAttachedToWindow;
+        }
+
+        @Override
+        public void setEndTarget(GestureState.GestureEndTarget endTarget) {
+            mTargetState = stateFromGestureEndTarget(endTarget);
+        }
+
+        protected void createBackgroundToOverviewAnim(RecentsWindowManager container,
+                PendingAnimation pa) {
+            //  Scale down recents from being full screen to being in overview.
+            RecentsView recentsView = container.getOverviewPanel();
+            pa.addFloat(recentsView, RECENTS_SCALE_PROPERTY,
+                    recentsView.getMaxScaleForFullScreen(), 1, LINEAR);
+            pa.addFloat(recentsView, FULLSCREEN_PROGRESS, 1, 0, LINEAR);
+
+            pa.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationStart(Animator animation) {
+                    TaskbarUIController taskbarUIController = getTaskbarController();
+                    if (taskbarUIController != null) {
+                        taskbarUIController.setSystemGestureInProgress(true);
+                    }
+                }
+            });
+        }
+    }
+}
\ No newline at end of file
diff --git a/quickstep/src/com/android/quickstep/ExternalDisplaySystemShortcut.kt b/quickstep/src/com/android/quickstep/ExternalDisplaySystemShortcut.kt
new file mode 100644
index 0000000..46c4f36
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/ExternalDisplaySystemShortcut.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.quickstep
+
+import android.view.View
+import com.android.launcher3.AbstractFloatingViewHelper
+import com.android.launcher3.R
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent
+import com.android.launcher3.popup.SystemShortcut
+import com.android.quickstep.views.RecentsView
+import com.android.quickstep.views.RecentsViewContainer
+import com.android.quickstep.views.TaskContainer
+import com.android.window.flags.Flags
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
+
+/** A menu item that allows the user to move the current app into external display. */
+class ExternalDisplaySystemShortcut(
+    container: RecentsViewContainer,
+    abstractFloatingViewHelper: AbstractFloatingViewHelper,
+    private val taskContainer: TaskContainer,
+) :
+    SystemShortcut<RecentsViewContainer>(
+        R.drawable.ic_external_display,
+        R.string.recent_task_option_external_display,
+        container,
+        taskContainer.itemInfo,
+        taskContainer.taskView,
+        abstractFloatingViewHelper,
+    ) {
+    override fun onClick(view: View) {
+        dismissTaskMenuView()
+        val recentsView = mTarget.getOverviewPanel<RecentsView<*, *>>()
+        recentsView.moveTaskToExternalDisplay(taskContainer) {
+            mTarget.statsLogManager
+                .logger()
+                .withItemInfo(taskContainer.itemInfo)
+                .log(LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_EXTERNAL_DISPLAY_TAP)
+        }
+    }
+
+    companion object {
+        @JvmOverloads
+        /**
+         * Creates a factory for creating move task to external display system shortcuts in
+         * [com.android.quickstep.TaskOverlayFactory].
+         */
+        fun createFactory(
+            abstractFloatingViewHelper: AbstractFloatingViewHelper = AbstractFloatingViewHelper()
+        ): TaskShortcutFactory =
+            object : TaskShortcutFactory {
+                override fun getShortcuts(
+                    container: RecentsViewContainer,
+                    taskContainer: TaskContainer,
+                ): List<ExternalDisplaySystemShortcut>? {
+                    return if (
+                        DesktopModeStatus.canEnterDesktopMode(container.asContext()) &&
+                            Flags.moveToExternalDisplayShortcut()
+                    )
+                        listOf(
+                            ExternalDisplaySystemShortcut(
+                                container,
+                                abstractFloatingViewHelper,
+                                taskContainer,
+                            )
+                        )
+                    else null
+                }
+
+                override fun showForGroupedTask() = true
+            }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/FallbackActivityInterface.java b/quickstep/src/com/android/quickstep/FallbackActivityInterface.java
index df83eb2..b787399 100644
--- a/quickstep/src/com/android/quickstep/FallbackActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/FallbackActivityInterface.java
@@ -36,8 +36,8 @@
 import com.android.quickstep.GestureState.GestureEndTarget;
 import com.android.quickstep.fallback.RecentsState;
 import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
-import com.android.quickstep.util.ActivityInitListener;
 import com.android.quickstep.util.AnimatorControllerWithResistance;
+import com.android.quickstep.util.ContextInitListener;
 import com.android.quickstep.views.RecentsView;
 
 import java.util.function.Consumer;
@@ -88,16 +88,16 @@
     }
 
     @Override
-    public ActivityInitListener createActivityInitListener(
+    public ContextInitListener<RecentsActivity> createActivityInitListener(
             Predicate<Boolean> onInitListener) {
-        return new ActivityInitListener<>((activity, alreadyOnHome) ->
+        return new ContextInitListener<>((activity, alreadyOnHome) ->
                 onInitListener.test(alreadyOnHome), RecentsActivity.ACTIVITY_TRACKER);
     }
 
     @Nullable
     @Override
     public RecentsActivity getCreatedContainer() {
-        return RecentsActivity.ACTIVITY_TRACKER.getCreatedActivity();
+        return RecentsActivity.ACTIVITY_TRACKER.getCreatedContext();
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/FallbackSwipeHandler.java b/quickstep/src/com/android/quickstep/FallbackSwipeHandler.java
index 9b66154..7abcfb8 100644
--- a/quickstep/src/com/android/quickstep/FallbackSwipeHandler.java
+++ b/quickstep/src/com/android/quickstep/FallbackSwipeHandler.java
@@ -64,6 +64,7 @@
 import com.android.launcher3.util.DisplayController;
 import com.android.quickstep.fallback.FallbackRecentsView;
 import com.android.quickstep.fallback.RecentsState;
+import com.android.quickstep.fallback.window.RecentsWindowManager;
 import com.android.quickstep.util.ActiveGestureLog;
 import com.android.quickstep.util.RectFSpringAnim;
 import com.android.quickstep.util.SurfaceTransaction.SurfaceProperties;
@@ -82,7 +83,7 @@
  * Handles the navigation gestures when a 3rd party launcher is the default home activity.
  */
 public class FallbackSwipeHandler extends
-        AbsSwipeUpHandler<RecentsActivity, FallbackRecentsView, RecentsState> {
+        AbsSwipeUpHandler<RecentsActivity, FallbackRecentsView<RecentsActivity>, RecentsState> {
 
     private static final String TAG = "FallbackSwipeHandler";
 
@@ -105,7 +106,7 @@
             TaskAnimationManager taskAnimationManager, GestureState gestureState, long touchTimeMs,
             boolean continuingLastGesture, InputConsumerController inputConsumer) {
         super(context, deviceState, taskAnimationManager, gestureState, touchTimeMs,
-                continuingLastGesture, inputConsumer);
+                continuingLastGesture, inputConsumer, null);
 
         mRunningOverHome = mGestureState.getRunningTask() != null
                 && mGestureState.getRunningTask().isHomeTask();
diff --git a/quickstep/src/com/android/quickstep/FallbackWindowInterface.java b/quickstep/src/com/android/quickstep/FallbackWindowInterface.java
new file mode 100644
index 0000000..832c093
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/FallbackWindowInterface.java
@@ -0,0 +1,259 @@
+/*
+ * 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;
+
+import static com.android.launcher3.util.NavigationMode.NO_BUTTON;
+import static com.android.quickstep.fallback.RecentsState.BACKGROUND_APP;
+import static com.android.quickstep.fallback.RecentsState.DEFAULT;
+import static com.android.quickstep.fallback.RecentsState.HOME;
+
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.content.Context;
+import android.graphics.Rect;
+import android.view.MotionEvent;
+import android.view.RemoteAnimationTarget;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.statemanager.StateManager;
+import com.android.launcher3.taskbar.FallbackTaskbarUIController;
+import com.android.launcher3.util.DisplayController;
+import com.android.quickstep.GestureState.GestureEndTarget;
+import com.android.quickstep.fallback.RecentsState;
+import com.android.quickstep.fallback.window.RecentsWindowManager;
+import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
+import com.android.quickstep.util.AnimatorControllerWithResistance;
+import com.android.quickstep.util.ContextInitListener;
+import com.android.quickstep.views.RecentsView;
+
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+
+/**
+ * {@link BaseWindowInterface} for recents when the default launcher is different than the
+ * currently running one and apps should interact with the {@link RecentsWindowManager} as opposed
+ * to the in-launcher one.
+ */
+public final class FallbackWindowInterface extends BaseWindowInterface{
+
+    private static FallbackWindowInterface INSTANCE;
+
+    private final RecentsWindowManager mRecentsWindowManager;
+
+    /**
+     * This is only null before init() or after destroy()
+     */
+    @Nullable
+    public static FallbackWindowInterface getInstance(){
+        return INSTANCE;
+    }
+
+    public static void init(RecentsWindowManager recentsWindowManager) {
+        if (INSTANCE == null) {
+            INSTANCE = new FallbackWindowInterface(recentsWindowManager);
+        }
+    }
+
+    private FallbackWindowInterface(RecentsWindowManager recentsWindowManager) {
+        super(DEFAULT, BACKGROUND_APP);
+        mRecentsWindowManager = recentsWindowManager;
+    }
+
+    public void destroy() {
+        INSTANCE = null;
+    }
+
+    /** 2 */
+    @Override
+    public int getSwipeUpDestinationAndLength(DeviceProfile dp, Context context, Rect outRect,
+            RecentsPagedOrientationHandler orientationHandler) {
+        calculateTaskSize(context, dp, outRect, orientationHandler);
+        if (dp.isVerticalBarLayout() && DisplayController.getNavigationMode(context) != NO_BUTTON) {
+            return dp.isSeascape() ? outRect.left : (dp.widthPx - outRect.right);
+        } else {
+            return dp.heightPx - outRect.bottom;
+        }
+    }
+
+    /** 5 */
+    @Override
+    public void onAssistantVisibilityChanged(float visibility) {
+        // This class becomes active when the screen is locked.
+        // Rather than having it handle assistant visibility changes, the assistant visibility is
+        // set to zero prior to this class becoming active.
+    }
+
+    /** 6 */
+    @Override
+    public BaseWindowInterface.AnimationFactory prepareRecentsUI(RecentsAnimationDeviceState
+            deviceState, boolean activityVisible,
+            Consumer<AnimatorControllerWithResistance> callback) {
+        notifyRecentsOfOrientation(deviceState.getRotationTouchHelper());
+        BaseWindowInterface.DefaultAnimationFactory factory =
+                new BaseWindowInterface.DefaultAnimationFactory(callback);
+        factory.initBackgroundStateUI();
+        return factory;
+    }
+
+    @Override
+    public ContextInitListener<RecentsWindowManager> createActivityInitListener(
+            Predicate<Boolean> onInitListener) {
+        return new ContextInitListener<>(
+                (activity, alreadyOnHome) -> onInitListener.test(alreadyOnHome),
+                RecentsWindowManager.getRecentsWindowTracker());
+    }
+
+    @Nullable
+    @Override
+    public RecentsWindowManager getCreatedContainer() {
+        return mRecentsWindowManager;
+    }
+
+    @Override
+    public FallbackTaskbarUIController getTaskbarController() {
+        RecentsWindowManager manager = getCreatedContainer();
+        if (manager == null) {
+            return null;
+        }
+        return null;
+        // todo b/365775636: pass a taskbar implementation
+        // return manager.getTaskbarUIController();
+    }
+
+    @Override
+    public Rect getOverviewWindowBounds(Rect homeBounds, RemoteAnimationTarget target) {
+        // TODO: Remove this once b/77875376 is fixed
+        return target.screenSpaceBounds;
+    }
+
+    @Nullable
+    @Override
+    public <T extends RecentsView<?, ?>> T getVisibleRecentsView() {
+        RecentsWindowManager manager = getCreatedContainer();
+        if(manager.isStarted() || isInLiveTileMode()){
+            return getCreatedContainer().getOverviewPanel();
+        }
+        return null;
+    }
+
+    @Override
+    public boolean switchToRecentsIfVisible(Animator.AnimatorListener animatorListener) {
+        return false;
+    }
+
+    @Override
+    protected int getOverviewScrimColorForState(RecentsWindowManager container,
+            RecentsState state) {
+        return state.getScrimColor(container.asContext());
+    }
+
+    @Override
+    public boolean deferStartingActivity(RecentsAnimationDeviceState deviceState, MotionEvent ev) {
+        // In non-gesture mode, user might be clicking on the home button which would directly
+        // start the home activity instead of going through recents. In that case, defer starting
+        // recents until we are sure it is a gesture.
+        return false;
+//        return !deviceState.isFullyGesturalNavMode();
+//                || super.deferStartingActivity(deviceState, ev);
+    }
+
+    @Override
+    public void onExitOverview(RotationTouchHelper deviceState, Runnable exitRunnable) {
+        final StateManager<RecentsState, RecentsWindowManager> stateManager =
+                getCreatedContainer().getStateManager();
+        if (stateManager.getState() == HOME) {
+            exitRunnable.run();
+            notifyRecentsOfOrientation(deviceState);
+            return;
+        }
+
+        stateManager.addStateListener(
+                new StateManager.StateListener<RecentsState>() {
+                    @Override
+                    public void onStateTransitionComplete(RecentsState toState) {
+                        // Are we going from Recents to Workspace?
+                        if (toState == HOME) {
+                            exitRunnable.run();
+                            notifyRecentsOfOrientation(deviceState);
+                            stateManager.removeStateListener(this);
+                        }
+                    }
+                });
+    }
+
+    @Override
+    public boolean isInLiveTileMode() {
+        RecentsWindowManager windowManager = getCreatedContainer();
+        return windowManager != null && windowManager.getStateManager().getState() == DEFAULT &&
+                windowManager.isStarted();
+    }
+
+    @Override
+    public void onLaunchTaskFailed() {
+        // TODO: probably go back to overview instead.
+        RecentsWindowManager manager = getCreatedContainer();
+        if (manager == null) {
+            return;
+        }
+        manager.<RecentsView>getOverviewPanel().startHome();
+    }
+
+    @Override
+    public RecentsState stateFromGestureEndTarget(GestureEndTarget endTarget) {
+        switch (endTarget) {
+            case RECENTS:
+                return DEFAULT;
+            case NEW_TASK:
+            case LAST_TASK:
+                return BACKGROUND_APP;
+            case HOME:
+            case ALL_APPS:
+            default:
+                return HOME;
+        }
+    }
+
+    private void notifyRecentsOfOrientation(RotationTouchHelper rotationTouchHelper) {
+        // reset layout on swipe to home
+        RecentsView recentsView = getCreatedContainer().getOverviewPanel();
+        recentsView.setLayoutRotation(rotationTouchHelper.getCurrentActiveRotation(),
+                rotationTouchHelper.getDisplayRotation());
+    }
+
+    @Override
+    public @Nullable Animator getParallelAnimationToLauncher(GestureEndTarget endTarget,
+            long duration, RecentsAnimationCallbacks callbacks) {
+        FallbackTaskbarUIController uiController = getTaskbarController();
+        Animator superAnimator = super.getParallelAnimationToLauncher(
+                endTarget, duration, callbacks);
+        if (uiController == null) {
+            return superAnimator;
+        }
+        RecentsState toState = stateFromGestureEndTarget(endTarget);
+        Animator taskbarAnimator = uiController.createAnimToRecentsState(toState, duration);
+        if (taskbarAnimator == null) {
+            return superAnimator;
+        }
+        if (superAnimator == null) {
+            return taskbarAnimator;
+        }
+        AnimatorSet animatorSet = new AnimatorSet();
+        animatorSet.playTogether(superAnimator, taskbarAnimator);
+        return animatorSet;
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/FocusState.kt b/quickstep/src/com/android/quickstep/FocusState.kt
new file mode 100644
index 0000000..ba3991f
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/FocusState.kt
@@ -0,0 +1,63 @@
+/*
+ * 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
+
+import android.os.RemoteException
+import android.util.Log
+import android.view.Display.DEFAULT_DISPLAY
+import com.android.launcher3.util.Executors
+import com.android.wm.shell.shared.IFocusTransitionListener.Stub
+import com.android.wm.shell.shared.IShellTransitions
+
+/** Class to track focus state of displays and windows */
+class FocusState {
+
+    var focusedDisplayId = DEFAULT_DISPLAY
+        private set
+
+    private var listeners = mutableSetOf<FocusChangeListener>()
+
+    fun addListener(l: FocusChangeListener) = listeners.add(l)
+
+    fun removeListener(l: FocusChangeListener) = listeners.remove(l)
+
+    fun init(transitions: IShellTransitions?) {
+        try {
+            transitions?.setFocusTransitionListener(
+                object : Stub() {
+                    override fun onFocusedDisplayChanged(displayId: Int) {
+                        Executors.MAIN_EXECUTOR.execute {
+                            listeners.forEach { it.onFocusedDisplayChanged(displayId) }
+                        }
+                    }
+                }
+            )
+        } catch (e: RemoteException) {
+            Log.w(TAG, "Failed call setFocusTransitionListener", e)
+        }
+    }
+
+    interface FocusChangeListener {
+        fun onFocusedDisplayChanged(displayId: Int)
+    }
+
+    override fun toString() = "{FocusState focusedDisplayId=$focusedDisplayId}"
+
+    companion object {
+        private const val TAG = "FocusState"
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/GestureState.java b/quickstep/src/com/android/quickstep/GestureState.java
index 9cc463a..5190ec8 100644
--- a/quickstep/src/com/android/quickstep/GestureState.java
+++ b/quickstep/src/com/android/quickstep/GestureState.java
@@ -24,11 +24,11 @@
 import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_HOME;
 import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_OVERVIEW;
 import static com.android.quickstep.MultiStateCallback.DEBUG_STATES;
-import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.SET_END_TARGET;
 import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.SET_END_TARGET_ALL_APPS;
 import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.SET_END_TARGET_HOME;
 import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.SET_END_TARGET_NEW_TASK;
 
+import android.app.TaskInfo;
 import android.content.Intent;
 import android.os.SystemClock;
 import android.view.MotionEvent;
@@ -37,12 +37,16 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.android.launcher3.Flags;
 import com.android.launcher3.statemanager.BaseState;
+import com.android.launcher3.statemanager.StatefulContainer;
 import com.android.quickstep.TopTaskTracker.CachedTaskInfo;
 import com.android.quickstep.util.ActiveGestureErrorDetector;
 import com.android.quickstep.util.ActiveGestureLog;
+import com.android.quickstep.util.ActiveGestureProtoLogProxy;
 import com.android.quickstep.views.RecentsViewContainer;
 import com.android.systemui.shared.recents.model.ThumbnailData;
+import com.android.wm.shell.shared.GroupedTaskInfo;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -190,7 +194,7 @@
     public GestureState(OverviewComponentObserver componentObserver, int gestureId) {
         mHomeIntent = componentObserver.getHomeIntent();
         mOverviewIntent = componentObserver.getOverviewIntent();
-        mContainerInterface = componentObserver.getActivityInterface();
+        mContainerInterface = componentObserver.getContainerInterface();
         mStateCallback = new MultiStateCallback(
                 STATE_NAMES.toArray(new String[0]), GestureState::getTrackedEventForState);
         mGestureId = gestureId;
@@ -269,8 +273,8 @@
     /**
      * @return the interface to the activity handing the UI updates for this gesture.
      */
-    public <S extends BaseState<S>, T extends RecentsViewContainer>
-            BaseContainerInterface<S, T> getContainerInterface() {
+    public <S extends BaseState<S>, T extends RecentsViewContainer & StatefulContainer<S>>
+    BaseContainerInterface getContainerInterface() {
         return mContainerInterface;
     }
 
@@ -301,6 +305,18 @@
     }
 
     /**
+     * Requests that handling for this gesture should use a synthetic transition, as in that it
+     * will need to start a recents transition that is not backed by a system transition.  This is
+     * generally only needed in scenarios where a system transition can not be created due to no
+     * changes in the WM hierarchy (ie. starting recents transition when you are already over home).
+     */
+    public boolean useSyntheticRecentsTransition() {
+        return mRunningTask.isHomeTask()
+                && (Flags.enableFallbackOverviewInWindow()
+                        || Flags.enableLauncherOverviewInWindow());
+    }
+
+    /**
      * @return the running task for this gesture.
      */
     @Nullable
@@ -316,13 +332,23 @@
         if (mRunningTask == null) {
             return new int[]{INVALID_TASK_ID, INVALID_TASK_ID};
         } else {
-            int cachedTasksSize = mRunningTask.mAllCachedTasks.size();
-            int count = Math.min(cachedTasksSize, getMultipleTasks ? 2 : 1);
-            int[] runningTaskIds = new int[count];
-            for (int i = 0; i < count; i++) {
-                runningTaskIds[i] = mRunningTask.mAllCachedTasks.get(i).taskId;
+            if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+                if (mRunningTask.getVisibleTasks().isEmpty()) {
+                    return new int[0];
+                }
+                GroupedTaskInfo topRunningTask = mRunningTask.getVisibleTasks().getFirst();
+                List<TaskInfo> groupedTasks = topRunningTask.getTaskInfoList();
+                return groupedTasks.stream().mapToInt(
+                        groupedTask -> groupedTask.taskId).toArray();
+            } else {
+                int cachedTasksSize = mRunningTask.mAllCachedTasks.size();
+                int count = Math.min(cachedTasksSize, getMultipleTasks ? 2 : 1);
+                int[] runningTaskIds = new int[count];
+                for (int i = 0; i < count; i++) {
+                    runningTaskIds[i] = mRunningTask.mAllCachedTasks.get(i).taskId;
+                }
+                return runningTaskIds;
             }
-            return runningTaskIds;
         }
     }
 
@@ -410,10 +436,7 @@
     public void setEndTarget(GestureEndTarget target, boolean isAtomic) {
         mEndTarget = target;
         mStateCallback.setState(STATE_END_TARGET_SET);
-        ActiveGestureLog.INSTANCE.addLog(
-                new ActiveGestureLog.CompoundString("setEndTarget ")
-                        .append(mEndTarget.name()),
-                /* gestureEvent= */ SET_END_TARGET);
+        ActiveGestureProtoLogProxy.logSetEndTarget(mEndTarget.name());
         switch (mEndTarget) {
             case HOME:
                 ActiveGestureLog.INSTANCE.trackEvent(SET_END_TARGET_HOME);
diff --git a/quickstep/src/com/android/quickstep/LauncherActivityInterface.java b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
index 85312e4..ef6a09d 100644
--- a/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
@@ -47,7 +47,6 @@
 import com.android.launcher3.util.NavigationMode;
 import com.android.quickstep.GestureState.GestureEndTarget;
 import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
-import com.android.quickstep.util.ActivityInitListener;
 import com.android.quickstep.util.AnimatorControllerWithResistance;
 import com.android.quickstep.util.LayoutUtils;
 import com.android.quickstep.views.RecentsView;
@@ -134,7 +133,7 @@
     }
 
     @Override
-    public ActivityInitListener createActivityInitListener(Predicate<Boolean> onInitListener) {
+    public LauncherInitListener createActivityInitListener(Predicate<Boolean> onInitListener) {
         return new LauncherInitListener((activity, alreadyOnHome) ->
                 onInitListener.test(alreadyOnHome));
     }
@@ -151,7 +150,7 @@
     @Nullable
     @Override
     public QuickstepLauncher getCreatedContainer() {
-        return QuickstepLauncher.ACTIVITY_TRACKER.getCreatedActivity();
+        return QuickstepLauncher.ACTIVITY_TRACKER.getCreatedContext();
     }
 
     @Nullable
diff --git a/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java b/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java
index d2dcd7b..dacafd4 100644
--- a/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java
+++ b/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java
@@ -46,6 +46,7 @@
 import com.android.launcher3.views.FloatingIconView;
 import com.android.launcher3.views.FloatingView;
 import com.android.launcher3.widget.LauncherAppWidgetHostView;
+import com.android.quickstep.fallback.window.RecentsWindowManager;
 import com.android.quickstep.util.RectFSpringAnim;
 import com.android.quickstep.util.ScalingWorkspaceRevealAnim;
 import com.android.quickstep.util.StaggeredWorkspaceAnim;
@@ -68,7 +69,7 @@
             TaskAnimationManager taskAnimationManager, GestureState gestureState, long touchTimeMs,
             boolean continuingLastGesture, InputConsumerController inputConsumer) {
         super(context, deviceState, taskAnimationManager, gestureState, touchTimeMs,
-                continuingLastGesture, inputConsumer);
+                continuingLastGesture, inputConsumer, null);
     }
 
 
diff --git a/quickstep/src/com/android/quickstep/MultiStateCallback.java b/quickstep/src/com/android/quickstep/MultiStateCallback.java
index df42efc..a9f196d 100644
--- a/quickstep/src/com/android/quickstep/MultiStateCallback.java
+++ b/quickstep/src/com/android/quickstep/MultiStateCallback.java
@@ -28,6 +28,7 @@
 import com.android.launcher3.config.FeatureFlags;
 import com.android.quickstep.util.ActiveGestureErrorDetector;
 import com.android.quickstep.util.ActiveGestureLog;
+import com.android.quickstep.util.ActiveGestureProtoLogProxy;
 
 import java.util.ArrayList;
 import java.util.LinkedList;
@@ -114,10 +115,9 @@
             if (gestureEvent == null) {
                 continue;
             }
-            if (gestureEvent.mLogEvent && gestureEvent.mTrackEvent) {
-                ActiveGestureLog.INSTANCE.addLog(gestureEvent.name(), gestureEvent);
-            } else if (gestureEvent.mLogEvent) {
-                ActiveGestureLog.INSTANCE.addLog(gestureEvent.name());
+            if (gestureEvent.mLogEvent) {
+                ActiveGestureProtoLogProxy.logDynamicString(
+                        gestureEvent.name(), gestureEvent.mTrackEvent ? gestureEvent : null);
             } else if (gestureEvent.mTrackEvent) {
                 ActiveGestureLog.INSTANCE.trackEvent(gestureEvent);
             }
diff --git a/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt b/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt
index 520bec3..461f963 100644
--- a/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt
+++ b/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt
@@ -27,6 +27,7 @@
 import androidx.annotation.UiThread
 import androidx.annotation.VisibleForTesting
 import com.android.internal.jank.Cuj
+import com.android.launcher3.Flags.enableLargeDesktopWindowingTile
 import com.android.launcher3.Flags.enableOverviewCommandHelperTimeout
 import com.android.launcher3.PagedView
 import com.android.launcher3.logger.LauncherAtom
@@ -45,8 +46,8 @@
 import com.android.quickstep.OverviewCommandHelper.CommandType.SHOW
 import com.android.quickstep.OverviewCommandHelper.CommandType.TOGGLE
 import com.android.quickstep.util.ActiveGestureLog
+import com.android.quickstep.util.ActiveGestureProtoLogProxy
 import com.android.quickstep.views.RecentsView
-import com.android.quickstep.views.RecentsViewContainer
 import com.android.quickstep.views.TaskView
 import com.android.systemui.shared.recents.model.ThumbnailData
 import com.android.systemui.shared.system.InteractionJankMonitorWrapper
@@ -80,11 +81,11 @@
      */
     private var keyboardTaskFocusIndex = -1
 
-    private val activityInterface: BaseActivityInterface<*, *>
-        get() = overviewComponentObserver.activityInterface
+    private val containerInterface: BaseContainerInterface<*, *>
+        get() = overviewComponentObserver.containerInterface
 
     private val visibleRecentsView: RecentsView<*, *>?
-        get() = activityInterface.getVisibleRecentsView<RecentsView<*, *>>()
+        get() = containerInterface.getVisibleRecentsView<RecentsView<*, *>>()
 
     /**
      * Adds a command to be executed next, after all pending tasks are completed. Max commands that
@@ -215,13 +216,12 @@
                 }
             }
             TOGGLE -> {
-                val taskView =
-                    if (recentsView.runningTaskView == null) {
-                        recentsView.getTaskViewAt(0)
-                    } else {
-                        recentsView.nextTaskView ?: recentsView.runningTaskView
-                    }
-                launchTask(recentsView, taskView, command, onCallbackResult)
+                launchTask(
+                    recentsView,
+                    getNextToggledTaskView(recentsView),
+                    command,
+                    onCallbackResult,
+                )
             }
             HOME -> {
                 recentsView.startHome()
@@ -229,6 +229,27 @@
             }
         }
 
+    private fun getNextToggledTaskView(recentsView: RecentsView<*, *>): TaskView? {
+        // When running task view is null we return last large taskView - typically focusView when
+        // grid only is not enabled else last desktop task view.
+        return if (recentsView.runningTaskView == null) {
+            recentsView.lastLargeTaskView ?: recentsView.getTaskViewAt(0)
+        } else {
+            if (
+                enableLargeDesktopWindowingTile() &&
+                    recentsView.getTaskViewCount() == recentsView.largeTilesCount &&
+                    recentsView.runningTaskView === recentsView.lastLargeTaskView
+            ) {
+                // Enables the toggle when only large tiles are in recents view.
+                // We return previous because unlike small tiles, large tiles are always
+                // on the right hand side.
+                recentsView.previousTaskView ?: recentsView.runningTaskView
+            } else {
+                recentsView.nextTaskView ?: recentsView.runningTaskView
+            }
+        }
+    }
+
     private fun launchTask(
         recents: RecentsView<*, *>,
         taskView: TaskView?,
@@ -258,10 +279,10 @@
         command: CommandInfo,
         onCallbackResult: () -> Unit,
     ): Boolean {
-        val recentsViewContainer = activityInterface.getCreatedContainer() as? RecentsViewContainer
+        val recentsViewContainer = containerInterface.getCreatedContainer()
         val recentsView: RecentsView<*, *>? = recentsViewContainer?.getOverviewPanel()
         val deviceProfile = recentsViewContainer?.getDeviceProfile()
-        val uiController = activityInterface.getTaskbarController()
+        val uiController = containerInterface.getTaskbarController()
         val allowQuickSwitch =
             uiController != null &&
                 deviceProfile != null &&
@@ -281,7 +302,7 @@
                     keyboardTaskFocusIndex = 0
                 }
             HOME -> {
-                ActiveGestureLog.INSTANCE.addLog("OverviewCommandHelper.executeCommand(HOME)")
+                ActiveGestureProtoLogProxy.logExecuteHomeCommand()
                 // Although IActivityTaskManager$Stub$Proxy.startActivity is a slow binder call,
                 // we should still call it on main thread because launcher is waiting for
                 // ActivityTaskManager to resume it. Also calling startActivity() on bg thread
@@ -316,13 +337,13 @@
                     onCallbackResult()
                 }
             }
-        if (activityInterface.switchToRecentsIfVisible(animatorListener)) {
+        if (containerInterface.switchToRecentsIfVisible(animatorListener)) {
             Log.d(TAG, "switching to Overview state - waiting: $command")
             // If successfully switched, wait until animation finishes
             return false
         }
 
-        val activity = activityInterface.getCreatedContainer()
+        val activity = containerInterface.getCreatedContainer()
         if (activity != null) {
             InteractionJankMonitorWrapper.begin(activity.rootView, Cuj.CUJ_LAUNCHER_QUICK_SWITCH)
         }
@@ -352,7 +373,7 @@
                     Log.d(TAG, "recents animation started: $command")
                     updateRecentsViewFocus(command)
                     logShowOverviewFrom(command.type)
-                    activityInterface.runOnInitBackgroundStateUI {
+                    containerInterface.runOnInitBackgroundStateUI {
                         Log.d(TAG, "recents animation started - onInitBackgroundStateUI: $command")
                         interactionHandler.onGestureEnded(0f, PointF())
                     }
@@ -366,7 +387,7 @@
                     interactionHandler.onGestureCancelled()
                     command.removeListener(this)
 
-                    activityInterface.getCreatedContainer() ?: return
+                    containerInterface.getCreatedContainer() ?: return
                     recentsView?.onRecentsAnimationComplete()
                 }
             }
@@ -473,7 +494,7 @@
     }
 
     private fun logShowOverviewFrom(commandType: CommandType) {
-        val container = activityInterface.getCreatedContainer() as? RecentsViewContainer ?: return
+        val container = containerInterface.getCreatedContainer() ?: return
         val event =
             when (commandType) {
                 SHOW -> LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_KEYBOARD_SHORTCUT
diff --git a/quickstep/src/com/android/quickstep/OverviewComponentObserver.java b/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
index ca19480..1f6c671 100644
--- a/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
+++ b/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
@@ -39,9 +39,10 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
 
+import com.android.launcher3.Flags;
 import com.android.launcher3.R;
 import com.android.launcher3.util.SimpleBroadcastReceiver;
-import com.android.quickstep.util.ActiveGestureLog;
+import com.android.quickstep.util.ActiveGestureProtoLogProxy;
 import com.android.systemui.shared.system.PackageManagerWrapper;
 
 import java.io.PrintWriter;
@@ -73,7 +74,7 @@
     private Consumer<Boolean> mOverviewChangeListener = b -> { };
 
     private String mUpdateRegisteredPackage;
-    private BaseActivityInterface mActivityInterface;
+    private BaseContainerInterface mContainerInterface;
     private Intent mOverviewIntent;
     private boolean mIsHomeAndOverviewSame;
     private boolean mIsDefaultHome;
@@ -150,11 +151,11 @@
         // Set assistant visibility to 0 from launcher's perspective, ensures any elements that
         // launcher made invisible become visible again before the new activity control helper
         // becomes active.
-        if (mActivityInterface != null) {
-            mActivityInterface.onAssistantVisibilityChanged(0.f);
+        if (mContainerInterface != null) {
+            mContainerInterface.onAssistantVisibilityChanged(0.f);
         }
 
-        if (SEPARATE_RECENTS_ACTIVITY.get()) {
+        if (SEPARATE_RECENTS_ACTIVITY.get() || Flags.enableLauncherOverviewInWindow()) {
             mIsDefaultHome = false;
             if (defaultHome == null) {
                 defaultHome = mMyHomeIntent.getComponent();
@@ -168,7 +169,7 @@
 
         if (!mIsHomeDisabled && (defaultHome == null || mIsDefaultHome)) {
             // User default home is same as out home app. Use Overview integrated in Launcher.
-            mActivityInterface = LauncherActivityInterface.INSTANCE;
+            mContainerInterface = LauncherActivityInterface.INSTANCE;
             mIsHomeAndOverviewSame = true;
             mOverviewIntent = mMyHomeIntent;
             mCurrentHomeIntent.setComponent(mMyHomeIntent.getComponent());
@@ -178,7 +179,11 @@
         } else {
             // The default home app is a different launcher. Use the fallback Overview instead.
 
-            mActivityInterface = FallbackActivityInterface.INSTANCE;
+            if (Flags.enableLauncherOverviewInWindow() || Flags.enableFallbackOverviewInWindow()) {
+                mContainerInterface = FallbackWindowInterface.getInstance();
+            } else {
+                mContainerInterface = FallbackActivityInterface.INSTANCE;
+            }
             mIsHomeAndOverviewSame = false;
             mOverviewIntent = mFallbackIntent;
             mCurrentHomeIntent.setComponent(defaultHome);
@@ -266,21 +271,12 @@
     }
 
     /**
-     * Get the current activity control helper for managing interactions to the overview activity.
+     * Get the current control helper for managing interactions to the overview container.
      *
-     * @return the current activity control helper
+     * @return the current control helper
      */
-    public BaseActivityInterface getActivityInterface() {
-        return mActivityInterface;
-    }
-
-    /**
-     * Get the current container control helper for managing interactions to the overview activity.
-     *
-     * @return the current container control helper
-     */
-    public BaseContainerInterface<?, ?> getContainerInterface() {
-        return mActivityInterface;
+    public BaseContainerInterface<?,?> getContainerInterface() {
+        return mContainerInterface;
     }
 
     public void dump(PrintWriter pw) {
@@ -309,10 +305,11 @@
      * Starts the intent for the current home activity.
      */
     public static void startHomeIntentSafely(
-            @NonNull Context context, @NonNull Intent homeIntent, @Nullable Bundle options,
+            @NonNull Context context,
+            @NonNull Intent homeIntent,
+            @Nullable Bundle options,
             @NonNull String reason) {
-        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
-                "OverviewComponentObserver.startHomeIntent: ").append(reason));
+        ActiveGestureProtoLogProxy.logStartHomeIntent(reason);
         try {
             context.startActivity(homeIntent, options);
         } catch (NullPointerException | ActivityNotFoundException | SecurityException e) {
diff --git a/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java b/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java
index f4e68dc..334bead 100644
--- a/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java
+++ b/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java
@@ -25,6 +25,7 @@
 
 import com.android.launcher3.BuildConfig;
 import com.android.launcher3.MainProcessInitializer;
+import com.android.quickstep.util.QuickstepProtoLogGroup;
 import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
 
 @SuppressWarnings("unused")
@@ -69,5 +70,7 @@
                     call.descriptor + " called on main thread under " + call.activeTrace
                             + " stackTrace: " + call.stackTrace));
         }
+
+        QuickstepProtoLogGroup.initProtoLog();
     }
 }
diff --git a/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java b/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
index 49b6f57..2828a84 100644
--- a/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
+++ b/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
@@ -3,10 +3,10 @@
 import static com.android.launcher3.taskbar.TaskbarThresholdUtils.getFromNavThreshold;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 
-import android.app.Activity;
 import android.content.Context;
 import android.content.res.Resources;
 import android.os.Bundle;
+import android.view.WindowInsets;
 
 import androidx.annotation.Nullable;
 
@@ -203,11 +203,13 @@
     }
 
     @Override
-    protected Activity getCurrentActivity() {
+    protected WindowInsets getWindowInsets() {
         RecentsAnimationDeviceState rads = new RecentsAnimationDeviceState(mContext);
         OverviewComponentObserver observer = new OverviewComponentObserver(mContext, rads);
         try {
-            return observer.getActivityInterface().getCreatedContainer();
+            RecentsViewContainer container = observer.getContainerInterface().getCreatedContainer();
+
+            return container == null ? null : container.getRootView().getRootWindowInsets();
         } finally {
             observer.onDestroy();
             rads.destroy();
diff --git a/quickstep/src/com/android/quickstep/RecentTasksList.java b/quickstep/src/com/android/quickstep/RecentTasksList.java
index 05bef35..85e2b6e 100644
--- a/quickstep/src/com/android/quickstep/RecentTasksList.java
+++ b/quickstep/src/com/android/quickstep/RecentTasksList.java
@@ -20,9 +20,9 @@
 
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 import static com.android.quickstep.util.SplitScreenUtils.convertShellSplitBoundsToLauncher;
-import static com.android.wm.shell.shared.GroupedRecentTaskInfo.TYPE_FREEFORM;
+import static com.android.wm.shell.shared.GroupedTaskInfo.TYPE_FREEFORM;
 
-import android.app.ActivityManager;
+import android.app.ActivityManager.RunningTaskInfo;
 import android.app.KeyguardManager;
 import android.app.TaskInfo;
 import android.content.ComponentName;
@@ -40,7 +40,7 @@
 import com.android.quickstep.util.GroupTask;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.wm.shell.recents.IRecentTasksListener;
-import com.android.wm.shell.shared.GroupedRecentTaskInfo;
+import com.android.wm.shell.shared.GroupedTaskInfo;
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
 
 import java.io.PrintWriter;
@@ -76,7 +76,7 @@
     private @Nullable RecentsModel.RunningTasksListener mRunningTasksListener;
     private @Nullable RecentsModel.RecentTasksChangedListener mRecentTasksChangedListener;
     // Tasks are stored in order of least recently launched to most recently launched.
-    private ArrayList<ActivityManager.RunningTaskInfo> mRunningTasks;
+    private ArrayList<RunningTaskInfo> mRunningTasks;
 
     public RecentTasksList(Context context, LooperExecutor mainThreadExecutor,
             KeyguardManager keyguardManager, SystemUiProxy sysUiProxy,
@@ -93,30 +93,42 @@
             }
 
             @Override
-            public void onRunningTaskAppeared(ActivityManager.RunningTaskInfo taskInfo) {
+            public void onRunningTaskAppeared(RunningTaskInfo taskInfo) {
                 mMainThreadExecutor.execute(() -> {
                     RecentTasksList.this.onRunningTaskAppeared(taskInfo);
                 });
             }
 
             @Override
-            public void onRunningTaskVanished(ActivityManager.RunningTaskInfo taskInfo) {
+            public void onRunningTaskVanished(RunningTaskInfo taskInfo) {
                 mMainThreadExecutor.execute(() -> {
                     RecentTasksList.this.onRunningTaskVanished(taskInfo);
                 });
             }
 
             @Override
-            public void onRunningTaskChanged(ActivityManager.RunningTaskInfo taskInfo) {
+            public void onRunningTaskChanged(RunningTaskInfo taskInfo) {
                 mMainThreadExecutor.execute(() -> {
                     RecentTasksList.this.onRunningTaskChanged(taskInfo);
                 });
             }
 
             @Override
-            public void onTaskMovedToFront(ActivityManager.RunningTaskInfo taskInfo) {
+            public void onTaskMovedToFront(GroupedTaskInfo taskToFront) {
                 mMainThreadExecutor.execute(() -> {
-                    topTaskTracker.onTaskMovedToFront(taskInfo);
+                    topTaskTracker.handleTaskMovedToFront(taskToFront.getTaskInfo1());
+                });
+            }
+
+            @Override
+            public void onTaskInfoChanged(RunningTaskInfo taskInfo) {
+                mMainThreadExecutor.execute(() -> topTaskTracker.onTaskChanged(taskInfo));
+            }
+
+            @Override
+            public void onVisibleTasksChanged(GroupedTaskInfo[] visibleTasks) {
+                mMainThreadExecutor.execute(() -> {
+                    topTaskTracker.onVisibleTasksChanged(visibleTasks);
                 });
             }
         });
@@ -245,7 +257,7 @@
         mRecentTasksChangedListener = null;
     }
 
-    private void initRunningTasks(ArrayList<ActivityManager.RunningTaskInfo> runningTasks) {
+    private void initRunningTasks(ArrayList<RunningTaskInfo> runningTasks) {
         // Tasks are retrieved in order of most recently launched/used to least recently launched.
         mRunningTasks = new ArrayList<>(runningTasks);
         Collections.reverse(mRunningTasks);
@@ -254,13 +266,13 @@
     /**
      * Gets the set of running tasks.
      */
-    public ArrayList<ActivityManager.RunningTaskInfo> getRunningTasks() {
+    public ArrayList<RunningTaskInfo> getRunningTasks() {
         return mRunningTasks;
     }
 
-    private void onRunningTaskAppeared(ActivityManager.RunningTaskInfo taskInfo) {
+    private void onRunningTaskAppeared(RunningTaskInfo taskInfo) {
         // Make sure this task is not already in the list
-        for (ActivityManager.RunningTaskInfo existingTask : mRunningTasks) {
+        for (RunningTaskInfo existingTask : mRunningTasks) {
             if (taskInfo.taskId == existingTask.taskId) {
                 return;
             }
@@ -271,9 +283,9 @@
         }
     }
 
-    private void onRunningTaskVanished(ActivityManager.RunningTaskInfo taskInfo) {
+    private void onRunningTaskVanished(RunningTaskInfo taskInfo) {
         // Find the task from the list of running tasks, if it exists
-        for (ActivityManager.RunningTaskInfo existingTask : mRunningTasks) {
+        for (RunningTaskInfo existingTask : mRunningTasks) {
             if (existingTask.taskId != taskInfo.taskId) continue;
 
             mRunningTasks.remove(existingTask);
@@ -284,9 +296,9 @@
         }
     }
 
-    private void onRunningTaskChanged(ActivityManager.RunningTaskInfo taskInfo) {
+    private void onRunningTaskChanged(RunningTaskInfo taskInfo) {
         // Find the task from the list of running tasks, if it exists
-        for (ActivityManager.RunningTaskInfo existingTask : mRunningTasks) {
+        for (RunningTaskInfo existingTask : mRunningTasks) {
             if (existingTask.taskId != taskInfo.taskId) continue;
 
             mRunningTasks.remove(existingTask);
@@ -304,7 +316,7 @@
     @VisibleForTesting
     TaskLoadResult loadTasksInBackground(int numTasks, int requestId, boolean loadKeysOnly) {
         int currentUserId = Process.myUserHandle().getIdentifier();
-        ArrayList<GroupedRecentTaskInfo> rawTasks;
+        ArrayList<GroupedTaskInfo> rawTasks;
         try {
             rawTasks = mSysUiProxy.getRecentTasks(numTasks, currentUserId);
         } catch (SystemUiProxy.GetRecentTasksException e) {
@@ -327,7 +339,7 @@
         TaskLoadResult allTasks = new TaskLoadResult(requestId, loadKeysOnly, rawTasks.size());
 
         int numVisibleTasks = 0;
-        for (GroupedRecentTaskInfo rawTask : rawTasks) {
+        for (GroupedTaskInfo rawTask : rawTasks) {
             if (rawTask.getType() == TYPE_FREEFORM) {
                 // TYPE_FREEFORM tasks is only created when desktop mode can be entered,
                 // leftover TYPE_FREEFORM tasks created when flag was on should be ignored.
@@ -339,14 +351,13 @@
                 }
                 continue;
             }
-            ActivityManager.RecentTaskInfo taskInfo1 = rawTask.getTaskInfo1();
-            ActivityManager.RecentTaskInfo taskInfo2 = rawTask.getTaskInfo2();
+            TaskInfo taskInfo1 = rawTask.getTaskInfo1();
+            TaskInfo taskInfo2 = rawTask.getTaskInfo2();
             Task.TaskKey task1Key = new Task.TaskKey(taskInfo1);
             Task task1 = loadKeysOnly
                     ? new Task(task1Key)
                     : Task.from(task1Key, taskInfo1,
                             tmpLockedUsers.get(task1Key.userId) /* isLocked */);
-            task1.setLastSnapshotData(taskInfo1);
             Task task2 = null;
             if (taskInfo2 != null) {
                 // Is split task
@@ -355,7 +366,6 @@
                         ? new Task(task2Key)
                         : Task.from(task2Key, taskInfo2,
                                 tmpLockedUsers.get(task2Key.userId) /* isLocked */);
-                task2.setLastSnapshotData(taskInfo2);
             } else {
                 // Is fullscreen task
                 if (numVisibleTasks > 0) {
@@ -379,17 +389,16 @@
         return allTasks;
     }
 
-    private @Nullable DesktopTask createDesktopTask(GroupedRecentTaskInfo recentTaskInfo) {
+    private @Nullable DesktopTask createDesktopTask(GroupedTaskInfo recentTaskInfo) {
         ArrayList<Task> tasks = new ArrayList<>(recentTaskInfo.getTaskInfoList().size());
         int[] minimizedTaskIds = recentTaskInfo.getMinimizedTaskIds();
         if (minimizedTaskIds.length == recentTaskInfo.getTaskInfoList().size()) {
             // All Tasks are minimized -> don't create a DesktopTask
             return null;
         }
-        for (ActivityManager.RecentTaskInfo taskInfo : recentTaskInfo.getTaskInfoList()) {
+        for (TaskInfo taskInfo : recentTaskInfo.getTaskInfoList()) {
             Task.TaskKey key = new Task.TaskKey(taskInfo);
             Task task = Task.from(key, taskInfo, false);
-            task.setLastSnapshotData(taskInfo);
             task.positionInParent = taskInfo.positionInParent;
             task.appBounds = taskInfo.configuration.windowConfiguration.getAppBounds();
             task.isVisible = taskInfo.isVisible;
@@ -424,14 +433,14 @@
         }
         writer.println(prefix + "  ]");
         int currentUserId = Process.myUserHandle().getIdentifier();
-        ArrayList<GroupedRecentTaskInfo> rawTasks;
+        ArrayList<GroupedTaskInfo> rawTasks;
         try {
             rawTasks = mSysUiProxy.getRecentTasks(Integer.MAX_VALUE, currentUserId);
         } catch (SystemUiProxy.GetRecentTasksException e) {
             rawTasks = new ArrayList<>();
         }
         writer.println(prefix + "  rawTasks=[");
-        for (GroupedRecentTaskInfo task : rawTasks) {
+        for (GroupedTaskInfo task : rawTasks) {
             TaskInfo taskInfo1 = task.getTaskInfo1();
             TaskInfo taskInfo2 = task.getTaskInfo2();
             ComponentName cn1 = taskInfo1.topActivity;
diff --git a/quickstep/src/com/android/quickstep/RecentsActivity.java b/quickstep/src/com/android/quickstep/RecentsActivity.java
index 9c60693..6075294 100644
--- a/quickstep/src/com/android/quickstep/RecentsActivity.java
+++ b/quickstep/src/com/android/quickstep/RecentsActivity.java
@@ -70,8 +70,9 @@
 import com.android.launcher3.statemanager.StatefulActivity;
 import com.android.launcher3.taskbar.FallbackTaskbarUIController;
 import com.android.launcher3.taskbar.TaskbarManager;
+import com.android.launcher3.taskbar.TaskbarUIController;
 import com.android.launcher3.util.ActivityOptionsWrapper;
-import com.android.launcher3.util.ActivityTracker;
+import com.android.launcher3.util.ContextTracker;
 import com.android.launcher3.util.RunnableList;
 import com.android.launcher3.util.SystemUiController;
 import com.android.launcher3.util.Themes;
@@ -102,8 +103,8 @@
         RecentsViewContainer {
     private static final String TAG = "RecentsActivity";
 
-    public static final ActivityTracker<RecentsActivity> ACTIVITY_TRACKER =
-            new ActivityTracker<>();
+    public static final ContextTracker.ActivityTracker<RecentsActivity> ACTIVITY_TRACKER =
+            new ContextTracker.ActivityTracker<>();
 
     private Handler mUiHandler = new Handler(Looper.getMainLooper());
 
@@ -115,16 +116,13 @@
     private FallbackRecentsView mFallbackRecentsView;
     private OverviewActionsView<?> mActionsView;
     private TISBindHelper mTISBindHelper;
-    private @Nullable FallbackTaskbarUIController mTaskbarUIController;
+    private @Nullable FallbackTaskbarUIController<RecentsActivity> mTaskbarUIController;
 
     private StateManager<RecentsState, RecentsActivity> mStateManager;
 
     // Strong refs to runners which are cleared when the activity is destroyed
     private RemoteAnimationFactory mActivityLaunchAnimationRunner;
 
-    // For handling degenerate cases where starting an activity doesn't actually trigger the remote
-    // animation callback
-    private final Handler mHandler = new Handler();
     private final Runnable mAnimationStartTimeoutRunnable = this::onAnimationStartTimeout;
     private SplitSelectStateController mSplitSelectStateController;
     @Nullable
@@ -137,7 +135,7 @@
         SystemUiProxy systemUiProxy = SystemUiProxy.INSTANCE.get(this);
         // SplitSelectStateController needs to be created before setContentView()
         mSplitSelectStateController =
-                new SplitSelectStateController(this, mHandler, getStateManager(),
+                new SplitSelectStateController(this, getStateManager(),
                         null /* depthController */, getStatsLogManager(),
                         systemUiProxy, RecentsModel.INSTANCE.get(this),
                         null /*activityBackCallback*/);
@@ -177,11 +175,14 @@
         mTISBindHelper.runOnBindToTouchInteractionService(r);
     }
 
-    public void setTaskbarUIController(FallbackTaskbarUIController taskbarUIController) {
-        mTaskbarUIController = taskbarUIController;
+    @Override
+    public void setTaskbarUIController(@Nullable TaskbarUIController taskbarUIController) {
+        mTaskbarUIController = (FallbackTaskbarUIController<RecentsActivity>) taskbarUIController;
     }
 
-    public FallbackTaskbarUIController getTaskbarUIController() {
+    @Nullable
+    @Override
+    public FallbackTaskbarUIController<RecentsActivity> getTaskbarUIController() {
         return mTaskbarUIController;
     }
 
@@ -198,7 +199,8 @@
     }
 
     @Override
-    protected void onHandleConfigurationChanged() {
+    public void onHandleConfigurationChanged() {
+        Trace.instant(Trace.TRACE_TAG_APP, "recentsActivity_onHandleConfigurationChanged");
         initDeviceProfile();
 
         AbstractFloatingView.closeOpenViews(this, true,
@@ -352,7 +354,6 @@
     @Override
     protected void onStop() {
         super.onStop();
-
         // Workaround for b/78520668, explicitly trim memory once UI is hidden
         onTrimMemory(TRIM_MEMORY_UI_HIDDEN);
         mFallbackRecentsView.updateLocusId();
@@ -425,7 +426,7 @@
     @Override
     protected void onDestroy() {
         super.onDestroy();
-        ACTIVITY_TRACKER.onActivityDestroyed(this);
+        ACTIVITY_TRACKER.onContextDestroyed(this);
         mActivityLaunchAnimationRunner = null;
         mSplitSelectStateController.onDestroy();
         mTISBindHelper.onDestroy();
@@ -518,6 +519,7 @@
     }
 
     @NonNull
+    @Override
     public TISBindHelper getTISBindHelper() {
         return mTISBindHelper;
     }
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java b/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java
index 0c5806b..8fc1a78 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java
@@ -15,13 +15,12 @@
  */
 package com.android.quickstep;
 
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
 import static android.view.RemoteAnimationTarget.MODE_CLOSING;
+import static android.view.RemoteAnimationTarget.MODE_OPENING;
 import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER;
 
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.ON_CANCEL_RECENTS_ANIMATION;
-import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.ON_FINISH_RECENTS_ANIMATION;
-import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.ON_START_RECENTS_ANIMATION;
 
 import android.graphics.Rect;
 import android.os.Bundle;
@@ -32,10 +31,10 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.UiThread;
 
+import com.android.launcher3.Flags;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.util.Preconditions;
-import com.android.quickstep.util.ActiveGestureErrorDetector;
-import com.android.quickstep.util.ActiveGestureLog;
+import com.android.quickstep.util.ActiveGestureProtoLogProxy;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.system.RecentsAnimationControllerCompat;
 
@@ -106,12 +105,14 @@
         long appCount = Arrays.stream(appTargets)
                 .filter(app -> app.mode == MODE_CLOSING)
                 .count();
-        if (appCount == 0) {
+
+        boolean isOpeningHome = Arrays.stream(appTargets).filter(app -> app.mode == MODE_OPENING
+                        && app.windowConfiguration.getActivityType() == ACTIVITY_TYPE_HOME)
+                .count() > 0;
+        if (appCount == 0 && (!(Flags.enableFallbackOverviewInWindow()
+                || Flags.enableLauncherOverviewInWindow()) || isOpeningHome)) {
+            ActiveGestureProtoLogProxy.logOnRecentsAnimationStartCancelled();
             // Edge case, if there are no closing app targets, then Launcher has nothing to handle
-            ActiveGestureLog.INSTANCE.addLog(
-                    /* event= */ "RecentsAnimationCallbacks.onAnimationStart (canceled)",
-                    /* extras= */ 0,
-                    /* gestureEvent= */ ON_START_RECENTS_ANIMATION);
             notifyAnimationCanceled();
             animationController.finish(false /* toHome */, false /* sendUserLeaveHint */,
                     null /* finishCb */);
@@ -138,10 +139,7 @@
                     extras);
 
             Utilities.postAsyncCallback(MAIN_EXECUTOR.getHandler(), () -> {
-                ActiveGestureLog.INSTANCE.addLog(
-                        /* event= */ "RecentsAnimationCallbacks.onAnimationStart",
-                        /* extras= */ targets.apps.length,
-                        /* gestureEvent= */ ON_START_RECENTS_ANIMATION);
+                ActiveGestureProtoLogProxy.logOnRecentsAnimationStart(targets.apps.length);
                 for (RecentsAnimationListener listener : getListeners()) {
                     listener.onRecentsAnimationStart(mController, targets);
                 }
@@ -153,9 +151,7 @@
     @Override
     public final void onAnimationCanceled(HashMap<Integer, ThumbnailData> thumbnailDatas) {
         Utilities.postAsyncCallback(MAIN_EXECUTOR.getHandler(), () -> {
-            ActiveGestureLog.INSTANCE.addLog(
-                    /* event= */ "RecentsAnimationCallbacks.onAnimationCanceled",
-                    /* gestureEvent= */ ON_CANCEL_RECENTS_ANIMATION);
+            ActiveGestureProtoLogProxy.logRecentsAnimationCallbacksOnAnimationCancelled();
             for (RecentsAnimationListener listener : getListeners()) {
                 listener.onRecentsAnimationCanceled(thumbnailDatas);
             }
@@ -166,8 +162,7 @@
     @Override
     public void onTasksAppeared(RemoteAnimationTarget[] apps) {
         Utilities.postAsyncCallback(MAIN_EXECUTOR.getHandler(), () -> {
-            ActiveGestureLog.INSTANCE.addLog("RecentsAnimationCallbacks.onTasksAppeared",
-                    ActiveGestureErrorDetector.GestureEvent.TASK_APPEARED);
+            ActiveGestureProtoLogProxy.logRecentsAnimationCallbacksOnTasksAppeared();
             for (RecentsAnimationListener listener : getListeners()) {
                 listener.onTasksAppeared(apps);
             }
@@ -176,9 +171,7 @@
 
     private void onAnimationFinished(RecentsAnimationController controller) {
         Utilities.postAsyncCallback(MAIN_EXECUTOR.getHandler(), () -> {
-            ActiveGestureLog.INSTANCE.addLog(
-                    /* event= */ "RecentsAnimationCallbacks.onAnimationFinished",
-                    ON_FINISH_RECENTS_ANIMATION);
+            ActiveGestureProtoLogProxy.logAbsSwipeUpHandlerOnRecentsAnimationFinished();
             for (RecentsAnimationListener listener : getListeners()) {
                 listener.onRecentsAnimationFinished(controller);
             }
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationController.java b/quickstep/src/com/android/quickstep/RecentsAnimationController.java
index 190d526..dcb0108 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationController.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationController.java
@@ -17,7 +17,6 @@
 
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
-import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.FINISH_RECENTS_ANIMATION;
 
 import android.os.Bundle;
 import android.os.RemoteException;
@@ -32,7 +31,7 @@
 import com.android.internal.os.IResultReceiver;
 import com.android.launcher3.util.Preconditions;
 import com.android.launcher3.util.RunnableList;
-import com.android.quickstep.util.ActiveGestureLog;
+import com.android.quickstep.util.ActiveGestureProtoLogProxy;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
 import com.android.systemui.shared.system.RecentsAnimationControllerCompat;
@@ -132,10 +131,7 @@
             // trigger the callback to be called immediately
             return;
         }
-        ActiveGestureLog.INSTANCE.addLog(
-                /* event= */ "finishRecentsAnimation",
-                /* extras= */ toRecents,
-                /* gestureEvent= */ FINISH_RECENTS_ANIMATION);
+        ActiveGestureProtoLogProxy.logFinishRecentsAnimation(toRecents);
         // Finish not yet requested
         mFinishRequested = true;
         mFinishTargetIsLauncher = toRecents;
@@ -144,7 +140,7 @@
             mController.finish(toRecents, sendUserLeaveHint, new IResultReceiver.Stub() {
                 @Override
                 public void send(int i, Bundle bundle) throws RemoteException {
-                    ActiveGestureLog.INSTANCE.addLog("finishRecentsAnimation-callback");
+                    ActiveGestureProtoLogProxy.logFinishRecentsAnimationCallback();
                     MAIN_EXECUTOR.execute(() -> {
                         mPendingFinishCallbacks.executeAllAndDestroy();
                     });
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
index 5131774..e296449 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
@@ -19,6 +19,7 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
 import static android.view.Display.DEFAULT_DISPLAY;
 
+import static com.android.launcher3.MotionEventsUtils.isTrackpadScroll;
 import static com.android.launcher3.util.DisplayController.CHANGE_ALL;
 import static com.android.launcher3.util.DisplayController.CHANGE_NAVIGATION_MODE;
 import static com.android.launcher3.util.DisplayController.CHANGE_ROTATION;
@@ -70,7 +71,7 @@
 import com.android.launcher3.util.SettingsCache;
 import com.android.quickstep.TopTaskTracker.CachedTaskInfo;
 import com.android.quickstep.util.ActiveGestureLog;
-import com.android.quickstep.util.AssistStateManager;
+import com.android.quickstep.util.ContextualSearchStateManager;
 import com.android.quickstep.util.GestureExclusionManager;
 import com.android.quickstep.util.GestureExclusionManager.ExclusionListener;
 import com.android.quickstep.util.NavBarPosition;
@@ -101,7 +102,7 @@
     private final DisplayController mDisplayController;
 
     private final GestureExclusionManager mExclusionManager;
-    private final AssistStateManager mAssistStateManager;
+    private final ContextualSearchStateManager mContextualSearchStateManager;
 
     private final RotationTouchHelper mRotationTouchHelper;
     private final TaskStackChangeListener mPipListener;
@@ -152,7 +153,7 @@
         mContext = context;
         mDisplayController = DisplayController.INSTANCE.get(context);
         mExclusionManager = exclusionManager;
-        mAssistStateManager = AssistStateManager.INSTANCE.get(context);
+        mContextualSearchStateManager = ContextualSearchStateManager.INSTANCE.get(context);
         mIsOneHandedModeSupported = SystemProperties.getBoolean(SUPPORT_ONE_HANDED_MODE, false);
         mRotationTouchHelper = RotationTouchHelper.INSTANCE.get(context);
         if (isInstanceForTouches) {
@@ -353,7 +354,11 @@
      * @return whether the given running task info matches the gesture-blocked task.
      */
     public boolean isGestureBlockedTask(CachedTaskInfo taskInfo) {
-        return taskInfo != null && taskInfo.getTaskId() == mGestureBlockingTaskId;
+        if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+            return taskInfo != null && taskInfo.topGroupedTaskContainsTask(mGestureBlockingTaskId);
+        } else {
+            return taskInfo != null && taskInfo.getTaskId() == mGestureBlockingTaskId;
+        }
     }
 
     /**
@@ -563,6 +568,7 @@
         return mAssistantAvailable
                 && !QuickStepContract.isAssistantGestureDisabled(mSystemUiStateFlags)
                 && mRotationTouchHelper.touchInAssistantRegion(ev)
+                && !isTrackpadScroll(ev)
                 && !isLockToAppActive();
     }
 
@@ -617,8 +623,9 @@
                 : QUICKSTEP_TOUCH_SLOP_RATIO_TWO_BUTTON;
         float touchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop();
 
-        if (mAssistStateManager.getLPNHCustomSlopMultiplier().isPresent()) {
-            float customSlopMultiplier = mAssistStateManager.getLPNHCustomSlopMultiplier().get();
+        if (mContextualSearchStateManager.getLPNHCustomSlopMultiplier().isPresent()) {
+            float customSlopMultiplier =
+                    mContextualSearchStateManager.getLPNHCustomSlopMultiplier().get();
             return customSlopMultiplier * slopMultiplier * touchSlop;
         } else {
             return slopMultiplier * touchSlop;
diff --git a/quickstep/src/com/android/quickstep/RemoteTargetGluer.java b/quickstep/src/com/android/quickstep/RemoteTargetGluer.java
index 8adc11a..06b2972 100644
--- a/quickstep/src/com/android/quickstep/RemoteTargetGluer.java
+++ b/quickstep/src/com/android/quickstep/RemoteTargetGluer.java
@@ -94,7 +94,7 @@
         RemoteTargetHandle[] handles = new RemoteTargetHandle[numHandles];
         for (int i = 0; i < numHandles; i++) {
             TaskViewSimulator tvs = new TaskViewSimulator(context, sizingStrategy);
-            tvs.setIsDesktopTask(forDesktop);
+            tvs.setIsDesktopTask(forDesktop , i);
             TransformParams transformParams = new TransformParams();
             handles[i] = new RemoteTargetHandle(tvs, transformParams);
         }
diff --git a/quickstep/src/com/android/quickstep/RotationTouchHelper.java b/quickstep/src/com/android/quickstep/RotationTouchHelper.java
index 80c07196..79abc0f 100644
--- a/quickstep/src/com/android/quickstep/RotationTouchHelper.java
+++ b/quickstep/src/com/android/quickstep/RotationTouchHelper.java
@@ -180,6 +180,7 @@
                 }
             }
         };
+        runOnDestroy(() -> mOrientationListener.disable());
         mNeedsInit = false;
     }
 
@@ -212,6 +213,7 @@
             r.run();
         }
         mNeedsInit = true;
+        mOnDestroyActions.clear();
     }
 
     public boolean isTaskListFrozen() {
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index f9b4dab..6c4c74c 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.java
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -21,7 +21,6 @@
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 import static com.android.launcher3.util.SplitConfigurationOptions.StagePosition;
-import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.RECENT_TASKS_MISSING;
 import static com.android.quickstep.util.LogUtils.splitFailureMessage;
 
 import android.app.ActivityManager;
@@ -44,14 +43,15 @@
 import android.os.UserHandle;
 import android.util.Log;
 import android.view.IRemoteAnimationRunner;
+import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.RemoteAnimationTarget;
 import android.view.SurfaceControl;
+import android.window.DesktopModeFlags;
 import android.window.IOnBackInvokedCallback;
 import android.window.RemoteTransition;
 import android.window.TaskSnapshot;
 import android.window.TransitionFilter;
-import android.window.flags.DesktopModeFlags;
 
 import androidx.annotation.MainThread;
 import androidx.annotation.Nullable;
@@ -60,11 +60,13 @@
 import com.android.internal.logging.InstanceId;
 import com.android.internal.util.ScreenshotRequest;
 import com.android.internal.view.AppearanceRegion;
-import com.android.launcher3.util.MainThreadInitializedObject;
+import com.android.launcher3.dagger.ApplicationContext;
+import com.android.launcher3.dagger.LauncherAppSingleton;
+import com.android.launcher3.util.DaggerSingletonObject;
 import com.android.launcher3.util.Preconditions;
-import com.android.launcher3.util.SafeCloseable;
-import com.android.quickstep.util.ActiveGestureLog;
-import com.android.quickstep.util.AssistUtils;
+import com.android.quickstep.dagger.QuickstepBaseAppComponent;
+import com.android.quickstep.util.ActiveGestureProtoLogProxy;
+import com.android.quickstep.util.ContextualSearchInvoker;
 import com.android.quickstep.util.unfold.ProxyUnfoldTransitionProvider;
 import com.android.systemui.shared.recents.ISystemUiProxy;
 import com.android.systemui.shared.recents.model.ThumbnailData;
@@ -90,7 +92,7 @@
 import com.android.wm.shell.recents.IRecentTasksListener;
 import com.android.wm.shell.recents.IRecentsAnimationController;
 import com.android.wm.shell.recents.IRecentsAnimationRunner;
-import com.android.wm.shell.shared.GroupedRecentTaskInfo;
+import com.android.wm.shell.shared.GroupedTaskInfo;
 import com.android.wm.shell.shared.IShellTransitions;
 import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
@@ -109,14 +111,17 @@
 import java.util.LinkedHashMap;
 import java.util.List;
 
+import javax.inject.Inject;
+
 /**
  * Holds the reference to SystemUI.
  */
-public class SystemUiProxy implements ISystemUiProxy, NavHandle, SafeCloseable {
+@LauncherAppSingleton
+public class SystemUiProxy implements ISystemUiProxy, NavHandle {
     private static final String TAG = "SystemUiProxy";
 
-    public static final MainThreadInitializedObject<SystemUiProxy> INSTANCE =
-            new MainThreadInitializedObject<>(SystemUiProxy::new);
+    public static final DaggerSingletonObject<SystemUiProxy> INSTANCE =
+            new DaggerSingletonObject<>(QuickstepBaseAppComponent::getSystemUiProxy);
 
     private static final int MSG_SET_SHELF_HEIGHT = 1;
     private static final int MSG_SET_LAUNCHER_KEEP_CLEAR_AREA_HEIGHT = 2;
@@ -161,6 +166,7 @@
     private IRemoteAnimationRunner mBackToLauncherRunner;
     private IDragAndDrop mDragAndDrop;
     private final HomeVisibilityState mHomeVisibilityState = new HomeVisibilityState();
+    private final FocusState mFocusState = new FocusState();
 
     // Used to dedupe calls to SystemUI
     private int mLastShelfHeight;
@@ -187,7 +193,8 @@
     @Nullable
     private final ProxyUnfoldTransitionProvider mUnfoldTransitionProvider;
 
-    private SystemUiProxy(Context context) {
+    @Inject
+    public SystemUiProxy(@ApplicationContext Context context) {
         mContext = context;
         mAsyncHandler = new Handler(UI_HELPER_EXECUTOR.getLooper(), this::handleMessageAsync);
         final Intent baseIntent = new Intent().setPackage(mContext.getPackageName());
@@ -204,13 +211,10 @@
     }
 
     @Override
-    public void close() { }
-
-    @Override
-    public void onBackPressed() {
+    public void onBackEvent(KeyEvent backEvent) {
         if (mSystemUiProxy != null) {
             try {
-                mSystemUiProxy.onBackPressed();
+                mSystemUiProxy.onBackEvent(backEvent);
             } catch (RemoteException e) {
                 Log.w(TAG, "Failed call onBackPressed", e);
             }
@@ -299,6 +303,7 @@
         registerSplitScreenListener(mSplitScreenListener);
         registerSplitSelectListener(mSplitSelectListener);
         mHomeVisibilityState.init(mShellTransitions);
+        mFocusState.init(mShellTransitions);
         setStartingWindowListener(mStartingWindowListener);
         setLauncherUnlockAnimationController(
                 mLauncherActivityClass, mLauncherUnlockAnimationController);
@@ -308,8 +313,8 @@
         setBackToLauncherCallback(mBackToLauncherCallback, mBackToLauncherRunner);
         setUnfoldAnimationListener(mUnfoldAnimationListener);
         setDesktopTaskListener(mDesktopTaskListener);
-        setAssistantOverridesRequested(
-                AssistUtils.newInstance(mContext).getSysUiAssistOverrideInvocationTypes());
+        setAssistantOverridesRequested(ContextualSearchInvoker.newInstance(mContext)
+                .getSysUiAssistOverrideInvocationTypes());
         mStateChangeCallbacks.forEach(Runnable::run);
 
         if (mUnfoldTransitionProvider != null) {
@@ -881,10 +886,12 @@
     /**
      * Tells SysUI to update the bubble bar location to the new location.
      * @param location new location for the bubble bar
+     * @param source what triggered the location update
      */
-    public void setBubbleBarLocation(BubbleBarLocation location) {
+    public void setBubbleBarLocation(BubbleBarLocation location,
+            @BubbleBarLocation.UpdateSource int source) {
         try {
-            mBubbles.setBubbleBarLocation(location);
+            mBubbles.setBubbleBarLocation(location, source);
         } catch (RemoteException e) {
             Log.w(TAG, "Failed call setBubbleBarLocation");
         }
@@ -1076,16 +1083,6 @@
         }
     }
 
-    public void removeFromSideStage(int taskId) {
-        if (mSplitScreen != null) {
-            try {
-                mSplitScreen.removeFromSideStage(taskId);
-            } catch (RemoteException e) {
-                Log.w(TAG, "Failed call removeFromSideStage");
-            }
-        }
-    }
-
     //
     // One handed
     //
@@ -1143,6 +1140,10 @@
         return mHomeVisibilityState;
     }
 
+    public FocusState getFocusState() {
+        return mFocusState;
+    }
+
     /**
      * Returns a surface which can be used to attach overlays to home task or null if
      * the task doesn't exist or sysui is not connected
@@ -1360,15 +1361,15 @@
      * @throws GetRecentTasksException if IRecentTasks is not initialized, or when we get
      * RemoteException from server side
      */
-    public ArrayList<GroupedRecentTaskInfo> getRecentTasks(int numTasks, int userId)
-            throws GetRecentTasksException {
+    public ArrayList<GroupedTaskInfo> getRecentTasks(int numTasks,
+            int userId) throws GetRecentTasksException {
         if (mRecentTasks == null) {
             Log.e(TAG, "getRecentTasks() failed due to null mRecentTasks");
             throw new GetRecentTasksException("null mRecentTasks");
         }
         try {
-            final GroupedRecentTaskInfo[] rawTasks = mRecentTasks.getRecentTasks(numTasks,
-                    RECENT_IGNORE_UNAVAILABLE, userId);
+            final GroupedTaskInfo[] rawTasks =
+                    mRecentTasks.getRecentTasks(numTasks, RECENT_IGNORE_UNAVAILABLE, userId);
             if (rawTasks == null) {
                 return new ArrayList<>();
             }
@@ -1429,10 +1430,10 @@
     /**
      * If task with the given id is on the desktop, bring it to front
      */
-    public void showDesktopApp(int taskId) {
+    public void showDesktopApp(int taskId, @Nullable RemoteTransition transition) {
         if (mDesktopMode != null) {
             try {
-                mDesktopMode.showDesktopApp(taskId);
+                mDesktopMode.showDesktopApp(taskId, transition);
             } catch (RemoteException e) {
                 Log.w(TAG, "Failed call showDesktopApp", e);
             }
@@ -1485,6 +1486,28 @@
         }
     }
 
+    /** Call shell to remove the desktop that is on given `displayId` */
+    public void removeDesktop(int displayId) {
+        if (mDesktopMode != null) {
+            try {
+                mDesktopMode.removeDesktop(displayId);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call removeDesktop", e);
+            }
+        }
+    }
+
+    /** Call shell to move a task with given `taskId` to external display. */
+    public void moveToExternalDisplay(int taskId) {
+        if (mDesktopMode != null) {
+            try {
+                mDesktopMode.moveToExternalDisplay(taskId);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call moveToExternalDisplay", e);
+            }
+        }
+    }
+
     //
     // Unfold transition
     //
@@ -1516,9 +1539,9 @@
      * Starts the recents activity. The caller should manage the thread on which this is called.
      */
     public boolean startRecentsActivity(Intent intent, ActivityOptions options,
-            RecentsAnimationListener listener) {
+            RecentsAnimationListener listener, boolean useSyntheticRecentsTransition) {
         if (mRecentTasks == null) {
-            ActiveGestureLog.INSTANCE.addLog("Null mRecentTasks", RECENT_TASKS_MISSING);
+            ActiveGestureProtoLogProxy.logRecentTasksMissing();
             return false;
         }
         final IRecentsAnimationRunner runner = new IRecentsAnimationRunner.Stub() {
@@ -1547,6 +1570,9 @@
             }
         };
         final Bundle optsBundle = options.toBundle();
+        if (useSyntheticRecentsTransition) {
+            optsBundle.putBoolean("is_synthetic_recents_transition", true);
+        }
         try {
             mRecentTasks.startRecentsTransition(mRecentsPendingIntent, intent, optsBundle,
                     mContext.getIApplicationThread(), runner);
@@ -1591,6 +1617,7 @@
         pw.println("\tmOneHanded=" + mOneHanded);
         pw.println("\tmShellTransitions=" + mShellTransitions);
         pw.println("\tmHomeVisibilityState=" + mHomeVisibilityState);
+        pw.println("\tmFocusState=" + mFocusState);
         pw.println("\tmStartingWindow=" + mStartingWindow);
         pw.println("\tmStartingWindowListener=" + mStartingWindowListener);
         pw.println("\tmSysuiUnlockAnimationController=" + mSysuiUnlockAnimationController);
diff --git a/quickstep/src/com/android/quickstep/TaskAnimationManager.java b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
index 289a2c1..0ea128a 100644
--- a/quickstep/src/com/android/quickstep/TaskAnimationManager.java
+++ b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
@@ -26,7 +26,6 @@
 import static com.android.quickstep.GestureState.STATE_END_TARGET_ANIMATION_FINISHED;
 import static com.android.quickstep.GestureState.STATE_RECENTS_ANIMATION_INITIALIZED;
 import static com.android.quickstep.GestureState.STATE_RECENTS_ANIMATION_STARTED;
-import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.START_RECENTS_ANIMATION;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED;
 
@@ -43,11 +42,13 @@
 import androidx.annotation.UiThread;
 
 import com.android.internal.util.ArrayUtils;
+import com.android.launcher3.Flags;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.taskbar.TaskbarUIController;
 import com.android.launcher3.util.DisplayController;
-import com.android.quickstep.util.ActiveGestureLog;
+import com.android.quickstep.fallback.window.RecentsWindowManager;
+import com.android.quickstep.util.ActiveGestureProtoLogProxy;
 import com.android.quickstep.util.SystemUiFlagUtils;
 import com.android.quickstep.views.RecentsView;
 import com.android.systemui.shared.recents.model.ThumbnailData;
@@ -64,6 +65,7 @@
             SystemProperties.getBoolean("persist.wm.debug.shell_transit_rotate", false);
 
     private final Context mCtx;
+    private RecentsWindowManager mRecentsWindowsManager;
     private RecentsAnimationController mController;
     private RecentsAnimationCallbacks mCallbacks;
     private RecentsAnimationTargets mTargets;
@@ -98,10 +100,10 @@
         }
     };
 
-    TaskAnimationManager(Context ctx) {
+    TaskAnimationManager(Context ctx, RecentsWindowManager manager) {
         mCtx = ctx;
+        mRecentsWindowsManager = manager;
     }
-
     SystemUiProxy getSystemUiProxy() {
         return SystemUiProxy.INSTANCE.get(mCtx);
     }
@@ -134,9 +136,7 @@
     @UiThread
     public RecentsAnimationCallbacks startRecentsAnimation(@NonNull GestureState gestureState,
             Intent intent, RecentsAnimationCallbacks.RecentsAnimationListener listener) {
-        ActiveGestureLog.INSTANCE.addLog(
-                /* event= */ "startRecentsAnimation",
-                /* gestureEvent= */ START_RECENTS_ANIMATION);
+        ActiveGestureProtoLogProxy.logStartRecentsAnimation();
         // Notify if recents animation is still running
         if (mController != null) {
             String msg = "New recents animation started before old animation completed";
@@ -166,9 +166,8 @@
             public void onRecentsAnimationStart(RecentsAnimationController controller,
                     RecentsAnimationTargets targets) {
                 if (enableHandleDelayedGestureCallbacks() && mRecentsAnimationStartPending) {
-                    ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
-                            "TaskAnimationManager.startRecentsAnimation(onRecentsAnimationStart): ")
-                            .append("Setting mRecentsAnimationStartPending = false"));
+                    ActiveGestureProtoLogProxy.logStartRecentsAnimationCallback(
+                            "onRecentsAnimationStart");
                     mRecentsAnimationStartPending = false;
                 }
                 if (mCallbacks == null) {
@@ -202,8 +201,7 @@
                         // Only finish if the end target is RECENTS. Otherwise, if the target is
                         // NEW_TASK, startActivityFromRecents will be skipped.
                         if (mLastGestureState.getEndTarget() == RECENTS) {
-                            finishRunningRecentsAnimation(false /* toHome */,
-                                    true /* forceFinish */, null /* forceFinishCb */);
+                            finishRunningRecentsAnimation(false /* toHome */);
                         }
                     });
                 }
@@ -212,10 +210,8 @@
             @Override
             public void onRecentsAnimationCanceled(HashMap<Integer, ThumbnailData> thumbnailDatas) {
                 if (enableHandleDelayedGestureCallbacks() && mRecentsAnimationStartPending) {
-                    ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
-                            "TaskAnimationManager.startRecentsAnimation")
-                            .append("(onRecentsAnimationCanceled): ")
-                            .append("Setting mRecentsAnimationStartPending = false"));
+                    ActiveGestureProtoLogProxy.logStartRecentsAnimationCallback(
+                            "onRecentsAnimationCanceled");
                     mRecentsAnimationStartPending = false;
                 }
                 cleanUpRecentsAnimation(newCallbacks);
@@ -224,10 +220,8 @@
             @Override
             public void onRecentsAnimationFinished(RecentsAnimationController controller) {
                 if (enableHandleDelayedGestureCallbacks() && mRecentsAnimationStartPending) {
-                    ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
-                            "TaskAnimationManager.startRecentsAnimation")
-                            .append("(onRecentsAnimationFinished): ")
-                            .append("Setting mRecentsAnimationStartPending = false"));
+                    ActiveGestureProtoLogProxy.logStartRecentsAnimationCallback(
+                            "onRecentsAnimationFinished");
                     mRecentsAnimationStartPending = false;
                 }
                 cleanUpRecentsAnimation(newCallbacks);
@@ -273,16 +267,14 @@
                     RecentsView recentsView =
                             containerInterface.getCreatedContainer().getOverviewPanel();
                     if (recentsView != null) {
-                        ActiveGestureLog.INSTANCE.addLog(
-                                new ActiveGestureLog.CompoundString("Launching side task id=")
-                                        .append(appearedTaskTarget.taskId));
+                        ActiveGestureProtoLogProxy.logLaunchingSideTask(appearedTaskTarget.taskId);
                         recentsView.launchSideTaskInLiveTileMode(appearedTaskTarget.taskId,
                                 appearedTaskTargets,
                                 new RemoteAnimationTarget[0] /* wallpaper */,
                                 nonAppTargets /* nonApps */);
                         return;
                     } else {
-                        ActiveGestureLog.INSTANCE.addLog("Unable to launch side task (no recents)");
+                        ActiveGestureProtoLogProxy.logLaunchingSideTaskFailed();
                     }
                 } else if (nonAppTargets.length > 0) {
                     TaskViewUtils.createSplitAuxiliarySurfacesAnimator(nonAppTargets /* nonApps */,
@@ -299,39 +291,48 @@
         mCallbacks.addListener(listener);
 
         final ActivityOptions options = ActivityOptions.makeBasic();
-        options.setPendingIntentBackgroundActivityStartMode(
-                ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS);
-        options.setTransientLaunch();
-        options.setSourceInfo(ActivityOptions.SourceInfo.TYPE_RECENTS_ANIMATION, eventTime);
 
-        // Notify taskbar that we should skip reacting to launcher visibility change to
-        // avoid a jumping taskbar.
-        TaskbarUIController taskbarUIController = containerInterface.getTaskbarController();
-        if (enableScalingRevealHomeAnimation() && taskbarUIController != null) {
-            taskbarUIController.setSkipLauncherVisibilityChange(true);
+        // TODO:(b/365777482) if flag is enabled, but on launcher it will crash.
+        if(containerInterface.getCreatedContainer() instanceof RecentsWindowManager
+                && (Flags.enableFallbackOverviewInWindow()
+                        || Flags.enableLauncherOverviewInWindow())) {
+            mRecentsAnimationStartPending = getSystemUiProxy().startRecentsActivity(intent, options,
+                    mCallbacks, gestureState.useSyntheticRecentsTransition());
+            mRecentsWindowsManager.startRecentsWindow(mCallbacks);
+        } else {
+            options.setPendingIntentBackgroundActivityStartMode(
+                    ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS);
+            options.setTransientLaunch();
+            options.setSourceInfo(ActivityOptions.SourceInfo.TYPE_RECENTS_ANIMATION, eventTime);
 
-            mCallbacks.addListener(new RecentsAnimationCallbacks.RecentsAnimationListener() {
-                @Override
-                public void onRecentsAnimationCanceled(
-                        @NonNull HashMap<Integer, ThumbnailData> thumbnailDatas) {
-                    taskbarUIController.setSkipLauncherVisibilityChange(false);
-                }
+            // Notify taskbar that we should skip reacting to launcher visibility change to
+            // avoid a jumping taskbar.
+            TaskbarUIController taskbarUIController = containerInterface.getTaskbarController();
+            if (enableScalingRevealHomeAnimation() && taskbarUIController != null) {
+                taskbarUIController.setSkipLauncherVisibilityChange(true);
 
-                @Override
-                public void onRecentsAnimationFinished(
-                        @NonNull RecentsAnimationController controller) {
-                    taskbarUIController.setSkipLauncherVisibilityChange(false);
-                }
-            });
+                mCallbacks.addListener(new RecentsAnimationCallbacks.RecentsAnimationListener() {
+                    @Override
+                    public void onRecentsAnimationCanceled(
+                            @NonNull HashMap<Integer, ThumbnailData> thumbnailDatas) {
+                        taskbarUIController.setSkipLauncherVisibilityChange(false);
+                    }
+
+                    @Override
+                    public void onRecentsAnimationFinished(
+                            @NonNull RecentsAnimationController controller) {
+                        taskbarUIController.setSkipLauncherVisibilityChange(false);
+                    }
+                });
+            }
+
+            mRecentsAnimationStartPending = getSystemUiProxy().startRecentsActivity(intent,
+                    options, mCallbacks, false /* useSyntheticRecentsTransition */);
         }
 
-        mRecentsAnimationStartPending = getSystemUiProxy()
-                .startRecentsActivity(intent, options, mCallbacks);
         if (enableHandleDelayedGestureCallbacks()) {
-            ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
-                    "TaskAnimationManager.startRecentsAnimation: ")
-                    .append("Setting mRecentsAnimationStartPending = ")
-                    .append(mRecentsAnimationStartPending));
+            ActiveGestureProtoLogProxy.logSettingRecentsAnimationStartPending(
+                    mRecentsAnimationStartPending);
         }
         gestureState.setState(STATE_RECENTS_ANIMATION_INITIALIZED);
         return mCallbacks;
@@ -341,7 +342,7 @@
      * Continues the existing running recents animation for a new gesture.
      */
     public RecentsAnimationCallbacks continueRecentsAnimation(GestureState gestureState) {
-        ActiveGestureLog.INSTANCE.addLog(/* event= */ "continueRecentsAnimation");
+        ActiveGestureProtoLogProxy.logContinueRecentsAnimation();
         mCallbacks.removeListener(mLastGestureState);
         mLastGestureState = gestureState;
         mCallbacks.addListener(gestureState);
@@ -423,8 +424,7 @@
     public void finishRunningRecentsAnimation(boolean toHome, boolean forceFinish,
             Runnable forceFinishCb) {
         if (mController != null) {
-            ActiveGestureLog.INSTANCE.addLog(
-                    /* event= */ "finishRunningRecentsAnimation", toHome);
+            ActiveGestureProtoLogProxy.logFinishRunningRecentsAnimation(toHome);
             if (forceFinish) {
                 mController.finishController(toHome, forceFinishCb, false /* sendUserLeaveHint */,
                         true /* forceFinish */);
@@ -461,11 +461,10 @@
      */
     private void cleanUpRecentsAnimation(RecentsAnimationCallbacks targetCallbacks) {
         if (mCallbacks != targetCallbacks) {
-            ActiveGestureLog.INSTANCE.addLog(
-                    /* event= */ "cleanUpRecentsAnimation skipped due to wrong callbacks");
+            ActiveGestureProtoLogProxy.logCleanUpRecentsAnimationSkipped();
             return;
         }
-        ActiveGestureLog.INSTANCE.addLog(/* event= */ "cleanUpRecentsAnimation");
+        ActiveGestureProtoLogProxy.logCleanUpRecentsAnimation();
         if (mLiveTileCleanUpHandler != null) {
             mLiveTileCleanUpHandler.run();
             mLiveTileCleanUpHandler = null;
diff --git a/quickstep/src/com/android/quickstep/TaskIconCache.java b/quickstep/src/com/android/quickstep/TaskIconCache.java
index 1f6c02c..c4221a1 100644
--- a/quickstep/src/com/android/quickstep/TaskIconCache.java
+++ b/quickstep/src/com/android/quickstep/TaskIconCache.java
@@ -116,6 +116,10 @@
                 () -> getCacheEntry(task),
                 MAIN_EXECUTOR,
                 result -> {
+                    task.icon = result.icon;
+                    task.titleDescription = result.contentDescription;
+                    task.title = result.title;
+
                     callback.onTaskIconReceived(
                             result.icon,
                             result.contentDescription,
@@ -226,7 +230,7 @@
         synchronized (mDefaultIcons) {
             if (mDefaultIconBase == null) {
                 try (BaseIconFactory bif = getIconFactory()) {
-                    mDefaultIconBase = bif.makeDefaultIcon();
+                    mDefaultIconBase = bif.makeDefaultIcon(mIconProvider);
                 }
             }
 
diff --git a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
index f4ff4b2..0dbdcb7 100644
--- a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
+++ b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
@@ -116,6 +116,7 @@
             TaskShortcutFactory.INSTALL,
             TaskShortcutFactory.FREE_FORM,
             DesktopSystemShortcut.Companion.createFactory(),
+            ExternalDisplaySystemShortcut.Companion.createFactory(),
             TaskShortcutFactory.WELLBEING,
             TaskShortcutFactory.SAVE_APP_PAIR,
             TaskShortcutFactory.SCREENSHOT,
@@ -173,9 +174,8 @@
 
         protected T getActionsView() {
             if (mActionsView == null) {
-                mActionsView = BaseActivity.fromContext(
-                        mTaskContainer.getTaskView().getContext()).findViewById(
-                        R.id.overview_actions_view);
+                mActionsView = (T) RecentsViewContainer.containerFromContext(
+                        mTaskContainer.getTaskView().getContext()).getActionsView();
             }
             return mActionsView;
         }
diff --git a/quickstep/src/com/android/quickstep/TaskUtils.java b/quickstep/src/com/android/quickstep/TaskUtils.java
index 63e536a..49f7daf 100644
--- a/quickstep/src/com/android/quickstep/TaskUtils.java
+++ b/quickstep/src/com/android/quickstep/TaskUtils.java
@@ -31,8 +31,8 @@
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.pm.UserCache;
+import com.android.launcher3.util.ApplicationInfoWrapper;
 import com.android.launcher3.util.ComponentKey;
-import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.util.TraceHelper;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
@@ -70,8 +70,8 @@
             return "";
         }
         UserHandle user = UserHandle.of(userId);
-        ApplicationInfo applicationInfo = PackageManagerHelper.INSTANCE.get(context)
-                .getApplicationInfo(packageName, user, 0);
+        ApplicationInfo applicationInfo =
+                new ApplicationInfoWrapper(context, packageName, user).getInfo();
         if (applicationInfo == null) {
             Log.e(TAG, "Failed to get title for userId=" + userId + ", packageName=" + packageName);
             return "";
diff --git a/quickstep/src/com/android/quickstep/TaskViewUtils.java b/quickstep/src/com/android/quickstep/TaskViewUtils.java
index d8063ba..07ee479 100644
--- a/quickstep/src/com/android/quickstep/TaskViewUtils.java
+++ b/quickstep/src/com/android/quickstep/TaskViewUtils.java
@@ -59,7 +59,6 @@
 
 import com.android.app.animation.Interpolators;
 import com.android.internal.jank.Cuj;
-import com.android.launcher3.BaseActivity;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.anim.AnimatedFloat;
 import com.android.launcher3.anim.AnimationSuccessListener;
@@ -68,6 +67,7 @@
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.statehandlers.DepthController;
 import com.android.launcher3.statemanager.StateManager;
+import com.android.launcher3.statemanager.StatefulContainer;
 import com.android.launcher3.taskbar.TaskbarUIController;
 import com.android.launcher3.util.DisplayController;
 import com.android.quickstep.RemoteTargetGluer.RemoteTargetHandle;
@@ -80,6 +80,7 @@
 import com.android.quickstep.views.DesktopTaskView;
 import com.android.quickstep.views.GroupedTaskView;
 import com.android.quickstep.views.RecentsView;
+import com.android.quickstep.views.RecentsViewContainer;
 import com.android.quickstep.views.TaskView;
 import com.android.systemui.animation.RemoteAnimationTargetCompat;
 import com.android.systemui.shared.recents.model.Task;
@@ -155,8 +156,9 @@
         return taskView;
     }
 
-    public static void createRecentsWindowAnimator(
-            @NonNull RecentsView recentsView,
+    public static <T extends Context & RecentsViewContainer & StatefulContainer<?>>
+    void createRecentsWindowAnimator(
+            @NonNull RecentsView<T, ?> recentsView,
             @NonNull TaskView v,
             boolean skipViewChanges,
             @NonNull RemoteAnimationTarget[] appTargets,
@@ -204,8 +206,9 @@
 
         int taskIndex = recentsView.indexOfChild(v);
         Context context = v.getContext();
-        BaseActivity baseActivity = BaseActivity.fromContext(context);
-        DeviceProfile dp = baseActivity.getDeviceProfile();
+
+        T container = RecentsViewContainer.containerFromContext(context);
+        DeviceProfile dp = container.getDeviceProfile();
         boolean showAsGrid = dp.isTablet;
         boolean parallaxCenterAndAdjacentTask =
                 !showAsGrid && taskIndex != recentsView.getCurrentPage();
@@ -417,7 +420,8 @@
 
         if (depthController != null) {
             out.setFloat(depthController.stateDepth, MULTI_PROPERTY_VALUE,
-                    BACKGROUND_APP.getDepth(baseActivity), TOUCH_RESPONSE);
+                    BACKGROUND_APP.getDepth(container),
+                    TOUCH_RESPONSE);
         }
     }
 
diff --git a/quickstep/src/com/android/quickstep/TopTaskTracker.java b/quickstep/src/com/android/quickstep/TopTaskTracker.java
index 3cf0542..80d6137 100644
--- a/quickstep/src/com/android/quickstep/TopTaskTracker.java
+++ b/quickstep/src/com/android/quickstep/TopTaskTracker.java
@@ -18,16 +18,18 @@
 import static android.app.ActivityTaskManager.INVALID_TASK_ID;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
-import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.content.Intent.ACTION_CHOOSER;
 import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS;
 import static android.view.Display.DEFAULT_DISPLAY;
 
 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT;
+import static com.android.wm.shell.shared.GroupedTaskInfo.TYPE_SPLIT;
 
-import android.annotation.UserIdInt;
 import android.app.ActivityManager.RunningTaskInfo;
+import android.app.TaskInfo;
 import android.content.Context;
+import android.util.ArrayMap;
+import android.util.Log;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -45,8 +47,10 @@
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.TaskStackChangeListener;
 import com.android.systemui.shared.system.TaskStackChangeListeners;
+import com.android.wm.shell.shared.GroupedTaskInfo;
 import com.android.wm.shell.splitscreen.ISplitScreenListener;
 
+import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -60,42 +64,71 @@
  */
 public class TopTaskTracker extends ISplitScreenListener.Stub
         implements TaskStackChangeListener, SafeCloseable {
-
+    private static final String TAG = "TopTaskTracker";
     public static MainThreadInitializedObject<TopTaskTracker> INSTANCE =
             new MainThreadInitializedObject<>(TopTaskTracker::new);
 
     private static final int HISTORY_SIZE = 5;
 
-    // Ordered list with first item being the most recent task.
-    private final LinkedList<RunningTaskInfo> mOrderedTaskList = new LinkedList<>();
-
     private final Context mContext;
+
+    // Only used when Flags.enableShellTopTaskTracking() is disabled
+    // Ordered list with first item being the most recent task.
+    private final LinkedList<TaskInfo> mOrderedTaskList = new LinkedList<>();
     private final SplitStageInfo mMainStagePosition = new SplitStageInfo();
     private final SplitStageInfo mSideStagePosition = new SplitStageInfo();
     private int mPinnedTaskId = INVALID_TASK_ID;
 
+    // Only used when Flags.enableShellTopTaskTracking() is enabled
+    // Mapping of display id to running tasks.  Running tasks are ordered from top most to
+    // bottom most.
+    private ArrayMap<Integer, ArrayList<GroupedTaskInfo>> mVisibleTasks = new ArrayMap<>();
+
     private TopTaskTracker(Context context) {
         mContext = context;
-        mMainStagePosition.stageType = SplitConfigurationOptions.STAGE_TYPE_MAIN;
-        mSideStagePosition.stageType = SplitConfigurationOptions.STAGE_TYPE_SIDE;
 
-        TaskStackChangeListeners.getInstance().registerTaskStackListener(this);
-        SystemUiProxy.INSTANCE.get(context).registerSplitScreenListener(this);
+        if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+            // Just prepopulate a list for the default display tasks so we don't need to add null
+            // checks everywhere
+            mVisibleTasks.put(DEFAULT_DISPLAY, new ArrayList<>());
+        } else {
+            mMainStagePosition.stageType = SplitConfigurationOptions.STAGE_TYPE_MAIN;
+            mSideStagePosition.stageType = SplitConfigurationOptions.STAGE_TYPE_SIDE;
+
+            TaskStackChangeListeners.getInstance().registerTaskStackListener(this);
+            SystemUiProxy.INSTANCE.get(context).registerSplitScreenListener(this);
+        }
     }
 
     @Override
     public void close() {
+        if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+            return;
+        }
+
         TaskStackChangeListeners.getInstance().unregisterTaskStackListener(this);
         SystemUiProxy.INSTANCE.get(mContext).unregisterSplitScreenListener(this);
     }
 
     @Override
     public void onTaskRemoved(int taskId) {
+        if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+            return;
+        }
+
         mOrderedTaskList.removeIf(rto -> rto.taskId == taskId);
     }
 
     @Override
     public void onTaskMovedToFront(RunningTaskInfo taskInfo) {
+        handleTaskMovedToFront(taskInfo);
+    }
+
+    void handleTaskMovedToFront(TaskInfo taskInfo) {
+        if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+            return;
+        }
+
         mOrderedTaskList.removeIf(rto -> rto.taskId == taskInfo.taskId);
         mOrderedTaskList.addFirst(taskInfo);
 
@@ -103,7 +136,7 @@
         // display's task to the list, to avoid showing non-home display's task upon going to
         // Recents animation.
         if (taskInfo.displayId != DEFAULT_DISPLAY) {
-            final RunningTaskInfo topTaskOnHomeDisplay = mOrderedTaskList.stream()
+            final TaskInfo topTaskOnHomeDisplay = mOrderedTaskList.stream()
                     .filter(rto -> rto.displayId == DEFAULT_DISPLAY).findFirst().orElse(null);
             if (topTaskOnHomeDisplay != null) {
                 mOrderedTaskList.removeIf(rto -> rto.taskId == topTaskOnHomeDisplay.taskId);
@@ -113,9 +146,9 @@
 
         if (mOrderedTaskList.size() >= HISTORY_SIZE) {
             // If we grow in size, remove the last taskInfo which is not part of the split task.
-            Iterator<RunningTaskInfo> itr = mOrderedTaskList.descendingIterator();
+            Iterator<TaskInfo> itr = mOrderedTaskList.descendingIterator();
             while (itr.hasNext()) {
-                RunningTaskInfo info = itr.next();
+                TaskInfo info = itr.next();
                 if (info.taskId != taskInfo.taskId
                         && info.taskId != mMainStagePosition.taskId
                         && info.taskId != mSideStagePosition.taskId) {
@@ -126,8 +159,39 @@
         }
     }
 
+    /**
+     * Called when the set of visible tasks have changed.
+     */
+    public void onVisibleTasksChanged(GroupedTaskInfo[] visibleTasks) {
+        if (!com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+            return;
+        }
+
+        // TODO(346588978): Per-display info, just have everything in order by display
+
+        // Clear existing tasks for each display
+        mVisibleTasks.forEach((displayId, visibleTasksOnDisplay) -> visibleTasksOnDisplay.clear());
+
+        // Update the visible tasks on each display
+        for (int i = 0; i < visibleTasks.length; i++) {
+            final int displayId = visibleTasks[i].getTaskInfo1().getDisplayId();
+            final ArrayList<GroupedTaskInfo> displayTasks;
+            if (mVisibleTasks.containsKey(displayId)) {
+                displayTasks = mVisibleTasks.get(displayId);
+            } else {
+                displayTasks = new ArrayList<>();
+                mVisibleTasks.put(displayId, displayTasks);
+            }
+            displayTasks.add(visibleTasks[i]);
+        }
+    }
+
     @Override
     public void onStagePositionChanged(@StageType int stage, @StagePosition int position) {
+        if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+            return;
+        }
+
         if (stage == SplitConfigurationOptions.STAGE_TYPE_MAIN) {
             mMainStagePosition.stagePosition = position;
         } else {
@@ -135,8 +199,25 @@
         }
     }
 
+    public void onTaskChanged(RunningTaskInfo taskInfo) {
+        if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+            return;
+        }
+
+        for (int i = 0; i < mOrderedTaskList.size(); i++) {
+            if (mOrderedTaskList.get(i).taskId == taskInfo.taskId) {
+                mOrderedTaskList.set(i, taskInfo);
+                break;
+            }
+        }
+    }
+
     @Override
     public void onTaskStageChanged(int taskId, @StageType int stage, boolean visible) {
+        if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+            return;
+        }
+
         // If a task is not visible anymore or has been moved to undefined, stop tracking it.
         if (!visible || stage == SplitConfigurationOptions.STAGE_TYPE_UNDEFINED) {
             if (mMainStagePosition.taskId == taskId) {
@@ -156,11 +237,19 @@
 
     @Override
     public void onActivityPinned(String packageName, int userId, int taskId, int stackId) {
+        if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+            return;
+        }
+
         mPinnedTaskId = taskId;
     }
 
     @Override
     public void onActivityUnpinned() {
+        if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+            return;
+        }
+
         mPinnedTaskId = INVALID_TASK_ID;
     }
 
@@ -169,21 +258,59 @@
      * Will return empty array if device is not in staged split
      */
     public int[] getRunningSplitTaskIds() {
-        if (mMainStagePosition.taskId == INVALID_TASK_ID
-                || mSideStagePosition.taskId == INVALID_TASK_ID) {
-            return new int[]{};
-        }
-        int[] out = new int[2];
-        if (mMainStagePosition.stagePosition == STAGE_POSITION_TOP_OR_LEFT) {
-            out[0] = mMainStagePosition.taskId;
-            out[1] = mSideStagePosition.taskId;
+        if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+            // TODO(346588978): This assumes default display for now
+            final ArrayList<GroupedTaskInfo> visibleTasks = mVisibleTasks.get(DEFAULT_DISPLAY);
+            final GroupedTaskInfo splitTaskInfo = visibleTasks.stream()
+                    .filter(taskInfo -> taskInfo.getType() == TYPE_SPLIT)
+                    .findFirst().orElse(null);
+            if (splitTaskInfo != null && splitTaskInfo.getSplitBounds() != null) {
+                return new int[] {
+                        splitTaskInfo.getSplitBounds().leftTopTaskId,
+                        splitTaskInfo.getSplitBounds().rightBottomTaskId
+                };
+            }
+            return new int[0];
         } else {
-            out[1] = mMainStagePosition.taskId;
-            out[0] = mSideStagePosition.taskId;
+            if (mMainStagePosition.taskId == INVALID_TASK_ID
+                    || mSideStagePosition.taskId == INVALID_TASK_ID) {
+                return new int[]{};
+            }
+            int[] out = new int[2];
+            if (mMainStagePosition.stagePosition == STAGE_POSITION_TOP_OR_LEFT) {
+                out[0] = mMainStagePosition.taskId;
+                out[1] = mSideStagePosition.taskId;
+            } else {
+                out[1] = mMainStagePosition.taskId;
+                out[0] = mSideStagePosition.taskId;
+            }
+            return out;
         }
-        return out;
     }
 
+    /**
+     * Dumps the list of tasks in top task tracker.
+     */
+    public void dump(PrintWriter pw) {
+        if (!com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+            return;
+        }
+
+        // TODO(346588978): This assumes default display for now
+        final ArrayList<GroupedTaskInfo> displayTasks = mVisibleTasks.get(DEFAULT_DISPLAY);
+        pw.println("TopTaskTracker:");
+        pw.println("  tasks: [");
+        for (GroupedTaskInfo taskInfo : displayTasks) {
+            final TaskInfo info = taskInfo.getTaskInfo1();
+            final boolean isExcluded = (info.baseIntent.getFlags()
+                    & FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) != 0;
+            pw.println("    " + info.taskId + ": excluded=" + isExcluded
+                    + " visibleRequested=" + info.isVisibleRequested
+                    + " visible=" + info.isVisible
+                    + " " + info.baseIntent.getComponent());
+        }
+        pw.println("  ]");
+    }
 
     /**
      * Returns the CachedTaskInfo for the top most task
@@ -191,25 +318,40 @@
     @NonNull
     @UiThread
     public CachedTaskInfo getCachedTopTask(boolean filterOnlyVisibleRecents) {
-        if (filterOnlyVisibleRecents) {
-            // Since we only know about the top most task, any filtering may not be applied on the
-            // cache. The second to top task may change while the top task is still the same.
-            RunningTaskInfo[] tasks = TraceHelper.allowIpcs("getCachedTopTask.true", () ->
-                    ActivityManagerWrapper.getInstance().getRunningTasks(true));
-            return new CachedTaskInfo(Arrays.asList(tasks));
-        }
+        if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+            // TODO(346588978): Currently ignore filterOnlyVisibleRecents, but perhaps make this an
+            //  explicit filter For things to ignore (ie. PIP/Bubbles/Assistant/etc/so that this is
+            //  explicit)
+            // TODO(346588978): This assumes default display for now (as does all of Launcher)
+            final ArrayList<GroupedTaskInfo> displayTasks = mVisibleTasks.get(DEFAULT_DISPLAY);
+            return new CachedTaskInfo(new ArrayList<>(displayTasks));
+        } else {
+            if (filterOnlyVisibleRecents) {
+                // Since we only know about the top most task, any filtering may not be applied on
+                // the cache. The second to top task may change while the top task is still the
+                // same.
+                RunningTaskInfo[] tasks = TraceHelper.allowIpcs("getCachedTopTask.true", () ->
+                        ActivityManagerWrapper.getInstance().getRunningTasks(true));
+                return new CachedTaskInfo(Arrays.asList(tasks));
+            }
 
-        if (mOrderedTaskList.isEmpty()) {
-            RunningTaskInfo[] tasks = TraceHelper.allowIpcs("getCachedTopTask.false", () ->
-                    ActivityManagerWrapper.getInstance().getRunningTasks(
-                            false /* filterOnlyVisibleRecents */));
-            Collections.addAll(mOrderedTaskList, tasks);
-        }
+            if (mOrderedTaskList.isEmpty()) {
+                RunningTaskInfo[] tasks = TraceHelper.allowIpcs("getCachedTopTask.false", () ->
+                        ActivityManagerWrapper.getInstance().getRunningTasks(
+                                false /* filterOnlyVisibleRecents */));
+                Collections.addAll(mOrderedTaskList, tasks);
+            }
 
-        // Strip the pinned task
-        ArrayList<RunningTaskInfo> tasks = new ArrayList<>(mOrderedTaskList);
-        tasks.removeIf(t -> t.taskId == mPinnedTaskId);
-        return new CachedTaskInfo(tasks);
+            ArrayList<TaskInfo> tasks = new ArrayList<>(mOrderedTaskList);
+            // Strip the pinned task and recents task
+            tasks.removeIf(t -> t.taskId == mPinnedTaskId || isRecentsTask(t));
+            return new CachedTaskInfo(tasks);
+        }
+    }
+
+    private static boolean isRecentsTask(TaskInfo task) {
+        return task != null && task.configuration.windowConfiguration
+                .getActivityType() == ACTIVITY_TYPE_RECENTS;
     }
 
     /**
@@ -218,24 +360,79 @@
      */
     public static class CachedTaskInfo {
 
+        // Only used when enableShellTopTaskTracking() is disabled
         @Nullable
-        private final RunningTaskInfo mTopTask;
-        public final List<RunningTaskInfo> mAllCachedTasks;
+        private final TaskInfo mTopTask;
+        @Nullable
+        public final List<TaskInfo> mAllCachedTasks;
 
-        CachedTaskInfo(List<RunningTaskInfo> allCachedTasks) {
+        // Only used when enableShellTopTaskTracking() is enabled
+        @Nullable
+        private final GroupedTaskInfo mTopGroupedTask;
+        @Nullable
+        private final ArrayList<GroupedTaskInfo> mVisibleTasks;
+
+
+        // Only used when enableShellTopTaskTracking() is enabled
+        CachedTaskInfo(@NonNull ArrayList<GroupedTaskInfo> visibleTasks) {
+            mAllCachedTasks = null;
+            mTopTask = null;
+            mVisibleTasks = visibleTasks;
+            mTopGroupedTask = !mVisibleTasks.isEmpty() ? mVisibleTasks.getFirst() : null;
+
+        }
+
+        // Only used when enableShellTopTaskTracking() is disabled
+        CachedTaskInfo(@NonNull List<TaskInfo> allCachedTasks) {
+            mVisibleTasks = null;
+            mTopGroupedTask = null;
             mAllCachedTasks = allCachedTasks;
             mTopTask = allCachedTasks.isEmpty() ? null : allCachedTasks.get(0);
         }
 
+        /**
+         * @return The list of visible tasks
+         */
+        public ArrayList<GroupedTaskInfo> getVisibleTasks() {
+            return mVisibleTasks;
+        }
+
+        /**
+         * @return The top task id
+         */
         public int getTaskId() {
-            return mTopTask == null ? INVALID_TASK_ID : mTopTask.taskId;
+            if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+                // Callers should use topGroupedTaskContainsTask() instead
+                return INVALID_TASK_ID;
+            } else {
+                return mTopTask != null ? mTopTask.taskId : INVALID_TASK_ID;
+            }
+        }
+
+        /**
+         * @return Whether the top grouped task contains the given {@param taskId} if
+         *         Flags.enableShellTopTaskTracking() is true, otherwise it checks the top
+         *         task as reported from TaskStackListener.
+         */
+        public boolean topGroupedTaskContainsTask(int taskId) {
+            if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+                return mTopGroupedTask != null && mTopGroupedTask.containsTask(taskId);
+            } else {
+                return mTopTask != null && mTopTask.taskId == taskId;
+            }
         }
 
         /**
          * Returns true if the root of the task chooser activity
          */
         public boolean isRootChooseActivity() {
-            return mTopTask != null && ACTION_CHOOSER.equals(mTopTask.baseIntent.getAction());
+            if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+                // TODO(346588978): Update this to not make an assumption on a specific task info
+                return mTopGroupedTask != null && ACTION_CHOOSER.equals(
+                        mTopGroupedTask.getTaskInfo1().baseIntent.getAction());
+            } else {
+                return mTopTask != null && ACTION_CHOOSER.equals(mTopTask.baseIntent.getAction());
+            }
         }
 
         /**
@@ -243,12 +440,16 @@
          * is another running task that is not excluded from recents, returns that underlying task.
          */
         public @Nullable CachedTaskInfo getVisibleNonExcludedTask() {
+            if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+                // Callers should not need this when the full set of visible tasks are provided
+                return null;
+            }
             if (mTopTask == null
                     || (mTopTask.baseIntent.getFlags() & FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) == 0) {
                 // Not an excluded task.
                 return null;
             }
-            List<RunningTaskInfo> visibleNonExcludedTasks = mAllCachedTasks.stream()
+            List<TaskInfo> visibleNonExcludedTasks = mAllCachedTasks.stream()
                     .filter(t -> t.isVisible
                             && (t.baseIntent.getFlags() & FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) == 0
                             && t.getActivityType() != ACTIVITY_TYPE_HOME
@@ -259,25 +460,30 @@
         }
 
         /**
-         * Returns true if this represents the HOME task
+         * Returns true if this represents the HOME activity type task
          */
         public boolean isHomeTask() {
-            return mTopTask != null && mTopTask.configuration.windowConfiguration
-                    .getActivityType() == ACTIVITY_TYPE_HOME;
-        }
-
-        public boolean isRecentsTask() {
-            return mTopTask != null && mTopTask.configuration.windowConfiguration
-                    .getActivityType() == ACTIVITY_TYPE_RECENTS;
+            if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+                // TODO(346588978): Update this to not make an assumption on a specific task info
+                return mTopGroupedTask != null
+                        && mTopGroupedTask.getTaskInfo1().getActivityType() == ACTIVITY_TYPE_HOME;
+            } else {
+                return mTopTask != null && mTopTask.configuration.windowConfiguration
+                        .getActivityType() == ACTIVITY_TYPE_HOME;
+            }
         }
 
         /**
-         * Returns {@code true} if this task windowing mode is set to {@link
-         * android.app.WindowConfiguration#WINDOWING_MODE_FREEFORM}
+         * Returns true if this represents the RECENTS activity type task
          */
-        public boolean isFreeformTask() {
-            return mTopTask != null && mTopTask.configuration.windowConfiguration.getWindowingMode()
-                    == WINDOWING_MODE_FREEFORM;
+        public boolean isRecentsTask() {
+            if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+                // TODO(346588978): Update this to not make an assumption on a specific task info
+                return mTopGroupedTask != null
+                        && TopTaskTracker.isRecentsTask(mTopGroupedTask.getTaskInfo1());
+            } else {
+                return TopTaskTracker.isRecentsTask(mTopTask);
+            }
         }
 
         /**
@@ -285,43 +491,78 @@
          * is loaded by the model
          */
         public Task[] getPlaceholderTasks() {
-            return mTopTask == null ? new Task[0]
-                    : new Task[]{Task.from(new TaskKey(mTopTask), mTopTask, false)};
+            if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+                // TODO(346588978): Update this to return more than a single task once the callers
+                //  are refactored
+                if (mVisibleTasks.isEmpty()) {
+                    return new Task[0];
+                }
+                final TaskInfo info = mVisibleTasks.getFirst().getTaskInfo1();
+                return new Task[]{Task.from(new TaskKey(info), info, false)};
+            } else {
+                return mTopTask == null ? new Task[0]
+                        : new Task[]{Task.from(new TaskKey(mTopTask), mTopTask, false)};
+            }
         }
 
         /**
          * Returns {@link Task} array corresponding to the provided task ids which can be used as a
          * placeholder until the true object is loaded by the model
          */
-        public Task[] getPlaceholderTasks(int[] taskIds) {
-            if (mTopTask == null) {
-                return new Task[0];
-            }
-            Task[] result = new Task[taskIds.length];
-            for (int i = 0; i < taskIds.length; i++) {
-                final int index = i;
-                int taskId = taskIds[i];
-                mAllCachedTasks.forEach(rti -> {
-                    if (rti.taskId == taskId) {
-                        result[index] = Task.from(new TaskKey(rti), rti, false);
-                    }
-                });
-            }
-            return result;
-        }
+        public Task[] getSplitPlaceholderTasks(int[] taskIds) {
+            if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+                if (mVisibleTasks.isEmpty()
+                        || mVisibleTasks.getFirst().getType() != TYPE_SPLIT) {
+                    return new Task[0];
+                }
 
-        @UserIdInt
-        @Nullable
-        public Integer getUserId() {
-            return mTopTask == null ? null : mTopTask.userId;
+                GroupedTaskInfo splitTask = mVisibleTasks.getFirst();
+                Task[] result = new Task[taskIds.length];
+                for (int i = 0; i < taskIds.length; i++) {
+                    TaskInfo info = splitTask.getTaskById(taskIds[i]);
+                    if (info == null) {
+                        Log.w(TAG, "Requested task (" + taskIds[i] + ") not found");
+                        return new Task[0];
+                    }
+                    result[i] = Task.from(new TaskKey(info), info, false);
+                }
+                return result;
+            } else {
+                if (mTopTask == null) {
+                    return new Task[0];
+                }
+                Task[] result = new Task[taskIds.length];
+                for (int i = 0; i < taskIds.length; i++) {
+                    final int index = i;
+                    int taskId = taskIds[i];
+                    mAllCachedTasks.forEach(rti -> {
+                        if (rti.taskId == taskId) {
+                            result[index] = Task.from(new TaskKey(rti), rti, false);
+                        }
+                    });
+                }
+                return result;
+            }
         }
 
         @Nullable
         public String getPackageName() {
-            if (mTopTask == null || mTopTask.baseActivity == null) {
-                return null;
+            if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+                // TODO(346588978): Update this to not make an assumption on a specific task info
+                if (mTopGroupedTask == null) {
+                    return null;
+                }
+                final TaskInfo info = mTopGroupedTask.getTaskInfo1();
+                if (info.baseActivity == null) {
+                    return null;
+                }
+                return info.baseActivity.getPackageName();
+            } else {
+                if (mTopTask == null || mTopTask.baseActivity == null) {
+                    return null;
+                }
+                return mTopTask.baseActivity.getPackageName();
             }
-            return mTopTask.baseActivity.getPackageName();
         }
     }
 }
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index 178636e..0242fb6 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -36,11 +36,6 @@
 import static com.android.quickstep.GestureState.TrackpadGestureType.getTrackpadGestureType;
 import static com.android.quickstep.InputConsumer.TYPE_CURSOR_HOVER;
 import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.FLAG_USING_OTHER_ACTIVITY_INPUT_CONSUMER;
-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;
 import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_UNFOLD_ANIMATION_FORWARDER;
@@ -76,19 +71,21 @@
 import android.view.InputDevice;
 import android.view.InputEvent;
 import android.view.MotionEvent;
+import android.view.View;
 
 import androidx.annotation.BinderThread;
+import androidx.annotation.MainThread;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
 import androidx.annotation.VisibleForTesting;
 
-import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.ConstantItem;
 import com.android.launcher3.EncryptionType;
 import com.android.launcher3.Flags;
 import com.android.launcher3.LauncherPrefs;
 import com.android.launcher3.anim.AnimatedFloat;
+import com.android.launcher3.desktop.DesktopAppLaunchTransitionManager;
 import com.android.launcher3.provider.RestoreDbTask;
 import com.android.launcher3.statehandlers.DesktopVisibilityController;
 import com.android.launcher3.statemanager.StatefulActivity;
@@ -107,6 +104,8 @@
 import com.android.launcher3.util.ScreenOnTracker;
 import com.android.launcher3.util.TraceHelper;
 import com.android.quickstep.OverviewCommandHelper.CommandType;
+import com.android.quickstep.fallback.window.RecentsWindowManager;
+import com.android.quickstep.fallback.window.RecentsWindowSwipeHandler;
 import com.android.quickstep.inputconsumers.AccessibilityInputConsumer;
 import com.android.quickstep.inputconsumers.AssistantInputConsumer;
 import com.android.quickstep.inputconsumers.BubbleBarInputConsumer;
@@ -124,8 +123,9 @@
 import com.android.quickstep.inputconsumers.TrackpadStatusBarInputConsumer;
 import com.android.quickstep.util.ActiveGestureLog;
 import com.android.quickstep.util.ActiveGestureLog.CompoundString;
-import com.android.quickstep.util.AssistStateManager;
-import com.android.quickstep.util.AssistUtils;
+import com.android.quickstep.util.ActiveGestureProtoLogProxy;
+import com.android.quickstep.util.ContextualSearchInvoker;
+import com.android.quickstep.util.ContextualSearchStateManager;
 import com.android.quickstep.views.RecentsViewContainer;
 import com.android.systemui.shared.recents.IOverviewProxy;
 import com.android.systemui.shared.recents.ISystemUiProxy;
@@ -299,7 +299,8 @@
         @Override
         public void onAssistantOverrideInvoked(int invocationType) {
             executeForTouchInteractionService(tis -> {
-                if (!AssistUtils.newInstance(tis).tryStartAssistOverride(invocationType)) {
+                if (!ContextualSearchInvoker.newInstance(tis)
+                        .tryStartAssistOverride(invocationType)) {
                     Log.w(TAG, "Failed to invoke Assist override");
                 }
             });
@@ -324,10 +325,10 @@
         @Override
         public void enterStageSplitFromRunningApp(boolean leftOrTop) {
             executeForTouchInteractionService(tis -> {
-                StatefulActivity activity =
-                        tis.mOverviewComponentObserver.getActivityInterface().getCreatedContainer();
-                if (activity != null) {
-                    activity.enterStageSplitFromRunningApp(leftOrTop);
+                RecentsViewContainer container = tis.mOverviewComponentObserver
+                        .getContainerInterface().getCreatedContainer();
+                if (container != null) {
+                    container.enterStageSplitFromRunningApp(leftOrTop);
                 }
             });
         }
@@ -343,36 +344,36 @@
 
         @BinderThread
         @Override
-        public void checkNavBarModes() {
-            MAIN_EXECUTOR.execute(() -> executeForTouchInteractionService(tis ->
-                    executeForTaskbarManager(TaskbarManager::checkNavBarModes)
-            ));
-        }
-
-        @BinderThread
-        @Override
-        public void finishBarAnimations() {
-            MAIN_EXECUTOR.execute(() -> executeForTouchInteractionService(tis ->
-                    executeForTaskbarManager(TaskbarManager::finishBarAnimations)
-            ));
-        }
-
-        @BinderThread
-        @Override
-        public void touchAutoDim(boolean reset) {
-            MAIN_EXECUTOR.execute(() -> executeForTouchInteractionService(tis ->
-                    executeForTaskbarManager(taskbarManager -> taskbarManager.touchAutoDim(reset))
-            ));
-        }
-
-        @BinderThread
-        @Override
-        public void transitionTo(@BarTransitions.TransitionMode int barMode,
-                boolean animate) {
+        public void checkNavBarModes(int displayId) {
             MAIN_EXECUTOR.execute(() -> executeForTouchInteractionService(tis ->
                     executeForTaskbarManager(
-                            taskbarManager -> taskbarManager.transitionTo(barMode, animate))
-            ));
+                            taskbarManager -> taskbarManager.checkNavBarModes(displayId))));
+        }
+
+        @BinderThread
+        @Override
+        public void finishBarAnimations(int displayId) {
+            MAIN_EXECUTOR.execute(() -> executeForTouchInteractionService(
+                    tis -> executeForTaskbarManager(
+                            taskbarManager -> taskbarManager.finishBarAnimations(displayId))));
+        }
+
+        @BinderThread
+        @Override
+        public void touchAutoDim(int displayId, boolean reset) {
+            MAIN_EXECUTOR.execute(() -> executeForTouchInteractionService(
+                    tis -> executeForTaskbarManager(
+                            taskbarManager -> taskbarManager.touchAutoDim(displayId, reset))));
+        }
+
+        @BinderThread
+        @Override
+        public void transitionTo(int displayId, @BarTransitions.TransitionMode int barMode,
+                boolean animate) {
+            MAIN_EXECUTOR.execute(() -> executeForTouchInteractionService(
+                    tis -> executeForTaskbarManager(
+                            taskbarManager -> taskbarManager.transitionTo(displayId, barMode,
+                                    animate))));
         }
 
         @BinderThread
@@ -551,11 +552,15 @@
                 @Override
                 public void onInputDeviceAdded(int deviceId) {
                     if (isTrackpadDevice(deviceId)) {
-                        boolean wasEmpty = mTrackpadsConnected.isEmpty();
-                        mTrackpadsConnected.add(deviceId);
-                        if (wasEmpty) {
-                            update();
-                        }
+                        // This updates internal TIS state so it needs to also run on the main
+                        // thread.
+                        MAIN_EXECUTOR.execute(() -> {
+                            boolean wasEmpty = mTrackpadsConnected.isEmpty();
+                            mTrackpadsConnected.add(deviceId);
+                            if (wasEmpty) {
+                                update();
+                            }
+                        });
                     }
                 }
 
@@ -565,12 +570,17 @@
 
                 @Override
                 public void onInputDeviceRemoved(int deviceId) {
-                    mTrackpadsConnected.remove(deviceId);
-                    if (mTrackpadsConnected.isEmpty()) {
-                        update();
-                    }
+                    // This updates internal TIS state so it needs to also run on the main
+                    // thread.
+                    MAIN_EXECUTOR.execute(() -> {
+                        mTrackpadsConnected.remove(deviceId);
+                        if (mTrackpadsConnected.isEmpty()) {
+                            update();
+                        }
+                    });
                 }
 
+                @MainThread
                 private void update() {
                     if (mInputMonitorCompat != null && !mTrackpadsConnected.isEmpty()) {
                         // Don't destroy and reinitialize input monitor due to trackpad
@@ -581,6 +591,7 @@
                 }
 
                 private boolean isTrackpadDevice(int deviceId) {
+                    // This is a blocking binder call that should run on a bg thread.
                     InputDevice inputDevice = mInputManager.getInputDevice(deviceId);
                     if (inputDevice == null) {
                         return false;
@@ -606,6 +617,11 @@
             this::createLauncherSwipeHandler;
     private final AbsSwipeUpHandler.Factory mFallbackSwipeHandlerFactory =
             this::createFallbackSwipeHandler;
+    private final AbsSwipeUpHandler.Factory mRecentsWindowSwipeHandlerFactory =
+            this::createRecentsWindowSwipeHandler;
+    // This needs to be a member to be queued and potentially removed later if the service is
+    // destroyed before the user is unlocked
+    private final Runnable mUserUnlockedRunnable = this::onUserUnlocked;
 
     private final ScreenOnTracker.ScreenOnListener mScreenOnListener = this::onScreenOnChanged;
 
@@ -638,6 +654,7 @@
     private InputEventReceiver mInputEventReceiver;
 
     private TaskbarManager mTaskbarManager;
+    private RecentsWindowManager mRecentsWindowManager;
     private Function<GestureState, AnimatedFloat> mSwipeUpProxyProvider = i -> null;
     private AllAppsActionManager mAllAppsActionManager;
     private InputManager mInputManager;
@@ -646,10 +663,13 @@
     private NavigationMode mGestureStartNavMode = null;
 
     private DesktopVisibilityController mDesktopVisibilityController;
+    private DesktopAppLaunchTransitionManager mDesktopAppLaunchTransitionManager;
 
     @Override
     public void onCreate() {
         super.onCreate();
+        Log.d(TAG, "onCreate: user=" + getUserId()
+                + " instance=" + System.identityHashCode(this));
         // Initialize anything here that is needed in direct boot mode.
         // Everything else should be initialized in onUserUnlocked() below.
         mMainChoreographer = Choreographer.getInstance();
@@ -668,11 +688,16 @@
         mDesktopVisibilityController = new DesktopVisibilityController(this);
         mTaskbarManager = new TaskbarManager(
                 this, mAllAppsActionManager, mNavCallbacks, mDesktopVisibilityController);
+        mDesktopAppLaunchTransitionManager =
+                new DesktopAppLaunchTransitionManager(this, SystemUiProxy.INSTANCE.get(this));
+        mDesktopAppLaunchTransitionManager.registerTransitions();
+        if (Flags.enableLauncherOverviewInWindow() || Flags.enableFallbackOverviewInWindow()) {
+            mRecentsWindowManager = new RecentsWindowManager(this);
+        }
         mInputConsumer = InputConsumerController.getRecentsAnimationInputConsumer();
 
         // Call runOnUserUnlocked() before any other callbacks to ensure everything is initialized.
-        LockedUserState.get(this).runOnUserUnlocked(this::onUserUnlocked);
-        LockedUserState.get(this).runOnUserUnlocked(mTaskbarManager::onUserUnlocked);
+        LockedUserState.get(this).runOnUserUnlocked(mUserUnlockedRunnable);
         mDeviceState.addNavigationModeChangedCallback(this::onNavigationModeChanged);
         sConnected = true;
 
@@ -680,7 +705,8 @@
     }
 
     private void disposeEventHandlers(String reason) {
-        Log.d(TAG, "disposeEventHandlers: Reason: " + reason);
+        Log.d(TAG, "disposeEventHandlers: Reason: " + reason
+                + " instance=" + System.identityHashCode(this));
         if (mInputEventReceiver != null) {
             mInputEventReceiver.dispose();
             mInputEventReceiver = null;
@@ -717,8 +743,9 @@
 
     @UiThread
     public void onUserUnlocked() {
-        Log.d(TAG, "onUserUnlocked: userId=" + getUserId());
-        mTaskAnimationManager = new TaskAnimationManager(this);
+        Log.d(TAG, "onUserUnlocked: userId=" + getUserId()
+                + " instance=" + System.identityHashCode(this));
+        mTaskAnimationManager = new TaskAnimationManager(this, mRecentsWindowManager);
         mOverviewComponentObserver = new OverviewComponentObserver(this, mDeviceState);
         mOverviewCommandHelper = new OverviewCommandHelper(this,
                 mOverviewComponentObserver, mTaskAnimationManager);
@@ -737,6 +764,8 @@
 
         mOverviewComponentObserver.setOverviewChangeListener(this::onOverviewTargetChange);
         onOverviewTargetChange(mOverviewComponentObserver.isHomeAndOverviewSame());
+
+        mTaskbarManager.onUserUnlocked();
     }
 
     public OverviewCommandHelper getOverviewCommandHelper() {
@@ -761,11 +790,15 @@
 
     private void onOverviewTargetChange(boolean isHomeAndOverviewSame) {
         mAllAppsActionManager.setHomeAndOverviewSame(isHomeAndOverviewSame);
-
-        StatefulActivity<?> newOverviewActivity =
-                mOverviewComponentObserver.getActivityInterface().getCreatedContainer();
-        if (newOverviewActivity != null) {
-            mTaskbarManager.setActivity(newOverviewActivity);
+        RecentsViewContainer newOverviewContainer =
+                mOverviewComponentObserver.getContainerInterface().getCreatedContainer();
+        if (newOverviewContainer != null) {
+            if (newOverviewContainer instanceof StatefulActivity activity) {
+                // This will also call setRecentsViewContainer() internally.
+                mTaskbarManager.setActivity(activity);
+            } else {
+                mTaskbarManager.setRecentsViewContainer(newOverviewContainer);
+            }
         }
         mTISBinder.onOverviewTargetChange();
     }
@@ -795,14 +828,15 @@
     @UiThread
     private void onAssistantVisibilityChanged() {
         if (LockedUserState.get(this).isUserUnlocked()) {
-            mOverviewComponentObserver.getActivityInterface().onAssistantVisibilityChanged(
+            mOverviewComponentObserver.getContainerInterface().onAssistantVisibilityChanged(
                     mDeviceState.getAssistantVisibility());
         }
     }
 
     @Override
     public void onDestroy() {
-        Log.d(TAG, "Touch service destroyed: user=" + getUserId());
+        Log.d(TAG, "onDestroy: user=" + getUserId()
+                + " instance=" + System.identityHashCode(this));
         sIsInitialized = false;
         if (LockedUserState.get(this).isUserUnlocked()) {
             mInputConsumer.unregisterInputConsumer();
@@ -818,16 +852,26 @@
         mTrackpadsConnected.clear();
 
         mTaskbarManager.destroy();
+
+        if (mRecentsWindowManager != null) {
+            mRecentsWindowManager.destroy();
+        }
+        if (mDesktopAppLaunchTransitionManager != null) {
+            mDesktopAppLaunchTransitionManager.unregisterTransitions();
+        }
+        mDesktopAppLaunchTransitionManager = null;
         mDesktopVisibilityController.onDestroy();
         sConnected = false;
 
+        LockedUserState.get(this).removeOnUserUnlockedRunnable(mUserUnlockedRunnable);
         ScreenOnTracker.INSTANCE.get(this).removeListener(mScreenOnListener);
         super.onDestroy();
     }
 
     @Override
     public IBinder onBind(Intent intent) {
-        Log.d(TAG, "Touch service connected: user=" + getUserId());
+        Log.d(TAG, "onBind: user=" + getUserId()
+                + " instance=" + System.identityHashCode(this));
         return mTISBinder;
     }
 
@@ -844,9 +888,7 @@
 
     private void onInputEvent(InputEvent ev) {
         if (!(ev instanceof MotionEvent)) {
-            ActiveGestureLog.INSTANCE.addLog(new CompoundString("TIS.onInputEvent: ")
-                    .append("Cannot process input event: received unknown event ")
-                    .append(ev.toString()));
+            ActiveGestureProtoLogProxy.logUnknownInputEvent(ev.toString());
             return;
         }
         MotionEvent event = (MotionEvent) ev;
@@ -855,27 +897,19 @@
                 TestProtocol.SEQUENCE_TIS, "TouchInteractionService.onInputEvent", event);
 
         if (!LockedUserState.get(this).isUserUnlocked()) {
-            ActiveGestureLog.INSTANCE.addLog(new CompoundString("TIS.onInputEvent: ")
-                    .append("Cannot process input event: user is locked"));
+            ActiveGestureProtoLogProxy.logOnInputEventUserLocked();
             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);
+            ActiveGestureProtoLogProxy.logOnInputEventNavModeSwitched(
+                    mGestureStartNavMode.name(), currentNavMode.name());
             event.setAction(ACTION_CANCEL);
         } else if (mDeviceState.isButtonNavMode()
                 && !mDeviceState.supportsAssistantGestureInButtonNav()
                 && !isTrackpadMotionEvent(event)) {
-            ActiveGestureLog.INSTANCE.addLog(new CompoundString("TIS.onInputEvent: ")
-                    .append("Cannot process input event: ")
-                    .append("using 3-button nav and event is not a trackpad event"));
+            ActiveGestureProtoLogProxy.logOnInputEventThreeButtonNav();
             return;
         }
 
@@ -891,12 +925,7 @@
             }
             if (mTaskAnimationManager.shouldIgnoreMotionEvents()) {
                 if (action == ACTION_DOWN || isHoverActionWithoutConsumer) {
-                    ActiveGestureLog.INSTANCE.addLog(
-                            new CompoundString("TIS.onMotionEvent: A new gesture has been ")
-                                    .append("started, but a previously-requested recents ")
-                                    .append("animation hasn't started. Ignoring all following ")
-                                    .append("motion events."),
-                            RECENTS_ANIMATION_START_PENDING);
+                    ActiveGestureProtoLogProxy.logOnInputIgnoringFollowingEvents();
                 }
                 return;
             }
@@ -911,7 +940,7 @@
         SafeCloseable traceToken = TraceHelper.INSTANCE.allowIpcs("TIS.onInputEvent");
 
         CompoundString reasonString = action == ACTION_DOWN
-                ? new CompoundString("TIS.onMotionEvent: ") : CompoundString.NO_OP;
+                ? CompoundString.newEmptyString() : CompoundString.NO_OP;
         if (action == ACTION_DOWN || isHoverActionWithoutConsumer) {
             mRotationTouchHelper.setOrientationTransformIfNeeded(event);
 
@@ -921,30 +950,27 @@
             BubbleControllers bubbleControllers = tac != null ? tac.getBubbleControllers() : null;
             boolean isOnBubbles = bubbleControllers != null
                     && BubbleBarInputConsumer.isEventOnBubbles(tac, event);
-            if (isInSwipeUpTouchRegion && tac != null) {
-                tac.closeKeyboardQuickSwitchView();
-            }
             if (mDeviceState.isButtonNavMode()
                     && mDeviceState.supportsAssistantGestureInButtonNav()) {
                 reasonString.append("in three button mode which supports Assistant gesture");
                 // Consume gesture event for Assistant (all other gestures should do nothing).
                 if (mDeviceState.canTriggerAssistantAction(event)) {
-                    reasonString.append(" and event can trigger assistant action")
-                            .append(", consuming gesture for assistant action");
+                    reasonString.append(" and event can trigger assistant action, "
+                            + "consuming gesture for assistant action");
                     mGestureState =
                             createGestureState(mGestureState, getTrackpadGestureType(event));
                     mUncheckedConsumer = tryCreateAssistantInputConsumer(mGestureState, event);
                 } else {
-                    reasonString.append(" but event cannot trigger Assistant")
-                            .append(", consuming gesture as no-op");
+                    reasonString.append(" but event cannot trigger Assistant, "
+                            + "consuming gesture as no-op");
                     mUncheckedConsumer = InputConsumer.NO_OP;
                 }
             } else if ((!isOneHandedModeActive && isInSwipeUpTouchRegion)
                     || isHoverActionWithoutConsumer || isOnBubbles) {
                 reasonString.append(!isOneHandedModeActive && isInSwipeUpTouchRegion
-                                ? "one handed mode is not active and event is in swipe up region"
-                                : "isHoverActionWithoutConsumer == true")
-                        .append(", creating new input consumer");
+                        ? "one handed mode is not active and event is in swipe up region, "
+                                + "creating new input consumer"
+                        : "isHoverActionWithoutConsumer == true, creating new input consumer");
                 // Clone the previous gesture state since onConsumerAboutToBeSwitched might trigger
                 // onConsumerInactive and wipe the previous gesture state
                 GestureState prevGestureState = new GestureState(mGestureState);
@@ -957,18 +983,18 @@
             } else if ((mDeviceState.isFullyGesturalNavMode() || isTrackpadMultiFingerSwipe(event))
                     && mDeviceState.canTriggerAssistantAction(event)) {
                 reasonString.append(mDeviceState.isFullyGesturalNavMode()
-                                ? "using fully gestural nav"
-                                : "event is a trackpad multi-finger swipe")
-                        .append(" and event can trigger assistant action")
-                        .append(", consuming gesture for assistant action");
+                        ? "using fully gestural nav and event can trigger assistant action, "
+                                + "consuming gesture for assistant action"
+                        : "event is a trackpad multi-finger swipe and event can trigger assistant "
+                                + "action, consuming gesture for assistant action");
                 mGestureState = createGestureState(mGestureState, getTrackpadGestureType(event));
                 // Do not change mConsumer as if there is an ongoing QuickSwitch gesture, we
                 // should not interrupt it. QuickSwitch assumes that interruption can only
                 // happen if the next gesture is also quick switch.
                 mUncheckedConsumer = tryCreateAssistantInputConsumer(mGestureState, event);
             } else if (mDeviceState.canTriggerOneHandedAction(event)) {
-                reasonString.append("event can trigger one-handed action")
-                                .append(", consuming gesture for one-handed action");
+                reasonString.append("event can trigger one-handed action, "
+                        + "consuming gesture for one-handed action");
                 // Consume gesture event for triggering one handed feature.
                 mUncheckedConsumer = new OneHandedModeInputConsumer(this, mDeviceState,
                         InputConsumer.NO_OP, mInputMonitorCompat);
@@ -986,41 +1012,25 @@
         if (mUncheckedConsumer != InputConsumer.NO_OP) {
             switch (action) {
                 case ACTION_DOWN:
-                    ActiveGestureLog.INSTANCE.addLog(reasonString);
+                    ActiveGestureProtoLogProxy.logOnInputEventActionDown(reasonString);
                     // fall through
                 case ACTION_UP:
-                    ActiveGestureLog.INSTANCE.addLog(
-                            new CompoundString("onMotionEvent(")
-                                    .append((int) event.getRawX())
-                                    .append(", ")
-                                    .append((int) event.getRawY())
-                                    .append("): ")
-                                    .append(MotionEvent.actionToString(action))
-                                    .append(", ")
-                                    .append(MotionEvent.classificationToString(
-                                            event.getClassification())),
-                            /* gestureEvent= */ action == ACTION_DOWN
-                                    ? MOTION_DOWN
-                                    : MOTION_UP);
+                    ActiveGestureProtoLogProxy.logOnInputEventActionUp(
+                            (int) event.getRawX(),
+                            (int) event.getRawY(),
+                            action,
+                            MotionEvent.classificationToString(event.getClassification()));
                     break;
                 case ACTION_MOVE:
-                    ActiveGestureLog.INSTANCE.addLog(
-                            new CompoundString("onMotionEvent: ")
-                                    .append(MotionEvent.actionToString(action))
-                                    .append(",")
-                                    .append(MotionEvent.classificationToString(
-                                            event.getClassification()))
-                                    .append(", pointerCount: ")
-                                    .append(event.getPointerCount()),
-                            MOTION_MOVE);
+                    ActiveGestureProtoLogProxy.logOnInputEventActionMove(
+                            MotionEvent.actionToString(action),
+                            MotionEvent.classificationToString(event.getClassification()),
+                            event.getPointerCount());
                     break;
                 default: {
-                    ActiveGestureLog.INSTANCE.addLog(
-                            new CompoundString("onMotionEvent: ")
-                                    .append(MotionEvent.actionToString(action))
-                                    .append(",")
-                                    .append(MotionEvent.classificationToString(
-                                            event.getClassification())));
+                    ActiveGestureProtoLogProxy.logOnInputEventGenericAction(
+                            MotionEvent.actionToString(action),
+                            MotionEvent.classificationToString(event.getClassification()));
                 }
             }
         }
@@ -1075,11 +1085,11 @@
             MotionEvent motionEvent,
             CompoundString reasonString) {
         if (mDeviceState.isGestureBlockedTask(gestureState.getRunningTask())) {
-            reasonString.append(SUBSTRING_PREFIX)
-                    .append("is gesture-blocked task, using base input consumer");
+            reasonString.append(
+                    "%sis gesture-blocked task, using base input consumer", SUBSTRING_PREFIX);
             return base;
         } else {
-            reasonString.append(SUBSTRING_PREFIX).append("using AssistantInputConsumer");
+            reasonString.append("%susing AssistantInputConsumer", SUBSTRING_PREFIX);
             return new AssistantInputConsumer(
                     this, gestureState, base, mInputMonitorCompat, mDeviceState, motionEvent);
         }
@@ -1110,10 +1120,8 @@
         gestureState.setTrackpadGestureType(trackpadGestureType);
 
         // Log initial state for the gesture.
-        ActiveGestureLog.INSTANCE.addLog(new CompoundString("Current running task package name=")
-                .append(taskInfo.getPackageName()));
-        ActiveGestureLog.INSTANCE.addLog(new CompoundString("Current SystemUi state flags=")
-                .append(mDeviceState.getSystemUiStateString()));
+        ActiveGestureProtoLogProxy.logRunningTaskPackage(taskInfo.getPackageName());
+        ActiveGestureProtoLogProxy.logSysuiStateFlags(mDeviceState.getSystemUiStateString());
         return gestureState;
     }
 
@@ -1150,12 +1158,11 @@
                 // This handles apps launched in direct boot mode (e.g. dialer) as well as apps
                 // launched while device is locked even after exiting direct boot mode (e.g. camera).
                 consumer = createDeviceLockedInputConsumer(
-                        newGestureState, reasonString.append(SUBSTRING_PREFIX)
-                                .append("can start system gesture"));
+                        newGestureState,
+                        reasonString.append("%scan start system gesture", SUBSTRING_PREFIX));
             } else {
                 consumer = getDefaultInputConsumer(
-                        reasonString.append(SUBSTRING_PREFIX)
-                                .append("cannot start system gesture"));
+                        reasonString.append("%scannot start system gesture", SUBSTRING_PREFIX));
             }
             logInputConsumerSelectionReason(consumer, reasonString);
             return consumer;
@@ -1167,13 +1174,12 @@
         // a followup gesture and the first gesture started in a valid system state.
         if (canStartSystemGesture || previousGestureState.isRecentsAnimationRunning()) {
             reasonString = newCompoundString(canStartSystemGesture
-                    ? "can start system gesture" : "recents animation was running")
-                    .append(", trying to use base consumer");
+                    ? "can start system gesture, trying to use base consumer"
+                    : "recents animation was running, trying to use base consumer");
             base = newBaseConsumer(previousGestureState, newGestureState, event, reasonString);
         } else {
-            reasonString = newCompoundString(
-                    "cannot start system gesture and recents animation was not running")
-                    .append(", trying to use default input consumer");
+            reasonString = newCompoundString("cannot start system gesture and recents "
+                    + "animation was not running, trying to use default input consumer");
             base = getDefaultInputConsumer(reasonString);
         }
         if (mDeviceState.isGesturalNavMode() || newGestureState.isTrackpadGesture()) {
@@ -1183,11 +1189,11 @@
             String reasonPrefix =
                     "device is in gesture navigation mode or 3-button mode with a trackpad gesture";
             if (mDeviceState.canTriggerAssistantAction(event)) {
-                reasonString.append(NEWLINE_PREFIX)
-                        .append(reasonPrefix)
-                        .append(SUBSTRING_PREFIX)
-                        .append("gesture can trigger the assistant")
-                        .append(", trying to use assistant input consumer");
+                reasonString.append("%s%s%sgesture can trigger the assistant, "
+                                + "trying to use assistant input consumer",
+                        NEWLINE_PREFIX,
+                        reasonPrefix,
+                        SUBSTRING_PREFIX);
                 base = tryCreateAssistantInputConsumer(base, newGestureState, event, reasonString);
             }
 
@@ -1198,11 +1204,11 @@
                         && !tac.isPhoneMode()
                         && !tac.isInStashedLauncherState();
                 if (canStartSystemGesture && useTaskbarConsumer) {
-                    reasonString.append(NEWLINE_PREFIX)
-                            .append(reasonPrefix)
-                            .append(SUBSTRING_PREFIX)
-                            .append("TaskbarActivityContext != null, ")
-                            .append("using TaskbarUnstashInputConsumer");
+                    reasonString.append("%s%s%sTaskbarActivityContext != null, "
+                                    + "using TaskbarUnstashInputConsumer",
+                            NEWLINE_PREFIX,
+                            reasonPrefix,
+                            SUBSTRING_PREFIX);
                     base = new TaskbarUnstashInputConsumer(this, base, mInputMonitorCompat, tac,
                             mOverviewCommandHelper, mGestureState);
                 }
@@ -1211,9 +1217,9 @@
                 // Create bubbles input consumer before NavHandleLongPressInputConsumer.
                 // This allows for nav handle to fall back to bubbles.
                 if (mDeviceState.isBubblesExpanded()) {
-                    reasonString = newCompoundString(reasonPrefix)
-                            .append(SUBSTRING_PREFIX)
-                            .append("bubbles expanded, trying to use default input consumer");
+                    reasonString = newCompoundString(reasonPrefix).append(
+                            "%sbubbles expanded, trying to use default input consumer",
+                            SUBSTRING_PREFIX);
                     // Bubbles can handle home gesture itself.
                     base = getDefaultInputConsumer(reasonString);
                 }
@@ -1224,10 +1230,10 @@
             if (canStartSystemGesture && !previousGestureState.isRecentsAnimationRunning()
                     && navHandle.canNavHandleBeLongPressed()
                     && !ignoreThreeFingerTrackpadForNavHandleLongPress(mGestureState)) {
-                reasonString.append(NEWLINE_PREFIX)
-                        .append(reasonPrefix)
-                        .append(SUBSTRING_PREFIX)
-                        .append("Not running recents animation, ");
+                reasonString.append("%s%s%sNot running recents animation, ",
+                                NEWLINE_PREFIX,
+                                reasonPrefix,
+                                SUBSTRING_PREFIX);
                 if (tac != null && tac.getNavHandle().canNavHandleBeLongPressed()) {
                     reasonString.append("stashed handle is long-pressable, ");
                 }
@@ -1239,74 +1245,74 @@
             if (!enableBubblesLongPressNavHandle()) {
                 // Continue overriding nav handle input consumer with bubbles
                 if (mDeviceState.isBubblesExpanded()) {
-                    reasonString = newCompoundString(reasonPrefix)
-                            .append(SUBSTRING_PREFIX)
-                            .append("bubbles expanded, trying to use default input consumer");
+                    reasonString = newCompoundString(reasonPrefix).append(
+                            "%sbubbles expanded, trying to use default input consumer",
+                            SUBSTRING_PREFIX);
                     // Bubbles can handle home gesture itself.
                     base = getDefaultInputConsumer(reasonString);
                 }
             }
 
             if (mDeviceState.isSystemUiDialogShowing()) {
-                reasonString = newCompoundString(reasonPrefix)
-                        .append(SUBSTRING_PREFIX)
-                        .append("system dialog is showing, using SysUiOverlayInputConsumer");
+                reasonString = newCompoundString(reasonPrefix).append(
+                        "%ssystem dialog is showing, using SysUiOverlayInputConsumer",
+                        SUBSTRING_PREFIX);
                 base = new SysUiOverlayInputConsumer(
                         getBaseContext(), mDeviceState, mInputMonitorCompat);
             }
 
             if (mGestureState.isTrackpadGesture()
                     && canStartSystemGesture && !previousGestureState.isRecentsAnimationRunning()) {
-                reasonString = newCompoundString(reasonPrefix)
-                        .append(SUBSTRING_PREFIX)
-                        .append("Trackpad 3-finger gesture, using TrackpadStatusBarInputConsumer");
+                reasonString = newCompoundString(reasonPrefix).append(
+                        "%sTrackpad 3-finger gesture, using TrackpadStatusBarInputConsumer",
+                        SUBSTRING_PREFIX);
                 base = new TrackpadStatusBarInputConsumer(getBaseContext(), base,
                         mInputMonitorCompat);
             }
 
             if (mDeviceState.isScreenPinningActive()) {
-                reasonString = newCompoundString(reasonPrefix)
-                        .append(SUBSTRING_PREFIX)
-                        .append("screen pinning is active, using ScreenPinnedInputConsumer");
+                reasonString = newCompoundString(reasonPrefix).append(
+                        "%sscreen pinning is active, using ScreenPinnedInputConsumer",
+                        SUBSTRING_PREFIX);
                 // Note: we only allow accessibility to wrap this, and it replaces the previous
                 // base input consumer (which should be NO_OP anyway since topTaskLocked == true).
                 base = new ScreenPinnedInputConsumer(this, newGestureState);
             }
 
             if (mDeviceState.canTriggerOneHandedAction(event)) {
-                reasonString.append(NEWLINE_PREFIX)
-                        .append(reasonPrefix)
-                        .append(SUBSTRING_PREFIX)
-                        .append("gesture can trigger one handed mode")
-                        .append(", using OneHandedModeInputConsumer");
+                reasonString.append("%s%s%sgesture can trigger one handed mode, "
+                                + "using OneHandedModeInputConsumer",
+                        NEWLINE_PREFIX,
+                        reasonPrefix,
+                        SUBSTRING_PREFIX);
                 base = new OneHandedModeInputConsumer(
                         this, mDeviceState, base, mInputMonitorCompat);
             }
 
             if (mDeviceState.isAccessibilityMenuAvailable()) {
-                reasonString.append(NEWLINE_PREFIX)
-                        .append(reasonPrefix)
-                        .append(SUBSTRING_PREFIX)
-                        .append("accessibility menu is available")
-                        .append(", using AccessibilityInputConsumer");
+                reasonString.append(
+                        "%s%s%saccessibility menu is available, using AccessibilityInputConsumer",
+                        NEWLINE_PREFIX,
+                        reasonPrefix,
+                        SUBSTRING_PREFIX);
                 base = new AccessibilityInputConsumer(
                         this, mDeviceState, mGestureState, base, mInputMonitorCompat);
             }
         } else {
             String reasonPrefix = "device is not in gesture navigation mode";
             if (mDeviceState.isScreenPinningActive()) {
-                reasonString = newCompoundString(reasonPrefix)
-                        .append(SUBSTRING_PREFIX)
-                        .append("screen pinning is active, trying to use default input consumer");
+                reasonString = newCompoundString(reasonPrefix).append(
+                        "%sscreen pinning is active, trying to use default input consumer",
+                        SUBSTRING_PREFIX);
                 base = getDefaultInputConsumer(reasonString);
             }
 
             if (mDeviceState.canTriggerOneHandedAction(event)) {
-                reasonString.append(NEWLINE_PREFIX)
-                        .append(reasonPrefix)
-                        .append(SUBSTRING_PREFIX)
-                        .append("gesture can trigger one handed mode")
-                        .append(", using OneHandedModeInputConsumer");
+                reasonString.append("%s%s%sgesture can trigger one handed mode, "
+                                + "using OneHandedModeInputConsumer",
+                        NEWLINE_PREFIX,
+                        reasonPrefix,
+                        SUBSTRING_PREFIX);
                 base = new OneHandedModeInputConsumer(
                         this, mDeviceState, base, mInputMonitorCompat);
             }
@@ -1316,7 +1322,7 @@
     }
 
     private CompoundString newCompoundString(String substring) {
-        return new CompoundString(NEWLINE_PREFIX).append(substring);
+        return new CompoundString("%s%s", NEWLINE_PREFIX, substring);
     }
 
     private boolean ignoreThreeFingerTrackpadForNavHandleLongPress(GestureState gestureState) {
@@ -1326,10 +1332,7 @@
 
     private void logInputConsumerSelectionReason(
             InputConsumer consumer, CompoundString reasonString) {
-        ActiveGestureLog.INSTANCE.addLog(new CompoundString("setInputConsumer: ")
-                .append(consumer.getName())
-                .append(". reason(s):")
-                .append(reasonString));
+        ActiveGestureProtoLogProxy.logSetInputConsumer(consumer.getName(), reasonString.toString());
         if ((consumer.getType() & InputConsumer.TYPE_OTHER_ACTIVITY) != 0) {
             ActiveGestureLog.INSTANCE.trackEvent(FLAG_USING_OTHER_ACTIVITY_INPUT_CONSUMER);
         }
@@ -1346,14 +1349,12 @@
             CompoundString reasonString) {
         if (mDeviceState.isKeyguardShowingOccluded()) {
             // This handles apps showing over the lockscreen (e.g. camera)
-            return createDeviceLockedInputConsumer(
-                    gestureState,
-                    reasonString.append(SUBSTRING_PREFIX)
-                            .append("keyguard is showing occluded")
-                            .append(", trying to use device locked input consumer"));
+            return createDeviceLockedInputConsumer(gestureState, reasonString.append(
+                    "%skeyguard is showing occluded, trying to use device locked input consumer",
+                    SUBSTRING_PREFIX));
         }
 
-        reasonString.append(SUBSTRING_PREFIX).append("keyguard is not showing occluded");
+        reasonString.append("%skeyguard is not showing occluded", SUBSTRING_PREFIX);
 
         TopTaskTracker.CachedTaskInfo runningTask = gestureState.getRunningTask();
         // Use overview input consumer for sharesheets on top of home.
@@ -1361,18 +1362,17 @@
                 && runningTask != null
                 && runningTask.isRootChooseActivity();
 
-        // In the case where we are in an excluded, translucent overlay, ignore it and treat the
-        // running activity as the task behind the overlay.
-        TopTaskTracker.CachedTaskInfo otherVisibleTask = runningTask == null
-                ? null
-                : runningTask.getVisibleNonExcludedTask();
-        if (otherVisibleTask != null) {
-            ActiveGestureLog.INSTANCE.addLog(new CompoundString("Changing active task to ")
-                    .append(otherVisibleTask.getPackageName())
-                    .append(" because the previous task running on top of this one (")
-                    .append(runningTask.getPackageName())
-                    .append(") was excluded from recents"));
-            gestureState.updateRunningTask(otherVisibleTask);
+        if (!com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+            // In the case where we are in an excluded, translucent overlay, ignore it and treat the
+            // running activity as the task behind the overlay.
+            TopTaskTracker.CachedTaskInfo otherVisibleTask = runningTask == null
+                    ? null
+                    : runningTask.getVisibleNonExcludedTask();
+            if (otherVisibleTask != null) {
+                ActiveGestureProtoLogProxy.logUpdateGestureStateRunningTask(
+                        otherVisibleTask.getPackageName(), runningTask.getPackageName());
+                gestureState.updateRunningTask(otherVisibleTask);
+            }
         }
 
         boolean previousGestureAnimatedToLauncher =
@@ -1397,11 +1397,12 @@
                     gestureState,
                     event,
                     forceOverviewInputConsumer,
-                    reasonString.append(SUBSTRING_PREFIX)
-                            .append("is in live tile mode, trying to use overview input consumer"));
+                    reasonString.append(
+                            "%sis in live tile mode, trying to use overview input consumer",
+                            SUBSTRING_PREFIX));
         } else if (runningTask == null) {
-            return getDefaultInputConsumer(reasonString.append(SUBSTRING_PREFIX)
-                    .append("running task == null"));
+            return getDefaultInputConsumer(reasonString.append(
+                    "%srunning task == null", SUBSTRING_PREFIX));
         } else if (previousGestureAnimatedToLauncher
                 || launcherResumedThroughShellTransition
                 || forceOverviewInputConsumer) {
@@ -1410,28 +1411,32 @@
                     gestureState,
                     event,
                     forceOverviewInputConsumer,
-                    reasonString.append(SUBSTRING_PREFIX)
-                            .append(previousGestureAnimatedToLauncher
-                                    ? "previous gesture animated to launcher"
+                    reasonString.append(previousGestureAnimatedToLauncher
+                                    ? "%sprevious gesture animated to launcher, "
+                                            + "trying to use overview input consumer"
                                     : (launcherResumedThroughShellTransition
-                                            ? "launcher resumed through a shell transition"
-                                            : "forceOverviewInputConsumer == true"))
-                            .append(", trying to use overview input consumer"));
+                                            ? "%slauncher resumed through a shell transition, "
+                                                    + "trying to use overview input consumer"
+                                            : "%sforceOverviewInputConsumer == true, "
+                                                    + "trying to use overview input consumer"),
+                            SUBSTRING_PREFIX));
         } else if (mDeviceState.isGestureBlockedTask(runningTask) || launcherChildActivityResumed) {
-            return getDefaultInputConsumer(reasonString.append(SUBSTRING_PREFIX)
-                    .append(launcherChildActivityResumed
-                            ? "is launcher child-task, trying to use default input consumer"
-                            : "is gesture-blocked task, trying to use default input consumer"));
+            return getDefaultInputConsumer(reasonString.append(launcherChildActivityResumed
+                    ? "%sis launcher child-task, trying to use default input consumer"
+                    : "%sis gesture-blocked task, trying to use default input consumer",
+                    SUBSTRING_PREFIX));
         } else {
-            reasonString.append(SUBSTRING_PREFIX)
-                    .append("using OtherActivityInputConsumer");
+            reasonString.append("%susing OtherActivityInputConsumer", SUBSTRING_PREFIX);
             return createOtherActivityInputConsumer(gestureState, event);
         }
     }
 
     public AbsSwipeUpHandler.Factory getSwipeUpHandlerFactory() {
-        return !mOverviewComponentObserver.isHomeAndOverviewSame()
-                ? mFallbackSwipeHandlerFactory : mLauncherSwipeHandlerFactory;
+        boolean recentsInWindow =
+                Flags.enableFallbackOverviewInWindow() || Flags.enableLauncherOverviewInWindow();
+        return mOverviewComponentObserver.isHomeAndOverviewSame()
+                ? mLauncherSwipeHandlerFactory : (recentsInWindow
+                ? mRecentsWindowSwipeHandlerFactory : mFallbackSwipeHandlerFactory);
     }
 
     private InputConsumer createOtherActivityInputConsumer(GestureState gestureState,
@@ -1450,20 +1455,18 @@
             GestureState gestureState, CompoundString reasonString) {
         if ((mDeviceState.isFullyGesturalNavMode() || gestureState.isTrackpadGesture())
                 && gestureState.getRunningTask() != null) {
-            reasonString.append(SUBSTRING_PREFIX)
-                    .append("device is in gesture nav mode or 3-button mode with a trackpad")
-                    .append(" gesture and running task != null")
-                    .append(", using DeviceLockedInputConsumer");
+            reasonString.append("%sdevice is in gesture nav mode or 3-button mode with a trackpad "
+                    + "gesture and running task != null, using DeviceLockedInputConsumer",
+                    SUBSTRING_PREFIX);
             return new DeviceLockedInputConsumer(
                     this, mDeviceState, mTaskAnimationManager, gestureState, mInputMonitorCompat);
         } else {
-            return getDefaultInputConsumer(reasonString
-                    .append(SUBSTRING_PREFIX)
-                    .append((mDeviceState.isFullyGesturalNavMode()
-                                    || gestureState.isTrackpadGesture())
-                            ? "running task == null"
-                            : "device is not in gesture nav mode and it's not a trackpad gesture")
-                    .append(", trying to use default input consumer"));
+            return getDefaultInputConsumer(reasonString.append(
+                    mDeviceState.isFullyGesturalNavMode() || gestureState.isTrackpadGesture()
+                            ? "%srunning task == null, trying to use default input consumer"
+                            : "%sdevice is not in gesture nav mode and it's not a trackpad gesture,"
+                                    + " trying to use default input consumer",
+                    SUBSTRING_PREFIX));
         }
     }
 
@@ -1475,35 +1478,35 @@
             CompoundString reasonString) {
         RecentsViewContainer container = gestureState.getContainerInterface().getCreatedContainer();
         if (container == null) {
-            return getDefaultInputConsumer(
-                    reasonString.append(SUBSTRING_PREFIX)
-                            .append("activity == null, trying to use default input consumer"));
+            return getDefaultInputConsumer(reasonString.append(
+                    "%sactivity == null, trying to use default input consumer", SUBSTRING_PREFIX));
         }
 
-        boolean hasWindowFocus = container.getRootView().hasWindowFocus();
+        View rootview = container.getRootView();
+        boolean hasWindowFocus = rootview != null && rootview.hasWindowFocus();
         boolean isPreviousGestureAnimatingToLauncher =
                 previousGestureState.isRunningAnimationToLauncher()
                         || mDeviceState.isPredictiveBackToHomeInProgress();
         boolean isInLiveTileMode = gestureState.getContainerInterface().isInLiveTileMode();
 
-        reasonString.append(SUBSTRING_PREFIX)
-                .append(hasWindowFocus
-                        ? "activity has window focus"
-                        : (isPreviousGestureAnimatingToLauncher
-                                ? "previous gesture is still animating to launcher"
-                                : isInLiveTileMode
-                                        ? "device is in live mode"
-                                        : "all overview focus conditions failed"));
+        reasonString.append(hasWindowFocus
+                ? "%sactivity has window focus"
+                : (isPreviousGestureAnimatingToLauncher
+                        ? "%sprevious gesture is still animating to launcher"
+                        : isInLiveTileMode
+                                ? "%sdevice is in live mode"
+                                : "%sall overview focus conditions failed"), SUBSTRING_PREFIX);
         if (hasWindowFocus
                 || isPreviousGestureAnimatingToLauncher
                 || isInLiveTileMode) {
-            reasonString.append(SUBSTRING_PREFIX)
-                    .append("overview should have focus, using OverviewInputConsumer");
+            reasonString.append(
+                    "%soverview should have focus, using OverviewInputConsumer", SUBSTRING_PREFIX);
             return new OverviewInputConsumer(gestureState, container, mInputMonitorCompat,
                     false /* startingInActivityBounds */);
         } else {
-            reasonString.append(SUBSTRING_PREFIX).append(
-                    "overview shouldn't have focus, using OverviewWithoutFocusInputConsumer");
+            reasonString.append(
+                    "%soverview shouldn't have focus, using OverviewWithoutFocusInputConsumer",
+                    SUBSTRING_PREFIX);
             final boolean disableHorizontalSwipe = mDeviceState.isInExclusionRegion(event);
             return new OverviewWithoutFocusInputConsumer(container.asContext(), mDeviceState,
                     gestureState, mInputMonitorCompat, disableHorizontalSwipe);
@@ -1540,12 +1543,14 @@
      */
     private @NonNull InputConsumer getDefaultInputConsumer(@NonNull CompoundString reasonString) {
         if (mResetGestureInputConsumer != null) {
-            reasonString.append(SUBSTRING_PREFIX).append(
-                    "mResetGestureInputConsumer initialized, using ResetGestureInputConsumer");
+            reasonString.append(
+                    "%smResetGestureInputConsumer initialized, using ResetGestureInputConsumer",
+                    SUBSTRING_PREFIX);
             return mResetGestureInputConsumer;
         } else {
-            reasonString.append(SUBSTRING_PREFIX).append(
-                    "mResetGestureInputConsumer not initialized, using no-op input consumer");
+            reasonString.append(
+                    "%smResetGestureInputConsumer not initialized, using no-op input consumer",
+                    SUBSTRING_PREFIX);
             // mResetGestureInputConsumer isn't initialized until onUserUnlocked(), so reset to
             // NO_OP until then (we never want these to be null).
             return InputConsumer.NO_OP;
@@ -1575,11 +1580,11 @@
             return;
         }
 
-        final BaseActivityInterface activityInterface =
-                mOverviewComponentObserver.getActivityInterface();
+        final BaseContainerInterface containerInterface =
+                mOverviewComponentObserver.getContainerInterface();
         final Intent overviewIntent = new Intent(
                 mOverviewComponentObserver.getOverviewIntentIgnoreSysUiState());
-        if (activityInterface.getCreatedContainer() != null && fromInit) {
+        if (containerInterface.getCreatedContainer() != null && fromInit) {
             // The activity has been created before the initialization of overview service. It is
             // usually happens when booting or launcher is the top activity, so we should already
             // have the latest state.
@@ -1589,8 +1594,7 @@
         // TODO(b/258022658): Remove temporary logging.
         Log.i(TAG, "preloadOverview: forSUWAllSet=" + forSUWAllSet
                 + ", isHomeAndOverviewSame=" + mOverviewComponentObserver.isHomeAndOverviewSame());
-
-        ActiveGestureLog.INSTANCE.addLog("preloadRecentsAnimation");
+        ActiveGestureProtoLogProxy.logPreloadRecentsAnimation();
         mTaskAnimationManager.preloadRecentsAnimation(overviewIntent);
     }
 
@@ -1599,18 +1603,18 @@
         if (!LockedUserState.get(this).isUserUnlocked()) {
             return;
         }
-        final BaseActivityInterface activityInterface =
-                mOverviewComponentObserver.getActivityInterface();
-        final BaseDraggingActivity activity = activityInterface.getCreatedContainer();
-        if (activity == null || activity.isStarted()) {
+        final BaseContainerInterface containerInterface =
+                mOverviewComponentObserver.getContainerInterface();
+        final RecentsViewContainer container = containerInterface.getCreatedContainer();
+        if (container == null || container.isStarted()) {
             // We only care about the existing background activity.
             return;
         }
-        Configuration oldConfig = activity.getResources().getConfiguration();
+        Configuration oldConfig = container.asContext().getResources().getConfiguration();
         boolean isFoldUnfold = isTablet(oldConfig) != isTablet(newConfig);
         if (!isFoldUnfold && mOverviewComponentObserver.canHandleConfigChanges(
-                activity.getComponentName(),
-                activity.getResources().getConfiguration().diff(newConfig))) {
+                container.getComponentName(),
+                container.asContext().getResources().getConfiguration().diff(newConfig))) {
             // Since navBar gestural height are different between portrait and landscape,
             // can handle orientation changes and refresh navigation gestural region through
             // onOneHandedModeChanged()
@@ -1649,11 +1653,11 @@
         pw.println("\tmInputEventReceiver=" + mInputEventReceiver);
         DisplayController.INSTANCE.get(this).dump(pw);
         pw.println("TouchState:");
-        BaseDraggingActivity createdOverviewActivity = mOverviewComponentObserver == null ? null
-                : mOverviewComponentObserver.getActivityInterface().getCreatedContainer();
+        RecentsViewContainer createdOverviewContainer = mOverviewComponentObserver == null ? null
+                : mOverviewComponentObserver.getContainerInterface().getCreatedContainer();
         boolean resumed = mOverviewComponentObserver != null
-                && mOverviewComponentObserver.getActivityInterface().isResumed();
-        pw.println("\tcreatedOverviewActivity=" + createdOverviewActivity);
+                && mOverviewComponentObserver.getContainerInterface().isResumed();
+        pw.println("\tcreatedOverviewActivity=" + createdOverviewContainer);
         pw.println("\tresumed=" + resumed);
         pw.println("\tmConsumer=" + mConsumer.getName());
         ActiveGestureLog.INSTANCE.dump("", pw);
@@ -1661,15 +1665,16 @@
         if (mTaskAnimationManager != null) {
             mTaskAnimationManager.dump("", pw);
         }
-        if (createdOverviewActivity != null) {
-            createdOverviewActivity.getDeviceProfile().dump(this, "", pw);
+        if (createdOverviewContainer != null) {
+            createdOverviewContainer.getDeviceProfile().dump(this, "", pw);
         }
         mTaskbarManager.dumpLogs("", pw);
         mDesktopVisibilityController.dumpLogs("", pw);
-        pw.println("AssistStateManager:");
-        AssistStateManager.INSTANCE.get(this).dump("\t", pw);
+        pw.println("ContextualSearchStateManager:");
+        ContextualSearchStateManager.INSTANCE.get(this).dump("\t", pw);
         SystemUiProxy.INSTANCE.get(this).dump(pw);
         DeviceConfigWrapper.get().dump("   ", pw);
+        TopTaskTracker.INSTANCE.get(this).dump(pw);
     }
 
     private AbsSwipeUpHandler createLauncherSwipeHandler(
@@ -1685,4 +1690,11 @@
                 gestureState, touchTimeMs, mTaskAnimationManager.isRecentsAnimationRunning(),
                 mInputConsumer);
     }
+
+    private AbsSwipeUpHandler createRecentsWindowSwipeHandler(
+            GestureState gestureState, long touchTimeMs) {
+        return new RecentsWindowSwipeHandler(this, mDeviceState, mTaskAnimationManager,
+                gestureState, touchTimeMs, mTaskAnimationManager.isRecentsAnimationRunning(),
+                mInputConsumer, mRecentsWindowManager);
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/ViewUtils.java b/quickstep/src/com/android/quickstep/ViewUtils.java
index 3b58dfc..cf6b04e 100644
--- a/quickstep/src/com/android/quickstep/ViewUtils.java
+++ b/quickstep/src/com/android/quickstep/ViewUtils.java
@@ -23,6 +23,7 @@
 
 import com.android.launcher3.Utilities;
 
+import java.util.ArrayList;
 import java.util.function.BooleanSupplier;
 
 /**
@@ -129,4 +130,18 @@
             }
         }
     }
+
+    /**
+     * Adds the view to the list of accessible children.
+     *
+     * @param view The view to add.
+     * @param outChildren The list of accessible children.
+     */
+    public static void addAccessibleChildToList(View view, ArrayList<View> outChildren) {
+        if (view.includeForAccessibility()) {
+            outChildren.add(view);
+        } else {
+            view.addChildrenForAccessibility(outChildren);
+        }
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/contextualeducation/SystemContextualEduStatsManager.java b/quickstep/src/com/android/quickstep/contextualeducation/SystemContextualEduStatsManager.java
index d470b88..6a72537 100644
--- a/quickstep/src/com/android/quickstep/contextualeducation/SystemContextualEduStatsManager.java
+++ b/quickstep/src/com/android/quickstep/contextualeducation/SystemContextualEduStatsManager.java
@@ -16,29 +16,28 @@
 
 package com.android.quickstep.contextualeducation;
 
-import android.content.Context;
-
 import com.android.launcher3.contextualeducation.ContextualEduStatsManager;
+import com.android.launcher3.dagger.LauncherAppSingleton;
 import com.android.quickstep.SystemUiProxy;
 import com.android.systemui.contextualeducation.GestureType;
 
+import javax.inject.Inject;
+
 /**
  * A class to update contextual education data via {@link SystemUiProxy}
  */
+@LauncherAppSingleton
 public class SystemContextualEduStatsManager extends ContextualEduStatsManager {
-    private Context mContext;
+    private final SystemUiProxy mSystemUiProxy;
 
-    public SystemContextualEduStatsManager(Context context) {
-        mContext = context;
+    @Inject
+    public SystemContextualEduStatsManager(SystemUiProxy systemUiProxy) {
+        mSystemUiProxy = systemUiProxy;
     }
 
     @Override
     public void updateEduStats(boolean isTrackpadGesture, GestureType gestureType) {
-        SystemUiProxy.INSTANCE.get(mContext).updateContextualEduStats(isTrackpadGesture,
+        mSystemUiProxy.updateContextualEduStats(isTrackpadGesture,
                 gestureType.name());
     }
-
-    @Override
-    public void close() {
-    }
 }
diff --git a/quickstep/src/com/android/quickstep/dagger/QuickStepModule.java b/quickstep/src/com/android/quickstep/dagger/QuickStepModule.java
index 08345b8..9f6360b 100644
--- a/quickstep/src/com/android/quickstep/dagger/QuickStepModule.java
+++ b/quickstep/src/com/android/quickstep/dagger/QuickStepModule.java
@@ -15,8 +15,21 @@
  */
 package com.android.quickstep.dagger;
 
+import com.android.launcher3.contextualeducation.ContextualEduStatsManager;
+import com.android.launcher3.uioverrides.SystemApiWrapper;
+import com.android.launcher3.uioverrides.plugins.PluginManagerWrapperImpl;
+import com.android.launcher3.util.ApiWrapper;
+import com.android.launcher3.util.PluginManagerWrapper;
+import com.android.quickstep.contextualeducation.SystemContextualEduStatsManager;
+
+import dagger.Binds;
 import dagger.Module;
 
 @Module
-public class QuickStepModule {
+public abstract class QuickStepModule {
+
+    @Binds abstract PluginManagerWrapper bindPluginManagerWrapper(PluginManagerWrapperImpl impl);
+    @Binds abstract ApiWrapper bindApiWrapper(SystemApiWrapper systemApiWrapper);
+    @Binds abstract ContextualEduStatsManager bindContextualEduStatsManager(
+            SystemContextualEduStatsManager manager);
 }
diff --git a/quickstep/src/com/android/quickstep/dagger/QuickstepBaseAppComponent.java b/quickstep/src/com/android/quickstep/dagger/QuickstepBaseAppComponent.java
index f2d5715..b2670e8 100644
--- a/quickstep/src/com/android/quickstep/dagger/QuickstepBaseAppComponent.java
+++ b/quickstep/src/com/android/quickstep/dagger/QuickstepBaseAppComponent.java
@@ -18,7 +18,9 @@
 
 import com.android.launcher3.dagger.LauncherAppComponent;
 import com.android.launcher3.dagger.LauncherBaseAppComponent;
-import com.android.quickstep.logging.SettingsChangeLogger;
+import com.android.launcher3.model.WellbeingModel;
+import com.android.quickstep.SystemUiProxy;
+import com.android.quickstep.util.AsyncClockEventDelegate;
 
 /**
  * Launcher Quickstep base component for Dagger injection.
@@ -29,5 +31,10 @@
  * See {@link LauncherAppComponent} for the one actually used.
  */
 public interface QuickstepBaseAppComponent extends LauncherBaseAppComponent {
-    SettingsChangeLogger getSettingsChangeLogger();
+
+    WellbeingModel getWellbeingModel();
+
+    AsyncClockEventDelegate getAsyncClockEventDelegate();
+
+    SystemUiProxy getSystemUiProxy();
 }
diff --git a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java
index 94764a5..daac9fb 100644
--- a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java
+++ b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java
@@ -18,6 +18,7 @@
 import static com.android.app.animation.Interpolators.FINAL_FRAME;
 import static com.android.app.animation.Interpolators.INSTANT;
 import static com.android.app.animation.Interpolators.LINEAR;
+import static com.android.launcher3.Flags.enableLargeDesktopWindowingTile;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_MODAL;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SCALE;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_TRANSLATE_X;
@@ -26,6 +27,7 @@
 import static com.android.launcher3.states.StateAnimationConfig.SKIP_OVERVIEW;
 import static com.android.quickstep.fallback.RecentsState.OVERVIEW_SPLIT_SELECT;
 import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_HORIZONTAL_OFFSET;
+import static com.android.quickstep.views.RecentsView.DESKTOP_CAROUSEL_DETACH_PROGRESS;
 import static com.android.quickstep.views.RecentsView.FULLSCREEN_PROGRESS;
 import static com.android.quickstep.views.RecentsView.RECENTS_GRID_PROGRESS;
 import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY;
@@ -47,9 +49,9 @@
 import com.android.launcher3.anim.PropertySetter;
 import com.android.launcher3.statemanager.StateManager.StateHandler;
 import com.android.launcher3.states.StateAnimationConfig;
-import com.android.quickstep.RecentsActivity;
 import com.android.quickstep.views.ClearAllButton;
 import com.android.quickstep.views.RecentsView;
+import com.android.quickstep.views.RecentsViewContainer;
 
 /**
  * State controller for fallback recents activity
@@ -57,12 +59,12 @@
 public class FallbackRecentsStateController implements StateHandler<RecentsState> {
 
     private final StateAnimationConfig mNoConfig = new StateAnimationConfig();
-    private final RecentsActivity mActivity;
+    private final RecentsViewContainer mRecentsViewContainer;
     private final FallbackRecentsView mRecentsView;
 
-    public FallbackRecentsStateController(RecentsActivity activity) {
-        mActivity = activity;
-        mRecentsView = activity.getOverviewPanel();
+    public FallbackRecentsStateController(RecentsViewContainer container) {
+        mRecentsViewContainer = container;
+        mRecentsView = container.getOverviewPanel();
     }
 
     @Override
@@ -81,7 +83,7 @@
         // While animating into recents, update the visible task data as needed
         setter.addOnFrameCallback(() -> mRecentsView.loadVisibleTaskData(FLAG_UPDATE_ALL));
         setter.addEndListener(success -> {
-            if (!success) {
+            if (!success && !toState.isRecentsViewVisible()) {
                 mRecentsView.reset();
             }
         });
@@ -96,10 +98,10 @@
         setter.setFloat(mRecentsView.getClearAllButton(), ClearAllButton.VISIBILITY_ALPHA,
                 clearAllButtonAlpha, LINEAR);
         float overviewButtonAlpha = state.hasOverviewActions() ? 1 : 0;
-        setter.setFloat(mActivity.getActionsView().getVisibilityAlpha(),
+        setter.setFloat(mRecentsViewContainer.getActionsView().getVisibilityAlpha(),
                 AnimatedFloat.VALUE, overviewButtonAlpha, LINEAR);
 
-        float[] scaleAndOffset = state.getOverviewScaleAndOffset(mActivity);
+        float[] scaleAndOffset = state.getOverviewScaleAndOffset(mRecentsViewContainer);
         setter.setFloat(mRecentsView, RECENTS_SCALE_PROPERTY, scaleAndOffset[0],
                 config.getInterpolator(ANIM_OVERVIEW_SCALE, LINEAR));
         setter.setFloat(mRecentsView, ADJACENT_PAGE_HORIZONTAL_OFFSET, scaleAndOffset[1],
@@ -110,16 +112,24 @@
         setter.setFloat(mRecentsView, TASK_MODALNESS, state.getOverviewModalness(),
                 config.getInterpolator(ANIM_OVERVIEW_MODAL, LINEAR));
         setter.setFloat(mRecentsView, FULLSCREEN_PROGRESS, state.isFullScreen() ? 1 : 0, LINEAR);
-        boolean showAsGrid = state.displayOverviewTasksAsGrid(mActivity.getDeviceProfile());
+        boolean showAsGrid =
+                state.displayOverviewTasksAsGrid(mRecentsViewContainer.getDeviceProfile());
         setter.setFloat(mRecentsView, RECENTS_GRID_PROGRESS, showAsGrid ? 1f : 0f,
                 getOverviewInterpolator(state));
         setter.setFloat(mRecentsView, TASK_THUMBNAIL_SPLASH_ALPHA,
                 state.showTaskThumbnailSplash() ? 1f : 0f, getOverviewInterpolator(state));
+        if (enableLargeDesktopWindowingTile()) {
+            setter.setFloat(mRecentsView, DESKTOP_CAROUSEL_DETACH_PROGRESS,
+                    state.detachDesktopCarousel() ? 1f : 0f,
+                    getOverviewInterpolator(state));
+        }
 
-        setter.setViewBackgroundColor(mActivity.getScrimView(), state.getScrimColor(mActivity),
+        setter.setViewBackgroundColor(mRecentsViewContainer.getScrimView(),
+                state.getScrimColor(mRecentsViewContainer.asContext()),
                 config.getInterpolator(ANIM_SCRIM_FADE, LINEAR));
         if (isSplitSelectionState(state)) {
-            int duration = state.getTransitionDuration(mActivity, true /* isToState */);
+            int duration =
+                    state.getTransitionDuration(mRecentsViewContainer.asContext(), true);
             // TODO (b/246851887): Pass in setter as a NO_ANIM PendingAnimation instead
             PendingAnimation pa = new PendingAnimation(duration);
             mRecentsView.createSplitSelectInitAnimation(pa, duration);
@@ -129,7 +139,7 @@
         Pair<FloatProperty<RecentsView>, FloatProperty<RecentsView>> taskViewsFloat =
                 mRecentsView.getPagedOrientationHandler().getSplitSelectTaskOffset(
                         TASK_PRIMARY_SPLIT_TRANSLATION, TASK_SECONDARY_SPLIT_TRANSLATION,
-                        mActivity.getDeviceProfile());
+                        mRecentsViewContainer.getDeviceProfile());
         setter.setFloat(mRecentsView, taskViewsFloat.first, isSplitSelectionState(state)
                 ? mRecentsView.getSplitSelectTranslation() : 0, LINEAR);
         setter.setFloat(mRecentsView, taskViewsFloat.second, 0, LINEAR);
diff --git a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
index e67a9bc..daad6b7 100644
--- a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
+++ b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
@@ -19,7 +19,6 @@
 
 import static com.android.quickstep.GestureState.GestureEndTarget.RECENTS;
 import static com.android.quickstep.fallback.RecentsState.DEFAULT;
-import static com.android.quickstep.fallback.RecentsState.HOME;
 import static com.android.quickstep.fallback.RecentsState.MODAL_TASK;
 import static com.android.quickstep.fallback.RecentsState.OVERVIEW_SPLIT_SELECT;
 
@@ -31,6 +30,7 @@
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.Flags;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.config.FeatureFlags;
@@ -38,17 +38,20 @@
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.statemanager.StateManager;
 import com.android.launcher3.statemanager.StateManager.StateListener;
+import com.android.launcher3.statemanager.StatefulContainer;
 import com.android.launcher3.util.SplitConfigurationOptions;
 import com.android.launcher3.util.SplitConfigurationOptions.SplitSelectSource;
+import com.android.quickstep.BaseContainerInterface;
 import com.android.quickstep.FallbackActivityInterface;
+import com.android.quickstep.FallbackWindowInterface;
 import com.android.quickstep.GestureState;
-import com.android.quickstep.RecentsActivity;
 import com.android.quickstep.RotationTouchHelper;
 import com.android.quickstep.util.GroupTask;
 import com.android.quickstep.util.SplitSelectStateController;
 import com.android.quickstep.util.TaskViewSimulator;
 import com.android.quickstep.views.OverviewActionsView;
 import com.android.quickstep.views.RecentsView;
+import com.android.quickstep.views.RecentsViewContainer;
 import com.android.quickstep.views.TaskView;
 import com.android.systemui.shared.recents.model.Task;
 
@@ -56,7 +59,8 @@
 import java.util.Arrays;
 import java.util.List;
 
-public class FallbackRecentsView extends RecentsView<RecentsActivity, RecentsState>
+public class FallbackRecentsView<CONTAINER_TYPE extends Context & RecentsViewContainer
+        & StatefulContainer<RecentsState>> extends RecentsView<CONTAINER_TYPE, RecentsState>
         implements StateListener<RecentsState> {
 
     private static final int TASK_DISMISS_DURATION = 150;
@@ -69,10 +73,16 @@
     }
 
     public FallbackRecentsView(Context context, AttributeSet attrs, int defStyleAttr) {
-        super(context, attrs, defStyleAttr, FallbackActivityInterface.INSTANCE);
+        super(context, attrs, defStyleAttr, getContainerInterface());
         mContainer.getStateManager().addStateListener(this);
     }
 
+    private static BaseContainerInterface<RecentsState, ?> getContainerInterface() {
+        return (Flags.enableFallbackOverviewInWindow() || Flags.enableLauncherOverviewInWindow())
+                ? FallbackWindowInterface.getInstance()
+                : FallbackActivityInterface.INSTANCE;
+    }
+
     @Override
     public void init(OverviewActionsView actionsView, SplitSelectStateController splitController,
             @Nullable DesktopRecentsTransitionController desktopRecentsTransitionController) {
@@ -93,7 +103,7 @@
     }
 
     @Override
-    public StateManager<RecentsState, RecentsActivity> getStateManager() {
+    public StateManager<RecentsState, ?> getStateManager() {
         return mContainer.getStateManager();
     }
 
@@ -260,7 +270,7 @@
 
     @Override
     public void onStateTransitionComplete(RecentsState finalState) {
-        if (finalState == HOME) {
+        if (!finalState.isRecentsViewVisible()) {
             // Clean-up logic that occurs when recents is no longer in use/visible.
             reset();
         }
@@ -283,7 +293,9 @@
             }
         }
 
-        if (isOverlayEnabled) {
+        // disabling this so app icons aren't drawn on top of recent tasks.
+        if (isOverlayEnabled && !(Flags.enableFallbackOverviewInWindow()
+                || Flags.enableLauncherOverviewInWindow())) {
             runActionOnRemoteHandles(remoteTargetHandle ->
                     remoteTargetHandle.getTaskViewSimulator().setDrawsBelowRecents(true));
         }
@@ -312,7 +324,7 @@
     }
 
     @Override
-    protected boolean canLaunchFullscreenTask() {
+    public boolean canLaunchFullscreenTask() {
         return !mContainer.isInState(OVERVIEW_SPLIT_SELECT);
     }
 
diff --git a/quickstep/src/com/android/quickstep/fallback/RecentsDragLayer.java b/quickstep/src/com/android/quickstep/fallback/RecentsDragLayer.java
index 29c3dc8..a2884b6 100644
--- a/quickstep/src/com/android/quickstep/fallback/RecentsDragLayer.java
+++ b/quickstep/src/com/android/quickstep/fallback/RecentsDragLayer.java
@@ -20,13 +20,12 @@
 
 import com.android.launcher3.util.TouchController;
 import com.android.launcher3.views.BaseDragLayer;
-import com.android.quickstep.RecentsActivity;
+import com.android.quickstep.views.RecentsViewContainer;
 
 /**
  * Drag layer for fallback recents activity
  */
-public class RecentsDragLayer extends BaseDragLayer<RecentsActivity> {
-
+public class RecentsDragLayer<T extends Context & RecentsViewContainer> extends BaseDragLayer<T> {
     public RecentsDragLayer(Context context, AttributeSet attrs) {
         super(context, attrs, 1 /* alphaChannelCount */);
     }
@@ -34,8 +33,8 @@
     @Override
     public void recreateControllers() {
         mControllers = new TouchController[] {
-                new RecentsTaskController(mActivity),
-                new FallbackNavBarTouchController(mActivity),
+                new RecentsTaskController(mContainer),
+                new FallbackNavBarTouchController(mContainer),
         };
     }
 }
diff --git a/quickstep/src/com/android/quickstep/fallback/RecentsState.java b/quickstep/src/com/android/quickstep/fallback/RecentsState.java
index ca9753f..34783c7 100644
--- a/quickstep/src/com/android/quickstep/fallback/RecentsState.java
+++ b/quickstep/src/com/android/quickstep/fallback/RecentsState.java
@@ -15,6 +15,7 @@
  */
 package com.android.quickstep.fallback;
 
+import static com.android.launcher3.Flags.enableDesktopWindowingCarouselDetach;
 import static com.android.launcher3.LauncherState.FLAG_CLOSE_POPUPS;
 import static com.android.launcher3.uioverrides.states.BackgroundAppState.getOverviewScaleAndOffsetForBackgroundState;
 import static com.android.launcher3.uioverrides.states.OverviewModalTaskState.getOverviewScaleAndOffsetForModalState;
@@ -42,6 +43,7 @@
     private static final int FLAG_LIVE_TILE = BaseState.getFlag(6);
     private static final int FLAG_RECENTS_VIEW_VISIBLE = BaseState.getFlag(7);
     private static final int FLAG_TASK_THUMBNAIL_SPLASH = BaseState.getFlag(8);
+    private static final int FLAG_DETACH_DESKTOP_CAROUSEL = BaseState.getFlag(9);
 
     public static final RecentsState DEFAULT = new RecentsState(0,
             FLAG_DISABLE_RESTORE | FLAG_CLEAR_ALL_BUTTON | FLAG_OVERVIEW_ACTIONS | FLAG_SHOW_AS_GRID
@@ -51,8 +53,8 @@
                     | FLAG_SHOW_AS_GRID | FLAG_SCRIM | FLAG_LIVE_TILE | FLAG_RECENTS_VIEW_VISIBLE);
     public static final RecentsState BACKGROUND_APP = new BackgroundAppState(2,
             FLAG_DISABLE_RESTORE | FLAG_NON_INTERACTIVE | FLAG_FULL_SCREEN
-                    | FLAG_RECENTS_VIEW_VISIBLE
-                    | FLAG_TASK_THUMBNAIL_SPLASH);
+                    | FLAG_RECENTS_VIEW_VISIBLE | FLAG_TASK_THUMBNAIL_SPLASH
+                    | FLAG_DETACH_DESKTOP_CAROUSEL);
     public static final RecentsState HOME = new RecentsState(3, 0);
     public static final RecentsState BG_LAUNCHER = new LauncherState(4, 0);
     public static final RecentsState OVERVIEW_SPLIT_SELECT = new RecentsState(5,
@@ -149,6 +151,11 @@
         return hasFlag(FLAG_TASK_THUMBNAIL_SPLASH);
     }
 
+    @Override
+    public boolean detachDesktopCarousel() {
+        return hasFlag(FLAG_DETACH_DESKTOP_CAROUSEL) && enableDesktopWindowingCarouselDetach();
+    }
+
     /**
      * True if the state has overview panel visible.
      */
diff --git a/quickstep/src/com/android/quickstep/fallback/RecentsTaskController.java b/quickstep/src/com/android/quickstep/fallback/RecentsTaskController.java
index 2cb398c..07da379 100644
--- a/quickstep/src/com/android/quickstep/fallback/RecentsTaskController.java
+++ b/quickstep/src/com/android/quickstep/fallback/RecentsTaskController.java
@@ -15,18 +15,22 @@
  */
 package com.android.quickstep.fallback;
 
+import android.content.Context;
+
+import com.android.launcher3.statemanager.StatefulContainer;
 import com.android.launcher3.uioverrides.touchcontrollers.TaskViewTouchController;
-import com.android.quickstep.RecentsActivity;
+import com.android.quickstep.views.RecentsViewContainer;
 
-public class RecentsTaskController extends TaskViewTouchController<RecentsActivity> {
-
-    public RecentsTaskController(RecentsActivity activity) {
-        super(activity);
+public class RecentsTaskController<T extends Context & RecentsViewContainer &
+        StatefulContainer<RecentsState>> extends TaskViewTouchController<T> {
+    public RecentsTaskController(T container) {
+        super(container);
     }
 
     @Override
     protected boolean isRecentsInteractive() {
-        return mContainer.hasWindowFocus() || mContainer.getStateManager().getState().hasLiveTile();
+        return mContainer.getRootView().hasWindowFocus()
+                || mContainer.getStateManager().getState().hasLiveTile();
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowContext.kt b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowContext.kt
new file mode 100644
index 0000000..52a7682
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowContext.kt
@@ -0,0 +1,99 @@
+/*
+ * 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.fallback.window
+
+import android.content.Context
+import android.graphics.PixelFormat
+import android.view.ContextThemeWrapper
+import android.view.ViewGroup
+import android.view.WindowManager
+import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
+import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_CONSUME_IME_INSETS
+import com.android.launcher3.DeviceProfile
+import com.android.launcher3.InvariantDeviceProfile
+import com.android.launcher3.util.Themes
+import com.android.launcher3.views.ActivityContext
+import com.android.launcher3.views.BaseDragLayer
+import com.android.quickstep.fallback.RecentsDragLayer
+
+/**
+ * Window context for the Overview overlays.
+ * <p>
+ * Overlays have their own window and need a window context.
+ */
+open class RecentsWindowContext(windowContext: Context) :
+    ContextThemeWrapper(windowContext, Themes.getActivityThemeRes(windowContext)), ActivityContext {
+
+    private var deviceProfile: DeviceProfile? = null
+    private var dragLayer: RecentsDragLayer<RecentsWindowManager> = RecentsDragLayer(this, null)
+    private val deviceProfileChangeListeners:
+        MutableList<DeviceProfile.OnDeviceProfileChangeListener> =
+        ArrayList()
+
+    private val windowTitle: String = "RecentsWindow"
+
+    protected var windowLayoutParams: WindowManager.LayoutParams? =
+        createDefaultWindowLayoutParams(
+            WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, windowTitle)
+
+    override fun getDragLayer(): BaseDragLayer<RecentsWindowManager> {
+        return dragLayer
+    }
+
+    override fun getDeviceProfile(): DeviceProfile {
+        if (deviceProfile == null) {
+            deviceProfile = InvariantDeviceProfile.INSTANCE[this].getDeviceProfile(this)
+                .copy(this)
+        }
+        return deviceProfile!!
+    }
+
+    override fun getOnDeviceProfileChangeListeners():
+        List<DeviceProfile.OnDeviceProfileChangeListener> {
+        return deviceProfileChangeListeners
+    }
+
+    /**
+     * Creates LayoutParams for adding a view directly to WindowManager as a new window.
+     *
+     * @param type The window type to pass to the created WindowManager.LayoutParams.
+     * @param title The window title to pass to the created WindowManager.LayoutParams.
+     */
+    fun createDefaultWindowLayoutParams(type: Int, title: String): WindowManager.LayoutParams {
+        var windowFlags =
+            (WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS or
+                WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS or
+                WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED)
+
+        val windowLayoutParams =
+            WindowManager.LayoutParams(
+                ViewGroup.LayoutParams.MATCH_PARENT,
+                ViewGroup.LayoutParams.MATCH_PARENT,
+                type,
+                windowFlags,
+                PixelFormat.TRANSLUCENT,
+            )
+
+        windowLayoutParams.title = title
+        windowLayoutParams.fitInsetsTypes = 0
+        windowLayoutParams.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
+        windowLayoutParams.isSystemApplicationOverlay = true
+        windowLayoutParams.privateFlags = PRIVATE_FLAG_CONSUME_IME_INSETS
+
+        return windowLayoutParams
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowManager.kt b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowManager.kt
new file mode 100644
index 0000000..843ef6c
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowManager.kt
@@ -0,0 +1,451 @@
+/*
+ * 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.fallback.window
+
+import android.animation.AnimatorSet
+import android.app.ActivityOptions
+import android.content.ComponentName
+import android.content.Context
+import android.content.LocusId
+import android.os.Bundle
+import android.view.KeyEvent
+import android.view.LayoutInflater
+import android.view.MotionEvent
+import android.view.RemoteAnimationAdapter
+import android.view.RemoteAnimationTarget
+import android.view.SurfaceControl
+import android.view.View
+import android.view.WindowManager
+import android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
+import android.window.RemoteTransition
+import com.android.launcher3.BaseActivity
+import com.android.launcher3.LauncherAnimationRunner
+import com.android.launcher3.LauncherAnimationRunner.RemoteAnimationFactory
+import com.android.launcher3.R
+import com.android.launcher3.statehandlers.DesktopVisibilityController
+import com.android.launcher3.statemanager.StateManager
+import com.android.launcher3.statemanager.StateManager.AtomicAnimationFactory
+import com.android.launcher3.statemanager.StatefulContainer
+import com.android.launcher3.taskbar.TaskbarUIController
+import com.android.launcher3.util.ContextTracker
+import com.android.launcher3.util.DisplayController
+import com.android.launcher3.util.RunnableList
+import com.android.launcher3.util.SystemUiController
+import com.android.launcher3.views.BaseDragLayer
+import com.android.launcher3.views.ScrimView
+import com.android.quickstep.FallbackWindowInterface
+import com.android.quickstep.OverviewComponentObserver
+import com.android.quickstep.RecentsAnimationCallbacks
+import com.android.quickstep.RecentsAnimationCallbacks.RecentsAnimationListener
+import com.android.quickstep.RecentsAnimationController
+import com.android.quickstep.RecentsModel
+import com.android.quickstep.RemoteAnimationTargets
+import com.android.quickstep.SystemUiProxy
+import com.android.quickstep.fallback.FallbackRecentsStateController
+import com.android.quickstep.fallback.FallbackRecentsView
+import com.android.quickstep.fallback.RecentsDragLayer
+import com.android.quickstep.fallback.RecentsState
+import com.android.quickstep.fallback.RecentsState.BACKGROUND_APP
+import com.android.quickstep.fallback.RecentsState.BG_LAUNCHER
+import com.android.quickstep.fallback.RecentsState.DEFAULT
+import com.android.quickstep.fallback.RecentsState.HOME
+import com.android.quickstep.fallback.RecentsState.MODAL_TASK
+import com.android.quickstep.fallback.RecentsState.OVERVIEW_SPLIT_SELECT
+import com.android.quickstep.util.RecentsAtomicAnimationFactory
+import com.android.quickstep.util.RecentsWindowProtoLogProxy
+import com.android.quickstep.util.SplitSelectStateController
+import com.android.quickstep.util.TISBindHelper
+import com.android.quickstep.views.OverviewActionsView
+import com.android.quickstep.views.RecentsView
+import com.android.quickstep.views.RecentsViewContainer
+import com.android.systemui.shared.recents.model.ThumbnailData
+import com.android.systemui.shared.system.TaskStackChangeListener
+import com.android.systemui.shared.system.TaskStackChangeListeners
+import java.util.function.Predicate
+
+/**
+ * Class that will manage RecentsView lifecycle within a window and interface correctly where
+ * needed. This allows us to run RecentsView in a window where needed.
+ *
+ * todo: b/365776320,b/365777482
+ *
+ * To add new protologs, see [RecentsWindowProtoLogProxy]. To enable logging to logcat, see
+ * [QuickstepProtoLogGroup.Constants.DEBUG_RECENTS_WINDOW]
+ */
+class RecentsWindowManager(context: Context) :
+    RecentsWindowContext(context), RecentsViewContainer, StatefulContainer<RecentsState> {
+
+    companion object {
+        private const val HOME_APPEAR_DURATION: Long = 250
+        private const val TAG = "RecentsWindowManager"
+
+        class RecentsWindowTracker : ContextTracker<RecentsWindowManager?>() {
+            override fun isHomeStarted(context: RecentsWindowManager?): Boolean {
+                return true
+            }
+        }
+
+        @JvmStatic val recentsWindowTracker = RecentsWindowTracker()
+    }
+
+    protected var recentsView: FallbackRecentsView<RecentsWindowManager>? = null
+    private val windowContext: Context = createWindowContext(TYPE_APPLICATION_OVERLAY, null)
+    private val windowManager: WindowManager =
+        windowContext.getSystemService(WindowManager::class.java)!!
+    private var layoutInflater: LayoutInflater = LayoutInflater.from(this).cloneInContext(this)
+    private var stateManager: StateManager<RecentsState, RecentsWindowManager> =
+        StateManager<RecentsState, RecentsWindowManager>(this, RecentsState.BG_LAUNCHER)
+    private var mSystemUiController: SystemUiController? = null
+
+    private var dragLayer: RecentsDragLayer<RecentsWindowManager>? = null
+    private var windowView: View? = null
+    private var actionsView: OverviewActionsView<*>? = null
+    private var scrimView: ScrimView? = null
+
+    private var callbacks: RecentsAnimationCallbacks? = null
+
+    private var taskbarUIController: TaskbarUIController? = null
+    private var tisBindHelper: TISBindHelper = TISBindHelper(this) {}
+
+    // Callback array that corresponds to events defined in @ActivityEvent
+    private val mEventCallbacks =
+        listOf(RunnableList(), RunnableList(), RunnableList(), RunnableList())
+    private var onInitListener: Predicate<Boolean>? = null
+
+    private val taskStackChangeListener =
+        object : TaskStackChangeListener {
+            override fun onTaskMovedToFront(taskId: Int) {
+                if ((isShowing() && isInState(DEFAULT))) {
+                    // handling state where we end recents animation by swiping livetile away
+                    // TODO: animate this switch.
+                    cleanupRecentsWindow()
+                }
+            }
+        }
+
+    private val recentsAnimationListener =
+        object : RecentsAnimationListener {
+            override fun onRecentsAnimationCanceled(thumbnailDatas: HashMap<Int, ThumbnailData>) {
+                recentAnimationStopped()
+            }
+
+            override fun onRecentsAnimationFinished(controller: RecentsAnimationController) {
+                recentAnimationStopped()
+            }
+        }
+
+    init {
+        FallbackWindowInterface.init(this)
+        TaskStackChangeListeners.getInstance().registerTaskStackListener(taskStackChangeListener)
+    }
+
+    override fun destroy() {
+        super.destroy()
+        cleanupRecentsWindow()
+        FallbackWindowInterface.getInstance()?.destroy()
+        TaskStackChangeListeners.getInstance().unregisterTaskStackListener(taskStackChangeListener)
+        callbacks?.removeListener(recentsAnimationListener)
+        recentsWindowTracker.onContextDestroyed(this)
+    }
+
+    override fun startHome() {
+        val recentsView: RecentsView<*, *> = getOverviewPanel()
+        recentsView.switchToScreenshot {
+            recentsView.finishRecentsAnimation(true) { startHomeInternal() }
+        }
+    }
+
+    private fun startHomeInternal() {
+        val runner = LauncherAnimationRunner(mainThreadHandler, mAnimationToHomeFactory, true)
+        val options =
+            ActivityOptions.makeRemoteAnimation(
+                RemoteAnimationAdapter(runner, HOME_APPEAR_DURATION, 0),
+                RemoteTransition(
+                    runner.toRemoteTransition(),
+                    iApplicationThread,
+                    "StartHomeFromRecents",
+                ),
+            )
+        OverviewComponentObserver.startHomeIntentSafely(this, options.toBundle(), TAG)
+        stateManager.moveToRestState()
+    }
+
+    private val mAnimationToHomeFactory =
+        RemoteAnimationFactory {
+            _: Int,
+            appTargets: Array<RemoteAnimationTarget>?,
+            wallpaperTargets: Array<RemoteAnimationTarget>?,
+            nonAppTargets: Array<RemoteAnimationTarget>?,
+            result: LauncherAnimationRunner.AnimationResult? ->
+            val controller =
+                getStateManager().createAnimationToNewWorkspace(BG_LAUNCHER, HOME_APPEAR_DURATION)
+            controller.dispatchOnStart()
+            val targets =
+                RemoteAnimationTargets(
+                    appTargets,
+                    wallpaperTargets,
+                    nonAppTargets,
+                    RemoteAnimationTarget.MODE_OPENING,
+                )
+            for (app in targets.apps) {
+                SurfaceControl.Transaction().setAlpha(app.leash, 1f).apply()
+            }
+            val anim = AnimatorSet()
+            anim.play(controller.animationPlayer)
+            anim.setDuration(HOME_APPEAR_DURATION)
+            result!!.setAnimation(
+                anim,
+                this@RecentsWindowManager,
+                {
+                    getStateManager().goToState(BG_LAUNCHER, false)
+                    cleanupRecentsWindow()
+                },
+                true, /* skipFirstFrame */
+            )
+        }
+
+    private val onBackInvokedCallback: () -> Unit = {
+        // If we are in live tile mode, launch the live task, otherwise return home
+        recentsView?.runningTaskView?.launchWithAnimation() ?: startHome()
+    }
+
+    private fun cleanupRecentsWindow() {
+        RecentsWindowProtoLogProxy.logCleanup(isShowing())
+        if (isShowing()) {
+            windowManager.removeViewImmediate(windowView)
+        }
+        stateManager.moveToRestState()
+        callbacks?.removeListener(recentsAnimationListener)
+    }
+
+    private fun isShowing(): Boolean {
+        return windowView?.parent != null
+    }
+
+    fun startRecentsWindow(callbacks: RecentsAnimationCallbacks? = null) {
+        RecentsWindowProtoLogProxy.logStartRecentsWindow(isShowing(), windowView == null)
+        if (isShowing()) {
+            return
+        }
+        if (windowView == null) {
+            windowView = layoutInflater.inflate(R.layout.fallback_recents_activity, null)
+        }
+        windowManager.addView(windowView, windowLayoutParams)
+
+        windowView
+            ?.findOnBackInvokedDispatcher()
+            ?.registerSystemOnBackInvokedCallback(onBackInvokedCallback)
+
+        windowView?.systemUiVisibility =
+            (View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
+                View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
+                View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION)
+
+        recentsView = windowView?.findViewById(R.id.overview_panel)
+        actionsView = windowView?.findViewById(R.id.overview_actions_view)
+        scrimView = windowView?.findViewById(R.id.scrim_view)
+        val systemUiProxy = SystemUiProxy.INSTANCE[this]
+        val splitSelectStateController =
+            SplitSelectStateController(
+                this,
+                getStateManager(),
+                null, /* depthController */
+                statsLogManager,
+                systemUiProxy,
+                RecentsModel.INSTANCE[this],
+                null, /*activityBackCallback*/
+            )
+        recentsView?.init(actionsView, splitSelectStateController, null)
+        dragLayer = windowView?.findViewById(R.id.drag_layer)
+
+        actionsView?.updateDimension(getDeviceProfile(), recentsView?.lastComputedTaskSize)
+        actionsView?.updateVerticalMargin(DisplayController.getNavigationMode(this))
+
+        mSystemUiController = SystemUiController(windowView)
+        recentsWindowTracker.handleCreate(this)
+
+        this.callbacks = callbacks
+        callbacks?.addListener(recentsAnimationListener)
+    }
+
+    private fun recentAnimationStopped() {
+        if (isInState(BACKGROUND_APP)) {
+            cleanupRecentsWindow()
+        }
+    }
+
+    override fun getComponentName(): ComponentName {
+        return ComponentName(this, RecentsWindowManager::class.java)
+    }
+
+    override fun canStartHomeSafely(): Boolean {
+        val overviewCommandHelper = tisBindHelper.overviewCommandHelper
+        return overviewCommandHelper == null || overviewCommandHelper.canStartHomeSafely()
+    }
+
+    override fun getDesktopVisibilityController(): DesktopVisibilityController? {
+        return tisBindHelper.desktopVisibilityController
+    }
+
+    override fun setTaskbarUIController(taskbarUIController: TaskbarUIController?) {
+        this.taskbarUIController = taskbarUIController
+    }
+
+    override fun getTaskbarUIController(): TaskbarUIController? {
+        return taskbarUIController
+    }
+
+    override fun getTISBindHelper(): TISBindHelper {
+        return tisBindHelper
+    }
+
+    fun registerInitListener(onInitListener: Predicate<Boolean>) {
+        this.onInitListener = onInitListener
+    }
+
+    override fun collectStateHandlers(out: MutableList<StateManager.StateHandler<RecentsState?>>?) {
+        out!!.add(FallbackRecentsStateController(this))
+    }
+
+    override fun getStateManager(): StateManager<RecentsState, RecentsWindowManager> {
+        return this.stateManager
+    }
+
+    override fun shouldAnimateStateChange(): Boolean {
+        return true
+    }
+
+    override fun isInState(state: RecentsState?): Boolean {
+        return stateManager.state == state
+    }
+
+    override fun onStateSetStart(state: RecentsState) {
+        super.onStateSetStart(state)
+        RecentsWindowProtoLogProxy.logOnStateSetStart(getStateName(state))
+    }
+
+    override fun onStateSetEnd(state: RecentsState) {
+        super.onStateSetEnd(state)
+        RecentsWindowProtoLogProxy.logOnStateSetEnd(getStateName(state))
+
+        if (state == HOME || state == BG_LAUNCHER) {
+            cleanupRecentsWindow()
+        }
+    }
+
+    private fun getStateName(state: RecentsState?): String {
+        return when (state) {
+            null -> "NULL"
+            DEFAULT -> "default"
+            MODAL_TASK -> "MODAL_TASK"
+            BACKGROUND_APP -> "BACKGROUND_APP"
+            HOME -> "HOME"
+            BG_LAUNCHER -> "BG_LAUNCHER"
+            OVERVIEW_SPLIT_SELECT -> "OVERVIEW_SPLIT_SELECT"
+            else -> "ordinal=" + state.ordinal
+        }
+    }
+
+    override fun getSystemUiController(): SystemUiController? {
+        if (mSystemUiController == null) {
+            mSystemUiController = SystemUiController(rootView)
+        }
+        return mSystemUiController
+    }
+
+    override fun getContext(): Context {
+        return this
+    }
+
+    override fun getScrimView(): ScrimView? {
+        return scrimView
+    }
+
+    override fun <T : View?> getOverviewPanel(): T {
+        return recentsView as T
+    }
+
+    override fun getRootView(): View? {
+        return windowView
+    }
+
+    override fun getDragLayer(): BaseDragLayer<RecentsWindowManager> {
+        return dragLayer!!
+    }
+
+    override fun dispatchGenericMotionEvent(ev: MotionEvent?): Boolean {
+        // TODO(b/368610710)
+        return false
+    }
+
+    override fun dispatchKeyEvent(ev: KeyEvent?): Boolean {
+        // TODO(b/368610710)
+        return false
+    }
+
+    override fun getActionsView(): OverviewActionsView<*>? {
+        return actionsView
+    }
+
+    override fun addForceInvisibleFlag(flag: Int) {}
+
+    override fun clearForceInvisibleFlag(flag: Int) {}
+
+    override fun setLocusContext(id: LocusId?, bundle: Bundle?) {
+        // no op
+    }
+
+    override fun isStarted(): Boolean {
+        return isShowing() && isInState(DEFAULT)
+    }
+
+    /** Adds a callback for the provided activity event */
+    override fun addEventCallback(@BaseActivity.ActivityEvent event: Int, callback: Runnable?) {
+        mEventCallbacks[event].add(callback)
+    }
+
+    /** Removes a previously added callback */
+    override fun removeEventCallback(@BaseActivity.ActivityEvent event: Int, callback: Runnable?) {
+        mEventCallbacks[event].remove(callback)
+    }
+
+    override fun runOnBindToTouchInteractionService(r: Runnable?) {
+        tisBindHelper.runOnBindToTouchInteractionService(r)
+    }
+
+    override fun addMultiWindowModeChangedListener(
+        listener: BaseActivity.MultiWindowModeChangedListener?
+    ) {
+        // TODO(b/368408838)
+    }
+
+    override fun removeMultiWindowModeChangedListener(
+        listener: BaseActivity.MultiWindowModeChangedListener?
+    ) {}
+
+    override fun returnToHomescreen() {
+        startHome()
+    }
+
+    override fun isRecentsViewVisible(): Boolean {
+        return getStateManager().state!!.isRecentsViewVisible
+    }
+
+    override fun createAtomicAnimationFactory(): AtomicAnimationFactory<RecentsState?>? {
+        return RecentsAtomicAnimationFactory<RecentsWindowManager, RecentsState>(this)
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowSwipeHandler.java b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowSwipeHandler.java
new file mode 100644
index 0000000..4f9d837
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowSwipeHandler.java
@@ -0,0 +1,439 @@
+/*
+ * 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.fallback.window;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.content.Intent.EXTRA_COMPONENT_NAME;
+import static android.content.Intent.EXTRA_USER;
+
+import static com.android.app.animation.Interpolators.ACCELERATE;
+import static com.android.launcher3.GestureNavContract.EXTRA_GESTURE_CONTRACT;
+import static com.android.launcher3.GestureNavContract.EXTRA_ICON_POSITION;
+import static com.android.launcher3.GestureNavContract.EXTRA_ICON_SURFACE;
+import static com.android.launcher3.GestureNavContract.EXTRA_ON_FINISH_CALLBACK;
+import static com.android.launcher3.GestureNavContract.EXTRA_REMOTE_CALLBACK;
+import static com.android.launcher3.anim.AnimatorListeners.forEndCallback;
+
+import android.app.ActivityManager.RunningTaskInfo;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.ParcelUuid;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.util.Log;
+import android.view.RemoteAnimationTarget;
+import android.view.Surface;
+import android.view.SurfaceControl;
+import android.view.SurfaceControl.Transaction;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.AnimatedFloat;
+import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.anim.SpringAnimationBuilder;
+import com.android.launcher3.states.StateAnimationConfig;
+import com.android.launcher3.util.DisplayController;
+import com.android.quickstep.AbsSwipeUpHandler;
+import com.android.quickstep.GestureState;
+import com.android.quickstep.RecentsAnimationController;
+import com.android.quickstep.RecentsAnimationDeviceState;
+import com.android.quickstep.RecentsAnimationTargets;
+import com.android.quickstep.TaskAnimationManager;
+import com.android.quickstep.fallback.FallbackRecentsView;
+import com.android.quickstep.fallback.RecentsState;
+import com.android.quickstep.util.RectFSpringAnim;
+import com.android.quickstep.util.SurfaceTransaction.SurfaceProperties;
+import com.android.quickstep.util.TransformParams;
+import com.android.quickstep.views.TaskView;
+import com.android.systemui.shared.recents.model.Task.TaskKey;
+import com.android.systemui.shared.system.InputConsumerController;
+
+import java.lang.ref.WeakReference;
+import java.util.List;
+import java.util.UUID;
+import java.util.function.Consumer;
+
+/**
+ * Handles the navigation gestures when a 3rd party launcher is the default home activity.
+ *
+ * Bugs: b/365775417
+ */
+public class RecentsWindowSwipeHandler extends AbsSwipeUpHandler<RecentsWindowManager,
+        FallbackRecentsView<RecentsWindowManager>, RecentsState> {
+
+    private static final String TAG = "RecentsWindowSwipeHandler";
+
+    /**
+     * Message used for receiving gesture nav contract information. We use a static messenger to
+     * avoid leaking too make binders in case the receiving launcher does not handle the contract
+     * properly.
+     */
+    private static StaticMessageReceiver sMessageReceiver = null;
+
+    private FallbackHomeAnimationFactory mActiveAnimationFactory;
+    private final boolean mRunningOverHome;
+
+    private final Matrix mTmpMatrix = new Matrix();
+    private float mMaxLauncherScale = 1;
+
+    private boolean mAppCanEnterPip;
+
+    public RecentsWindowSwipeHandler(Context context, RecentsAnimationDeviceState deviceState,
+            TaskAnimationManager taskAnimationManager, GestureState gestureState, long touchTimeMs,
+            boolean continuingLastGesture, InputConsumerController inputConsumer,
+            RecentsWindowManager recentsWindowManager) {
+        super(context, deviceState, taskAnimationManager, gestureState, touchTimeMs,
+                continuingLastGesture, inputConsumer, recentsWindowManager);
+
+        mRunningOverHome = mGestureState.getRunningTask() != null
+                && mGestureState.getRunningTask().isHomeTask();
+
+        initTransformParams();
+    }
+
+    @Override
+    public void onRecentsAnimationStart(RecentsAnimationController controller,
+            RecentsAnimationTargets targets) {
+        super.onRecentsAnimationStart(controller, targets);
+        initTransformParams();
+    }
+
+    private void initTransformParams() {
+        if (mActiveAnimationFactory != null) {
+            mActiveAnimationFactory.initTransformParams();
+            return;
+        }
+        runActionOnRemoteHandles(remoteTargetHandle ->
+                remoteTargetHandle.getTransformParams().setHomeBuilderProxy(
+                        RecentsWindowSwipeHandler.this::updateHomeActivityTransformDuringSwipeUp));
+    }
+
+    @Override
+    protected void initTransitionEndpoints(DeviceProfile dp) {
+        super.initTransitionEndpoints(dp);
+        if (mRunningOverHome) {
+            // Full screen scale should be independent of remote target handle
+            mMaxLauncherScale = 1 / mRemoteTargetHandles[0].getTaskViewSimulator()
+                    .getFullScreenScale();
+        }
+    }
+
+    private void updateHomeActivityTransformDuringSwipeUp(SurfaceProperties builder,
+            RemoteAnimationTarget app, TransformParams params) {
+        if (mActiveAnimationFactory != null) {
+            return;
+        }
+        setHomeScaleAndAlpha(builder, app, mCurrentShift.value, 0);
+    }
+
+    private void setHomeScaleAndAlpha(SurfaceProperties builder,
+            RemoteAnimationTarget app, float verticalShift, float alpha) {
+        if (app.windowConfiguration.getActivityType() != ACTIVITY_TYPE_HOME) {
+            return;
+        }
+        float scale = Utilities.mapRange(verticalShift, 1, mMaxLauncherScale);
+        mTmpMatrix.setScale(scale, scale,
+                app.localBounds.exactCenterX(), app.localBounds.exactCenterY());
+        builder.setMatrix(mTmpMatrix).setAlpha(alpha);
+        builder.setShow();
+    }
+
+    @Override
+    protected HomeAnimationFactory createHomeAnimationFactory(
+            List<IBinder> launchCookies,
+            long duration,
+            boolean isTargetTranslucent,
+            boolean appCanEnterPip,
+            RemoteAnimationTarget runningTaskTarget,
+            @Nullable TaskView targetTaskView) {
+        mAppCanEnterPip = appCanEnterPip;
+        if (appCanEnterPip) {
+            return new FallbackPipToHomeAnimationFactory();
+        }
+        mActiveAnimationFactory = new FallbackHomeAnimationFactory(duration);
+        //todo: b/368410893 follow up on this as its intent focused and seems to cut immediately
+        Intent intent = new Intent(mGestureState.getHomeIntent());
+        if (runningTaskTarget != null) {
+            mActiveAnimationFactory.addGestureContract(intent, runningTaskTarget.taskInfo);
+        }
+        return mActiveAnimationFactory;
+    }
+
+    @Override
+    protected void finishRecentsControllerToHome(Runnable callback) {
+        final Runnable recentsCallback;
+        if (mAppCanEnterPip) {
+            // Make sure Launcher is resumed after auto-enter-pip transition to actually trigger
+            // the PiP task appearing.
+            recentsCallback = () -> {
+                callback.run();
+                mRecentsWindowManager.startHome();
+            };
+        } else {
+            recentsCallback = callback;
+        }
+        mRecentsView.cleanupRemoteTargets();
+        mRecentsAnimationController.finish(
+                mAppCanEnterPip /* toRecents */, recentsCallback, true /* sendUserLeaveHint */);
+    }
+
+    @Override
+    protected void switchToScreenshot() {
+        if (mRunningOverHome) {
+            // When the current task is home, then we don't need to capture anything
+            mStateCallback.setStateOnUiThread(STATE_SCREENSHOT_CAPTURED);
+        } else {
+            super.switchToScreenshot();
+        }
+    }
+
+    @Override
+    protected void notifyGestureAnimationStartToRecents() {
+        if (mRunningOverHome) {
+            if (DisplayController.getNavigationMode(mContext).hasGestures) {
+                mRecentsView.onGestureAnimationStartOnHome(
+                        mGestureState.getRunningTask().getPlaceholderTasks(),
+                        mDeviceState.getRotationTouchHelper());
+            }
+        } else {
+            super.notifyGestureAnimationStartToRecents();
+        }
+    }
+
+    private class FallbackPipToHomeAnimationFactory extends HomeAnimationFactory {
+        @NonNull
+        @Override
+        public AnimatorPlaybackController createActivityAnimationToHome() {
+            // copied from {@link LauncherSwipeHandlerV2.LauncherHomeAnimationFactory}
+            long accuracy = 2 * Math.max(mDp.widthPx, mDp.heightPx);
+            return mContainer.getStateManager().createAnimationToNewWorkspace(
+                    RecentsState.HOME, accuracy, StateAnimationConfig.SKIP_ALL_ANIMATIONS);
+        }
+    }
+
+    private class FallbackHomeAnimationFactory extends HomeAnimationFactory
+            implements Consumer<Message> {
+        private final Rect mTempRect = new Rect();
+
+        private final TransformParams mTransformParams = new TransformParams();
+        private final AnimatedFloat mHomeAlpha = new AnimatedFloat(this::updateAppTransforms);
+        private final AnimatedFloat mVerticalShiftForScale =
+                new AnimatedFloat(this::updateAppTransforms);
+        private final AnimatedFloat mRecentsAlpha = new AnimatedFloat(this:: updateAppTransforms);
+
+        private final RectF mTargetRect = new RectF();
+        private SurfaceControl mSurfaceControl;
+
+        private boolean mAnimationFinished;
+        private Message mOnFinishCallback;
+
+        private final long mDuration;
+
+        private RectFSpringAnim mSpringAnim;
+        FallbackHomeAnimationFactory(long duration) {
+            mDuration = duration;
+
+            if (mRunningOverHome) {
+                mVerticalShiftForScale.value = mCurrentShift.value;
+            }
+            mRecentsAlpha.value = 1;
+            mHomeAlpha.value = 0;
+
+            initTransformParams();
+        }
+
+        @NonNull
+        @Override
+        public RectF getWindowTargetRect() {
+            if (mTargetRect.isEmpty()) {
+                mTargetRect.set(super.getWindowTargetRect());
+            }
+            return mTargetRect;
+        }
+
+        @NonNull
+        @Override
+        public AnimatorPlaybackController createActivityAnimationToHome() {
+            PendingAnimation pa = new PendingAnimation(mDuration);
+            pa.setFloat(mRecentsAlpha, AnimatedFloat.VALUE, 0, ACCELERATE);
+            pa.setFloat(mHomeAlpha, AnimatedFloat.VALUE, 1, ACCELERATE);
+            return pa.createPlaybackController();
+        }
+
+        @Override
+        public void playAtomicAnimation(float velocity) {
+            if (!mRunningOverHome) {
+                return;
+            }
+            // Spring back launcher scale
+            new SpringAnimationBuilder(mContext)
+                    .setStartValue(mVerticalShiftForScale.value)
+                    .setEndValue(0)
+                    .setStartVelocity(-velocity / mTransitionDragLength)
+                    .setMinimumVisibleChange(1f / mDp.heightPx)
+                    .setDampingRatio(0.6f)
+                    .setStiffness(800)
+                    .build(mVerticalShiftForScale, AnimatedFloat.VALUE)
+                    .start();
+        }
+
+        @Override
+        public void setAnimation(RectFSpringAnim anim) {
+            mSpringAnim = anim;
+            mSpringAnim.addAnimatorListener(forEndCallback(this::onRectAnimationEnd));
+        }
+
+        private void initTransformParams() {
+            runActionOnRemoteHandles(remoteTargetHandle ->
+                    remoteTargetHandle.getTransformParams().setHomeBuilderProxy(
+                            FallbackHomeAnimationFactory.this
+                                    ::updateHomeActivityTransformDuringHomeAnim));
+
+            mTransformParams.setTargetSet(mRecentsAnimationTargets);
+        }
+
+        private void updateRecentsActivityTransformDuringHomeAnim(SurfaceProperties builder,
+                RemoteAnimationTarget app, TransformParams params) {
+            if (app.mode != mRecentsAnimationTargets.targetMode) {
+                return;
+            }
+            builder.setAlpha(mRecentsAlpha.value);
+        }
+
+        private void updateAppTransforms() {
+            mTransformParams.applySurfaceParams(
+                    mTransformParams.createSurfaceParams(FallbackHomeAnimationFactory.this
+                            ::updateRecentsActivityTransformDuringHomeAnim));
+        }
+
+        private void updateHomeActivityTransformDuringHomeAnim(SurfaceProperties builder,
+                RemoteAnimationTarget app, TransformParams params) {
+            setHomeScaleAndAlpha(builder, app, mVerticalShiftForScale.value, mHomeAlpha.value);
+        }
+
+        private void onRectAnimationEnd() {
+            mAnimationFinished = true;
+            maybeSendEndMessage();
+        }
+
+        private void maybeSendEndMessage() {
+            if (mAnimationFinished && mOnFinishCallback != null) {
+                try {
+                    mOnFinishCallback.replyTo.send(mOnFinishCallback);
+                } catch (RemoteException e) {
+                    Log.e(TAG, "Error sending icon position", e);
+                }
+            }
+        }
+
+        @Override
+        public void accept(Message msg) {
+            try {
+                Bundle data = msg.getData();
+                RectF position = data.getParcelable(EXTRA_ICON_POSITION);
+                if (!position.isEmpty()) {
+                    mSurfaceControl = data.getParcelable(EXTRA_ICON_SURFACE);
+                    mTargetRect.set(position);
+                    if (mSpringAnim != null) {
+                        mSpringAnim.onTargetPositionChanged();
+                    }
+                }
+                mOnFinishCallback = data.getParcelable(EXTRA_ON_FINISH_CALLBACK);
+                maybeSendEndMessage();
+            } catch (Exception e) {
+                // Ignore
+            }
+        }
+
+        @Override
+        public void update(RectF currentRect, float progress, float radius, int overlayAlpha) {
+            if (mSurfaceControl != null) {
+                currentRect.roundOut(mTempRect);
+                Transaction t = new Transaction();
+                try {
+                    t.setGeometry(mSurfaceControl, null, mTempRect, Surface.ROTATION_0);
+                    t.apply();
+                } catch (RuntimeException e) {
+                    // Ignore
+                }
+            }
+        }
+
+        private void addGestureContract(Intent intent, RunningTaskInfo runningTaskInfo) {
+            if (mRunningOverHome || runningTaskInfo == null) {
+                return;
+            }
+
+            TaskKey key = new TaskKey(runningTaskInfo);
+            if (key.getComponent() != null) {
+                if (sMessageReceiver == null) {
+                    sMessageReceiver = new StaticMessageReceiver();
+                }
+
+                Bundle gestureNavContract = new Bundle();
+                gestureNavContract.putParcelable(EXTRA_COMPONENT_NAME, key.getComponent());
+                gestureNavContract.putParcelable(EXTRA_USER, UserHandle.of(key.userId));
+                gestureNavContract.putParcelable(
+                        EXTRA_REMOTE_CALLBACK, sMessageReceiver.newCallback(this));
+                intent.putExtra(EXTRA_GESTURE_CONTRACT, gestureNavContract);
+            }
+        }
+    }
+
+    private static class StaticMessageReceiver implements Handler.Callback {
+
+        private final Messenger mMessenger =
+                new Messenger(new Handler(Looper.getMainLooper(), this));
+
+        private ParcelUuid mCurrentUID = new ParcelUuid(UUID.randomUUID());
+        private WeakReference<Consumer<Message>> mCurrentCallback = new WeakReference<>(null);
+
+        public Message newCallback(Consumer<Message> callback) {
+            mCurrentUID = new ParcelUuid(UUID.randomUUID());
+            mCurrentCallback = new WeakReference<>(callback);
+
+            Message msg = Message.obtain();
+            msg.replyTo = mMessenger;
+            msg.obj = mCurrentUID;
+            return msg;
+        }
+
+        @Override
+        public boolean handleMessage(@NonNull Message message) {
+            if (mCurrentUID.equals(message.obj)) {
+                Consumer<Message> consumer = mCurrentCallback.get();
+                if (consumer != null) {
+                    consumer.accept(message);
+                    return true;
+                }
+            }
+            return false;
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/BubbleBarInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/BubbleBarInputConsumer.java
index 92031c5..778c231 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/BubbleBarInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/BubbleBarInputConsumer.java
@@ -23,10 +23,12 @@
 import android.view.MotionEvent;
 import android.view.ViewConfiguration;
 
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.taskbar.TaskbarActivityContext;
+import com.android.launcher3.taskbar.bubbles.BubbleBarSwipeController;
 import com.android.launcher3.taskbar.bubbles.BubbleBarViewController;
 import com.android.launcher3.taskbar.bubbles.BubbleControllers;
-import com.android.launcher3.taskbar.bubbles.BubbleDragController;
 import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController;
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.shared.TestProtocol;
@@ -40,10 +42,11 @@
 
     private final BubbleStashController mBubbleStashController;
     private final BubbleBarViewController mBubbleBarViewController;
-    private final BubbleDragController mBubbleDragController;
+    @Nullable
+    private final BubbleBarSwipeController mBubbleBarSwipeController;
     private final InputMonitorCompat mInputMonitorCompat;
 
-    private boolean mSwipeUpOnBubbleHandle;
+    private boolean mPilfered;
     private boolean mPassedTouchSlop;
     private boolean mStashedOrCollapsedOnDown;
 
@@ -57,7 +60,8 @@
             InputMonitorCompat inputMonitorCompat) {
         mBubbleStashController = bubbleControllers.bubbleStashController;
         mBubbleBarViewController = bubbleControllers.bubbleBarViewController;
-        mBubbleDragController = bubbleControllers.bubbleDragController;
+        mBubbleBarSwipeController = bubbleControllers.bubbleBarSwipeController.orElse(null);
+
         mInputMonitorCompat = inputMonitorCompat;
         mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
         mTimeForTap = ViewConfiguration.getTapTimeout();
@@ -77,6 +81,9 @@
                 mDownPos.set(ev.getX(), ev.getY());
                 mLastPos.set(mDownPos);
                 mStashedOrCollapsedOnDown = mBubbleStashController.isStashed() || isCollapsed();
+                if (mBubbleBarSwipeController != null) {
+                    mBubbleBarSwipeController.start();
+                }
                 break;
             case MotionEvent.ACTION_MOVE:
                 int pointerIndex = ev.findPointerIndex(mActivePointerId);
@@ -90,11 +97,10 @@
                 if (!mPassedTouchSlop) {
                     mPassedTouchSlop = Math.abs(dY) > mTouchSlop || Math.abs(dX) > mTouchSlop;
                 }
-                if (mStashedOrCollapsedOnDown && !mSwipeUpOnBubbleHandle && mPassedTouchSlop) {
-                    boolean verticalGesture = Math.abs(dY) > Math.abs(dX);
-                    if (verticalGesture && !mBubbleDragController.isDragging()) {
-                        mSwipeUpOnBubbleHandle = true;
-                        mBubbleStashController.showBubbleBar(/* expandBubbles= */ true);
+                if (mBubbleBarSwipeController != null) {
+                    mBubbleBarSwipeController.swipeTo(dY);
+                    if (!mPilfered && mBubbleBarSwipeController.isSwipeGesture()) {
+                        mPilfered = true;
                         // Bubbles is handling the swipe so make sure no one else gets it.
                         TestLogging.recordEvent(TestProtocol.SEQUENCE_PILFER, "pilferPointers");
                         mInputMonitorCompat.pilferPointers();
@@ -102,8 +108,10 @@
                 }
                 break;
             case MotionEvent.ACTION_UP:
+                boolean swipeUpOnBubbleHandle = mBubbleBarSwipeController != null
+                        && mBubbleBarSwipeController.isSwipeGesture();
                 boolean isWithinTapTime = ev.getEventTime() - ev.getDownTime() <= mTimeForTap;
-                if (isWithinTapTime && !mSwipeUpOnBubbleHandle && !mPassedTouchSlop
+                if (isWithinTapTime && !swipeUpOnBubbleHandle && !mPassedTouchSlop
                         && mStashedOrCollapsedOnDown) {
                     // Taps on the handle / collapsed state should open the bar
                     mBubbleStashController.showBubbleBar(/* expandBubbles= */ true);
@@ -116,8 +124,11 @@
     }
 
     private void cleanupAfterMotionEvent() {
+        if (mBubbleBarSwipeController != null) {
+            mBubbleBarSwipeController.finish();
+        }
         mPassedTouchSlop = false;
-        mSwipeUpOnBubbleHandle = false;
+        mPilfered = false;
     }
 
     private boolean isCollapsed() {
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/DelegateInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/DelegateInputConsumer.java
index 5557639..4afd92a 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/DelegateInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/DelegateInputConsumer.java
@@ -5,7 +5,7 @@
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.shared.TestProtocol;
 import com.android.quickstep.InputConsumer;
-import com.android.quickstep.util.ActiveGestureLog;
+import com.android.quickstep.util.ActiveGestureProtoLogProxy;
 import com.android.systemui.shared.system.InputMonitorCompat;
 
 public abstract class DelegateInputConsumer implements InputConsumer {
@@ -57,8 +57,7 @@
     }
 
     protected void setActive(MotionEvent ev) {
-        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(getDelegatorName())
-                .append(" became active"));
+        ActiveGestureProtoLogProxy.logInputConsumerBecameActive(getDelegatorName());
 
         mState = STATE_ACTIVE;
         TestLogging.recordEvent(TestProtocol.SEQUENCE_PILFER, "pilferPointers");
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressHandler.java b/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressHandler.java
index 1d00e53..155d095 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressHandler.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressHandler.java
@@ -16,25 +16,65 @@
 
 package com.android.quickstep.inputconsumers;
 
+import static android.app.contextualsearch.ContextualSearchManager.ENTRYPOINT_LONG_PRESS_NAV_HANDLE;
+
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_ASSISTANT_SUCCESSFUL_NAV_HANDLE;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_OMNI_GET_LONG_PRESS_RUNNABLE;
+import static com.android.launcher3.logging.StatsLogManager.LauncherLatencyEvent.LAUNCHER_LATENCY_OMNI_RUNNABLE;
+
 import android.content.Context;
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.ViewConfiguration;
 
 import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.R;
+import com.android.launcher3.logging.InstanceId;
+import com.android.launcher3.logging.InstanceIdSequence;
+import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.util.ResourceBasedOverride;
+import com.android.launcher3.util.VibratorWrapper;
+import com.android.quickstep.DeviceConfigWrapper;
 import com.android.quickstep.NavHandle;
+import com.android.quickstep.TopTaskTracker;
+import com.android.quickstep.util.ContextualSearchHapticManager;
+import com.android.quickstep.util.ContextualSearchInvoker;
+import com.android.quickstep.util.ContextualSearchStateManager;
 
 /**
  * Class for extending nav handle long press behavior
  */
 public class NavHandleLongPressHandler implements ResourceBasedOverride {
 
+    private static final String TAG = "NavHandleLongPressHandler";
+
+    protected final Context mContext;
+    protected final VibratorWrapper mVibratorWrapper;
+    protected final ContextualSearchHapticManager mContextualSearchHapticManager;
+    protected final ContextualSearchInvoker mContextualSearchInvoker;
+    protected final StatsLogManager mStatsLogManager;
+    private boolean mPendingInvocation;
+
+    public NavHandleLongPressHandler(Context context) {
+        mContext = context;
+        mStatsLogManager = StatsLogManager.newInstance(context);
+        mVibratorWrapper = VibratorWrapper.INSTANCE.get(mContext);
+        mContextualSearchHapticManager = ContextualSearchHapticManager.INSTANCE.get(context);
+        mContextualSearchInvoker = ContextualSearchInvoker.newInstance(mContext);
+    }
+
     /** Creates NavHandleLongPressHandler as specified by overrides */
     public static NavHandleLongPressHandler newInstance(Context context) {
         return Overrides.getObject(NavHandleLongPressHandler.class, context,
                 R.string.nav_handle_long_press_handler_class);
     }
 
+    protected boolean isContextualSearchEntrypointEnabled(NavHandle navHandle) {
+        return DeviceConfigWrapper.get().getEnableLongPressNavHandle();
+    }
+
     /**
      * Called when nav handle is long pressed to get the Runnable that should be executed by the
      * caller to invoke long press behavior. If null is returned that means long press couldn't be
@@ -46,8 +86,48 @@
      *
      * @param navHandle to handle this long press
      */
-    public @Nullable Runnable getLongPressRunnable(NavHandle navHandle) {
-        return null;
+    @Nullable
+    @VisibleForTesting
+    final Runnable getLongPressRunnable(NavHandle navHandle) {
+        if (!isContextualSearchEntrypointEnabled(navHandle)) {
+            Log.i(TAG, "Contextual Search invocation failed: entry point disabled");
+            mVibratorWrapper.cancelVibrate();
+            return null;
+        }
+
+        if (!mContextualSearchInvoker.runContextualSearchInvocationChecksAndLogFailures()) {
+            Log.i(TAG, "Contextual Search invocation failed: precondition not satisfied");
+            mVibratorWrapper.cancelVibrate();
+            return null;
+        }
+
+        mPendingInvocation = true;
+        Log.i(TAG, "Contextual Search invocation: invocation runnable created");
+        InstanceId instanceId = new InstanceIdSequence().newInstanceId();
+        mStatsLogManager.logger().withInstanceId(instanceId).log(
+                LAUNCHER_OMNI_GET_LONG_PRESS_RUNNABLE);
+        long startTimeMillis = SystemClock.elapsedRealtime();
+        return () -> {
+            mStatsLogManager.latencyLogger().withInstanceId(instanceId).withLatency(
+                    SystemClock.elapsedRealtime() - startTimeMillis).log(
+                    LAUNCHER_LATENCY_OMNI_RUNNABLE);
+            if (mContextualSearchInvoker.invokeContextualSearchUncheckedWithHaptic(
+                    ENTRYPOINT_LONG_PRESS_NAV_HANDLE)) {
+                Log.i(TAG, "Contextual Search invocation successful");
+
+                String runningPackage = TopTaskTracker.INSTANCE.get(mContext).getCachedTopTask(
+                        /* filterOnlyVisibleRecents */ true).getPackageName();
+                mStatsLogManager.logger().withPackageName(runningPackage)
+                        .log(LAUNCHER_LAUNCH_ASSISTANT_SUCCESSFUL_NAV_HANDLE);
+            } else {
+                mVibratorWrapper.cancelVibrate();
+                if (DeviceConfigWrapper.get().getAnimateLpnh()
+                        && !DeviceConfigWrapper.get().getShrinkNavHandleOnPress()) {
+                    navHandle.animateNavBarLongPress(
+                            /*isTouchDown*/false, /*shrink*/ false, /*durationMs*/160);
+                }
+            }
+        };
     }
 
     /**
@@ -55,7 +135,15 @@
      *
      * @param navHandle to handle the animation for this touch
      */
-    public void onTouchStarted(NavHandle navHandle) {}
+    @VisibleForTesting
+    final void onTouchStarted(NavHandle navHandle) {
+        mPendingInvocation = false;
+        if (isContextualSearchEntrypointEnabled(navHandle)
+                && mContextualSearchInvoker.runContextualSearchInvocationChecksAndLogFailures()) {
+            Log.i(TAG, "Contextual Search invocation: touch started");
+            startNavBarAnimation(navHandle);
+        }
+    }
 
     /**
      * Called when nav handle gesture is finished by the user lifting their finger or the system
@@ -64,5 +152,46 @@
      * @param navHandle to handle the animation for this touch
      * @param reason why the touch ended
      */
-    public void onTouchFinished(NavHandle navHandle, String reason) {}
+    @VisibleForTesting
+    final void onTouchFinished(NavHandle navHandle, String reason) {
+        Log.i(TAG, "Contextual Search invocation: touch finished with reason: " + reason);
+
+        if (!DeviceConfigWrapper.get().getShrinkNavHandleOnPress() || !mPendingInvocation) {
+            mVibratorWrapper.cancelVibrate();
+        }
+
+        if (DeviceConfigWrapper.get().getAnimateLpnh()) {
+            if (DeviceConfigWrapper.get().getShrinkNavHandleOnPress()) {
+                navHandle.animateNavBarLongPress(
+                        /*isTouchDown*/false, /*shrink*/ true, /*durationMs*/200);
+            } else {
+                navHandle.animateNavBarLongPress(
+                        /*isTouchDown*/false, /*shrink*/ false, /*durationMs*/ 160);
+            }
+        }
+    }
+
+    @VisibleForTesting
+    final void startNavBarAnimation(NavHandle navHandle) {
+        mContextualSearchHapticManager.vibrateForSearchHint();
+
+        if (DeviceConfigWrapper.get().getAnimateLpnh()) {
+            if (DeviceConfigWrapper.get().getShrinkNavHandleOnPress()) {
+                navHandle.animateNavBarLongPress(
+                        /*isTouchDown*/ true, /*shrink*/true, /*durationMs*/200);
+            } else {
+                long longPressTimeout;
+                ContextualSearchStateManager contextualSearchStateManager =
+                        ContextualSearchStateManager.INSTANCE.get(mContext);
+                if (contextualSearchStateManager.getLPNHDurationMillis().isPresent()) {
+                    longPressTimeout =
+                            contextualSearchStateManager.getLPNHDurationMillis().get().intValue();
+                } else {
+                    longPressTimeout = ViewConfiguration.getLongPressTimeout();
+                }
+                navHandle.animateNavBarLongPress(
+                        /*isTouchDown*/ true, /*shrink*/ false, /*durationMs*/ longPressTimeout);
+            }
+        }
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java
index f4d3695..f5bef05e 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java
@@ -38,7 +38,7 @@
 import com.android.quickstep.NavHandle;
 import com.android.quickstep.RecentsAnimationDeviceState;
 import com.android.quickstep.TopTaskTracker;
-import com.android.quickstep.util.AssistStateManager;
+import com.android.quickstep.util.ContextualSearchStateManager;
 import com.android.systemui.shared.system.InputMonitorCompat;
 
 /**
@@ -75,9 +75,11 @@
         super(delegate, inputMonitor);
         mScreenWidth = DisplayController.INSTANCE.get(context).getInfo().currentSize.x;
         mDeepPressEnabled = DeviceConfigWrapper.get().getEnableLpnhDeepPress();
-        AssistStateManager assistStateManager = AssistStateManager.INSTANCE.get(context);
-        if (assistStateManager.getLPNHDurationMillis().isPresent()) {
-            mLongPressTimeout = assistStateManager.getLPNHDurationMillis().get().intValue();
+        ContextualSearchStateManager contextualSearchStateManager =
+                ContextualSearchStateManager.INSTANCE.get(context);
+        if (contextualSearchStateManager.getLPNHDurationMillis().isPresent()) {
+            mLongPressTimeout =
+                    contextualSearchStateManager.getLPNHDurationMillis().get().intValue();
         } else {
             mLongPressTimeout = ViewConfiguration.getLongPressTimeout();
         }
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
index 69d3bc9..c4198db 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
@@ -41,6 +41,7 @@
 
 import androidx.annotation.UiThread;
 
+import com.android.launcher3.Flags;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.testing.TestLogging;
@@ -156,6 +157,7 @@
         mStartDisplacement = continuingPreviousGesture ? 0 : -mTouchSlop;
         mDisableHorizontalSwipe = !mPassedPilferInputSlop && disableHorizontalSwipe;
         mRotationTouchHelper = mDeviceState.getRotationTouchHelper();
+
     }
 
     @Override
@@ -424,7 +426,11 @@
             mTaskAnimationManager.notifyRecentsAnimationState(mInteractionHandler);
             notifyGestureStarted(true /*isLikelyToStartNewTask*/);
         } else {
-            Intent intent = new Intent(mInteractionHandler.getLaunchIntent());
+            // todo differentiate intent based on if we are on home or in app for overview in window
+            boolean useHomeIntentForWindow = Flags.enableFallbackOverviewInWindow()
+                    || Flags.enableLauncherOverviewInWindow();
+            Intent intent = new Intent(useHomeIntentForWindow ? mInteractionHandler.getHomeIntent()
+                : mInteractionHandler.getLaunchIntent());
             intent.putExtra(INTENT_EXTRA_LOG_TRACE_ID, mGestureState.getGestureId());
             mActiveCallbacks = mTaskAnimationManager.startRecentsAnimation(mGestureState, intent,
                     mInteractionHandler);
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java
index c61f71d..a236eca 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java
@@ -28,6 +28,7 @@
 
 import com.android.launcher3.Utilities;
 import com.android.launcher3.statemanager.BaseState;
+import com.android.launcher3.statemanager.StatefulContainer;
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.shared.TestProtocol;
 import com.android.launcher3.views.BaseDragLayer;
@@ -41,7 +42,8 @@
 /**
  * Input consumer for handling touch on the recents/Launcher activity.
  */
-public class OverviewInputConsumer<S extends BaseState<S>, T extends RecentsViewContainer>
+public class OverviewInputConsumer<S extends BaseState<S>,
+        T extends RecentsViewContainer & StatefulContainer<S>>
         implements InputConsumer {
 
     private final T mContainer;
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java
index 5ad55ae..49bff8d 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java
@@ -228,10 +228,13 @@
         }
 
         float velocityYPxPerS = mVelocityTracker.getYVelocity();
+        float dY = Math.abs(mLastPos.y - mDownPos.y);
         if (mCanPlayTaskbarBgAlphaAnimation
                 && mMotionMoveCount >= NUM_MOTION_MOVE_THRESHOLD // Arbitrary value
                 && velocityYPxPerS != 0 // Ignore these
-                && velocityYPxPerS >= mTaskbarSlowVelocityYThreshold) {
+                && velocityYPxPerS >= mTaskbarSlowVelocityYThreshold
+                && dY != 0
+                && dY > mTouchSlop) {
             mTaskbarActivityContext.playTaskbarBackgroundAlphaAnimation();
             mCanPlayTaskbarBgAlphaAnimation = false;
         }
diff --git a/quickstep/src/com/android/quickstep/interaction/TutorialController.java b/quickstep/src/com/android/quickstep/interaction/TutorialController.java
index 5028da4..9510a05 100644
--- a/quickstep/src/com/android/quickstep/interaction/TutorialController.java
+++ b/quickstep/src/com/android/quickstep/interaction/TutorialController.java
@@ -52,7 +52,6 @@
 import androidx.annotation.StringRes;
 import androidx.annotation.StyleRes;
 import androidx.appcompat.app.AlertDialog;
-import androidx.appcompat.content.res.AppCompatResources;
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.R;
@@ -610,8 +609,8 @@
 
     private void updateDrawables() {
         if (mContext != null) {
-            mTutorialFragment.getRootView().setBackground(AppCompatResources.getDrawable(
-                    mContext, getMockWallpaperResId()));
+            mTutorialFragment.getRootView()
+                    .setBackground(mContext.getDrawable(getMockWallpaperResId()));
             mTutorialFragment.updateFeedbackAnimation();
             mFakeLauncherView.setBackgroundColor(getFakeLauncherColor());
             updateFakeViewLayout(mFakeHotseatView, getMockHotseatResId());
@@ -619,9 +618,7 @@
             mFakeTaskView.animate().alpha(1).setListener(
                     AnimatorListeners.forSuccessCallback(() -> mFakeTaskView.animate().cancel()));
             mFakePreviousTaskView.setFakeTaskViewFillColor(getMockPreviousAppTaskThumbnailColor());
-            mFakeIconView.setBackground(AppCompatResources.getDrawable(
-                    mContext, getMockAppIconResId()));
-
+            mFakeIconView.setBackground(mContext.getDrawable(getMockAppIconResId()));
             mExitingAppView.setBackgroundColor(getExitingAppColor());
             mFakeTaskView.setBackgroundColor(getFakeTaskViewColor());
             updateHotseatChildViewColor(mHotseatIconView);
diff --git a/quickstep/src/com/android/quickstep/interaction/TutorialStepIndicator.java b/quickstep/src/com/android/quickstep/interaction/TutorialStepIndicator.java
index ae0e725..f1fc179 100644
--- a/quickstep/src/com/android/quickstep/interaction/TutorialStepIndicator.java
+++ b/quickstep/src/com/android/quickstep/interaction/TutorialStepIndicator.java
@@ -22,8 +22,6 @@
 import android.widget.ImageView;
 import android.widget.LinearLayout;
 
-import androidx.appcompat.content.res.AppCompatResources;
-
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.icons.GraphicsUtils;
@@ -91,9 +89,8 @@
         int inactiveStepIndicatorColor = GraphicsUtils.getAttrColor(
                 getContext(), android.R.attr.textColorSecondaryInverse);
         for (int i = 0; i < mTotalSteps; i++) {
-            Drawable pageIndicatorPillDrawable = AppCompatResources.getDrawable(
-                    getContext(), R.drawable.tutorial_step_indicator_pill);
-
+            Drawable pageIndicatorPillDrawable =
+                    getContext().getDrawable(R.drawable.tutorial_step_indicator_pill);
             if (i >= getChildCount()) {
                 ImageView pageIndicatorPill = new ImageView(getContext());
                 pageIndicatorPill.setImageDrawable(pageIndicatorPillDrawable);
diff --git a/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java b/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java
index 995635f..dd721e1 100644
--- a/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java
+++ b/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java
@@ -44,20 +44,16 @@
 import com.android.launcher3.LauncherPrefs;
 import com.android.launcher3.R;
 import com.android.launcher3.dagger.ApplicationContext;
-import com.android.launcher3.dagger.LauncherAppSingleton;
 import com.android.launcher3.logging.InstanceId;
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.logging.StatsLogManager.StatsLogger;
 import com.android.launcher3.model.DeviceGridState;
-import com.android.launcher3.util.DaggerSingletonObject;
-import com.android.launcher3.util.DaggerSingletonTracker;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.DisplayController.Info;
-import com.android.launcher3.util.ExecutorUtil;
+import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.util.NavigationMode;
 import com.android.launcher3.util.SafeCloseable;
 import com.android.launcher3.util.SettingsCache;
-import com.android.quickstep.dagger.QuickstepBaseAppComponent;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
@@ -65,12 +61,9 @@
 import java.io.IOException;
 import java.util.Optional;
 
-import javax.inject.Inject;
-
 /**
  * Utility class to log launcher settings changes
  */
-@LauncherAppSingleton
 public class SettingsChangeLogger implements
         DisplayController.DisplayInfoChangeListener, OnSharedPreferenceChangeListener,
         SafeCloseable {
@@ -78,8 +71,8 @@
     /**
      * Singleton instance
      */
-    public static DaggerSingletonObject<SettingsChangeLogger> INSTANCE =
-            new DaggerSingletonObject<>(QuickstepBaseAppComponent::getSettingsChangeLogger);
+    public static MainThreadInitializedObject<SettingsChangeLogger> INSTANCE =
+            new MainThreadInitializedObject<>(SettingsChangeLogger::new);
 
     private static final String TAG = "SettingsChangeLogger";
     private static final String BOOLEAN_PREF = "SwitchPreference";
@@ -92,31 +85,26 @@
     private StatsLogManager.LauncherEvent mNotificationDotsEvent;
     private StatsLogManager.LauncherEvent mHomeScreenSuggestionEvent;
 
-    @Inject
-    SettingsChangeLogger(@ApplicationContext Context context, DaggerSingletonTracker tracker) {
-        this(context, StatsLogManager.newInstance(context), tracker);
+    SettingsChangeLogger(@ApplicationContext Context context) {
+        this(context, StatsLogManager.newInstance(context));
     }
 
     @VisibleForTesting
-    SettingsChangeLogger(Context context, StatsLogManager statsLogManager,
-            DaggerSingletonTracker tracker) {
+    SettingsChangeLogger(Context context, StatsLogManager statsLogManager) {
         mContext = context;
         mStatsLogManager = statsLogManager;
         mLoggablePrefs = loadPrefKeys(context);
 
-        ExecutorUtil.executeSyncOnMainOrFail(() -> {
-            DisplayController.INSTANCE.get(context).addChangeListener(this);
-            mNavMode = DisplayController.getNavigationMode(context);
+        DisplayController.INSTANCE.get(context).addChangeListener(this);
+        mNavMode = DisplayController.getNavigationMode(context);
 
-            getPrefs(context).registerOnSharedPreferenceChangeListener(this);
-            getDevicePrefs(context).registerOnSharedPreferenceChangeListener(this);
+        getPrefs(context).registerOnSharedPreferenceChangeListener(this);
+        getDevicePrefs(context).registerOnSharedPreferenceChangeListener(this);
 
-            SettingsCache settingsCache = SettingsCache.INSTANCE.get(context);
-            settingsCache.register(NOTIFICATION_BADGING_URI,
-                    this::onNotificationDotsChanged);
-            onNotificationDotsChanged(settingsCache.getValue(NOTIFICATION_BADGING_URI));
-            tracker.addCloseable(this);
-        });
+        SettingsCache settingsCache = SettingsCache.INSTANCE.get(context);
+        settingsCache.register(NOTIFICATION_BADGING_URI,
+                this::onNotificationDotsChanged);
+        onNotificationDotsChanged(settingsCache.getValue(NOTIFICATION_BADGING_URI));
     }
 
     private static ArrayMap<String, LoggablePref> loadPrefKeys(Context context) {
@@ -223,8 +211,6 @@
     public void close() {
         getPrefs(mContext).unregisterOnSharedPreferenceChangeListener(this);
         getDevicePrefs(mContext).unregisterOnSharedPreferenceChangeListener(this);
-        SettingsCache settingsCache = SettingsCache.INSTANCE.get(mContext);
-        settingsCache.unregister(NOTIFICATION_BADGING_URI, this::onNotificationDotsChanged);
     }
 
     @VisibleForTesting
diff --git a/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
index 1d4160d..2daaaf9 100644
--- a/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
+++ b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
@@ -386,12 +386,12 @@
                 // and then write to StatsLog.
                 app.getModel().enqueueModelUpdateTask((taskController, dataModel, apps) ->
                         write(event, applyOverwrites(mItemInfo.buildProto(
-                                dataModel.collections.get(mItemInfo.container)))));
+                                dataModel.collections.get(mItemInfo.container), mContext))));
             })) {
                 // Write log on the model thread so that logs do not go out of order
                 // (for eg: drop comes after drag)
                 Executors.MODEL_EXECUTOR.execute(
-                        () -> write(event, applyOverwrites(mItemInfo.buildProto())));
+                        () -> write(event, applyOverwrites(mItemInfo.buildProto(mContext))));
             }
         }
 
diff --git a/quickstep/src/com/android/quickstep/recents/data/RecentTasksRepository.kt b/quickstep/src/com/android/quickstep/recents/data/RecentTasksRepository.kt
index 9c4248c..3b59864 100644
--- a/quickstep/src/com/android/quickstep/recents/data/RecentTasksRepository.kt
+++ b/quickstep/src/com/android/quickstep/recents/data/RecentTasksRepository.kt
@@ -40,5 +40,5 @@
      * Sets the tasks that are visible, indicating that properties relating to visuals need to be
      * populated e.g. icons/thumbnails etc.
      */
-    fun setVisibleTasks(visibleTaskIdList: List<Int>)
+    fun setVisibleTasks(visibleTaskIdList: Set<Int>)
 }
diff --git a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
index eb3c2d1..6c627ef 100644
--- a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
+++ b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
@@ -17,151 +17,160 @@
 package com.android.quickstep.recents.data
 
 import android.graphics.drawable.Drawable
+import android.util.Log
 import com.android.launcher3.util.coroutines.DispatcherProvider
 import com.android.quickstep.recents.data.TaskVisualsChangedDelegate.TaskIconChangedCallback
 import com.android.quickstep.recents.data.TaskVisualsChangedDelegate.TaskThumbnailChangedCallback
 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
 import com.android.systemui.shared.recents.model.ThumbnailData
 import kotlin.coroutines.resume
 import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.Job
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.callbackFlow
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.distinctUntilChangedBy
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.flow.update
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.suspendCancellableCoroutine
 import kotlinx.coroutines.withContext
 
-@OptIn(ExperimentalCoroutinesApi::class)
 class TasksRepository(
     private val recentsModel: RecentTasksDataSource,
     private val taskThumbnailDataSource: TaskThumbnailDataSource,
     private val taskIconDataSource: TaskIconDataSource,
     private val taskVisualsChangedDelegate: TaskVisualsChangedDelegate,
-    recentsCoroutineScope: CoroutineScope,
+    private val recentsCoroutineScope: CoroutineScope,
     private val dispatcherProvider: DispatcherProvider,
 ) : RecentTasksRepository {
-    private val groupedTaskData = MutableStateFlow(emptyList<GroupTask>())
-    private val visibleTaskIds = MutableStateFlow(emptySet<Int>())
-
-    private val taskData =
-        groupedTaskData.map { groupTaskList -> groupTaskList.flatMap { it.tasks } }
-    private val visibleTasks =
-        combine(taskData, visibleTaskIds) { tasks, visibleIds ->
-            tasks.filter { it.key.id in visibleIds }
-        }
-
-    private val iconQueryResults: Flow<Map<Int, TaskIconQueryResponse?>> =
-        visibleTasks
-            .map { visibleTasksList -> visibleTasksList.map(::getIconDataRequest) }
-            .flatMapLatest { iconRequestFlows: List<IconDataRequest> ->
-                if (iconRequestFlows.isEmpty()) {
-                    flowOf(emptyMap())
-                } else {
-                    combine(iconRequestFlows) { it.toMap() }
-                }
-            }
-            .distinctUntilChanged()
-
-    private val thumbnailQueryResults: Flow<Map<Int, ThumbnailData?>> =
-        visibleTasks
-            .map { visibleTasksList -> visibleTasksList.map(::getThumbnailDataRequest) }
-            .flatMapLatest { thumbnailRequestFlows: List<ThumbnailDataRequest> ->
-                if (thumbnailRequestFlows.isEmpty()) {
-                    flowOf(emptyMap())
-                } else {
-                    combine(thumbnailRequestFlows) { it.toMap() }
-                }
-            }
-            .distinctUntilChanged()
-
-    private val augmentedTaskData: Flow<List<Task>> =
-        combine(taskData, thumbnailQueryResults, iconQueryResults) {
-                tasks,
-                thumbnailQueryResults,
-                iconQueryResults ->
-                tasks.onEach { task ->
-                    // Add retrieved thumbnails + remove unnecessary thumbnails (e.g. invisible)
-                    task.thumbnail = thumbnailQueryResults[task.key.id]
-
-                    // TODO(b/352331675) don't load icons for DesktopTaskView
-                    // Add retrieved icons + remove unnecessary icons
-                    val iconQueryResult = iconQueryResults[task.key.id]
-                    task.icon = iconQueryResult?.icon
-                    task.titleDescription = iconQueryResult?.contentDescription
-                    task.title = iconQueryResult?.title
-                }
-            }
-            .flowOn(dispatcherProvider.io)
-            .shareIn(recentsCoroutineScope, SharingStarted.WhileSubscribed(), replay = 1)
+    private val tasks = MutableStateFlow(MapForStateFlow<Int, Task>(emptyMap()))
+    private val taskRequests = HashMap<Int, Pair<Task.TaskKey, Job>>()
 
     override fun getAllTaskData(forceRefresh: Boolean): Flow<List<Task>> {
         if (forceRefresh) {
-            recentsModel.getTasks { groupedTaskData.value = it }
+            recentsModel.getTasks { result ->
+                tasks.value =
+                    MapForStateFlow(
+                        result
+                            .flatMap { groupTask -> groupTask.tasks }
+                            .associateBy { it.key.id }
+                            .also {
+                                // Clean tasks that are not in the latest group tasks list.
+                                val tasksNoLongerVisible = it.keys.subtract(tasks.value.keys)
+                                removeTasks(tasksNoLongerVisible)
+                            }
+                    )
+            }
         }
-        return augmentedTaskData
+        return tasks.map { it.values.toList() }
     }
 
-    override fun getTaskDataById(taskId: Int): Flow<Task?> =
-        augmentedTaskData.map { taskList -> taskList.firstOrNull { it.key.id == taskId } }
+    override fun getTaskDataById(taskId: Int) = tasks.map { it[taskId] }
 
-    override fun getThumbnailById(taskId: Int): Flow<ThumbnailData?> =
+    override fun getThumbnailById(taskId: Int) =
         getTaskDataById(taskId).map { it?.thumbnail }.distinctUntilChangedBy { it?.snapshotId }
 
-    override fun setVisibleTasks(visibleTaskIdList: List<Int>) {
-        this.visibleTaskIds.value = visibleTaskIdList.toSet()
+    override fun setVisibleTasks(visibleTaskIdList: Set<Int>) {
+        val tasksNoLongerVisible = taskRequests.keys.subtract(visibleTaskIdList)
+        val newlyVisibleTasks = visibleTaskIdList.subtract(taskRequests.keys)
+        if (tasksNoLongerVisible.isNotEmpty() || newlyVisibleTasks.isNotEmpty()) {
+            Log.d(
+                TAG,
+                "setVisibleTasks to: $visibleTaskIdList, " +
+                    "removed: $tasksNoLongerVisible, added: $newlyVisibleTasks",
+            )
+        }
+
+        // Remove tasks are no longer visible
+        removeTasks(tasksNoLongerVisible)
+        // Add new tasks to be requested
+        newlyVisibleTasks.forEach { taskId -> requestTaskData(taskId) }
     }
 
-    /** Flow wrapper for [TaskThumbnailDataSource.getThumbnailInBackground] api */
-    private fun getThumbnailDataRequest(task: Task): ThumbnailDataRequest = callbackFlow {
-        trySend(task.key.id to task.thumbnail)
-        trySend(task.key.id to getThumbnailFromDataSource(task))
+    private fun requestTaskData(taskId: Int) {
+        val task = tasks.value[taskId] ?: return
+        taskRequests[taskId] =
+            Pair(
+                task.key,
+                recentsCoroutineScope.launch {
+                    Log.i(TAG, "requestTaskData: $taskId")
+                    fetchIcon(task)
+                    fetchThumbnail(task)
+                },
+            )
+    }
 
-        val callback =
+    private fun removeTasks(tasksToRemove: Set<Int>) {
+        if (tasksToRemove.isEmpty()) return
+
+        Log.i(TAG, "removeTasks: $tasksToRemove")
+        tasksToRemove.forEach { taskId ->
+            val request = taskRequests.remove(taskId) ?: return
+            val (taskKey, job) = request
+            job.cancel()
+
+            // un-registering callbacks
+            taskVisualsChangedDelegate.unregisterTaskIconChangedCallback(taskKey)
+            taskVisualsChangedDelegate.unregisterTaskThumbnailChangedCallback(taskKey)
+
+            // Clearing Task to reduce memory footprint
+            tasks.value[taskId]?.apply {
+                thumbnail = null
+                icon = null
+                title = null
+                titleDescription = null
+            }
+        }
+        tasks.update { oldValue -> MapForStateFlow(oldValue) }
+    }
+
+    private suspend fun fetchIcon(task: Task) {
+        updateIcon(task.key.id, getIconFromDataSource(task)) // Fetch icon from cache
+        taskVisualsChangedDelegate.registerTaskIconChangedCallback(
+            task.key,
+            object : TaskIconChangedCallback {
+                override fun onTaskIconChanged() {
+                    recentsCoroutineScope.launch {
+                        updateIcon(task.key.id, getIconFromDataSource(task))
+                    }
+                }
+            },
+        )
+    }
+
+    private suspend fun fetchThumbnail(task: Task) {
+        updateThumbnail(task.key.id, getThumbnailFromDataSource(task))
+        taskVisualsChangedDelegate.registerTaskThumbnailChangedCallback(
+            task.key,
             object : TaskThumbnailChangedCallback {
                 override fun onTaskThumbnailChanged(thumbnailData: ThumbnailData?) {
-                    trySend(task.key.id to thumbnailData)
+                    updateThumbnail(task.key.id, thumbnailData)
                 }
 
                 override fun onHighResLoadingStateChanged() {
-                    launch { trySend(task.key.id to getThumbnailFromDataSource(task)) }
+                    recentsCoroutineScope.launch {
+                        updateThumbnail(task.key.id, getThumbnailFromDataSource(task))
+                    }
                 }
-            }
-        taskVisualsChangedDelegate.registerTaskThumbnailChangedCallback(task.key, callback)
-        awaitClose { taskVisualsChangedDelegate.unregisterTaskThumbnailChangedCallback(task.key) }
+            },
+        )
     }
 
-    /** Flow wrapper for [TaskIconDataSource.getIconInBackground] api */
-    private fun getIconDataRequest(task: Task): IconDataRequest =
-        callbackFlow {
-                trySend(task.key.id to task.getTaskIconQueryResponse())
-                trySend(task.key.id to getIconFromDataSource(task))
+    private fun updateIcon(taskId: Int, iconData: IconData) {
+        val task = tasks.value[taskId] ?: return
+        task.icon = iconData.icon
+        task.titleDescription = iconData.contentDescription
+        task.title = iconData.title
+        tasks.update { oldValue -> MapForStateFlow(oldValue + (taskId to task)) }
+    }
 
-                val callback =
-                    object : TaskIconChangedCallback {
-                        override fun onTaskIconChanged() {
-                            launch { trySend(task.key.id to getIconFromDataSource(task)) }
-                        }
-                    }
-                taskVisualsChangedDelegate.registerTaskIconChangedCallback(task.key, callback)
-                awaitClose {
-                    taskVisualsChangedDelegate.unregisterTaskIconChangedCallback(task.key)
-                }
-            }
-            .distinctUntilChanged()
+    private fun updateThumbnail(taskId: Int, thumbnail: ThumbnailData?) {
+        val task = tasks.value[taskId] ?: return
+        task.thumbnail = thumbnail
+        tasks.update { oldValue -> MapForStateFlow(oldValue + (taskId to task)) }
+    }
 
     private suspend fun getThumbnailFromDataSource(task: Task) =
         withContext(dispatcherProvider.main) {
@@ -182,33 +191,27 @@
                         ->
                         icon.constantState?.let {
                             continuation.resume(
-                                TaskIconQueryResponse(
-                                    it.newDrawable().mutate(),
-                                    contentDescription,
-                                    title
-                                )
+                                IconData(it.newDrawable().mutate(), contentDescription, title)
                             )
                         }
                     }
                 continuation.invokeOnCancellation { cancellableTask?.cancel() }
             }
         }
+
+    companion object {
+        private const val TAG = "TasksRepository"
+    }
+
+    /** Helper class to support StateFlow emissions when using a Map with a MutableStateFlow. */
+    private data class MapForStateFlow<K, T>(
+        private val backingMap: Map<K, T>,
+        private val updated: Long = System.nanoTime(),
+    ) : Map<K, T> by backingMap
+
+    private data class IconData(
+        val icon: Drawable,
+        val contentDescription: String,
+        val title: String,
+    )
 }
-
-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/di/RecentsDependencies.kt b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
index 0a5544f..dd11d48 100644
--- a/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
+++ b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
@@ -19,6 +19,7 @@
 import android.content.Context
 import android.util.Log
 import android.view.View
+import com.android.launcher3.util.coroutines.DispatcherProvider
 import com.android.launcher3.util.coroutines.ProductionDispatchers
 import com.android.quickstep.RecentsModel
 import com.android.quickstep.recents.data.RecentTasksRepository
@@ -34,6 +35,7 @@
 import com.android.quickstep.task.viewmodel.TaskContainerData
 import com.android.quickstep.task.viewmodel.TaskOverlayViewModel
 import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel
+import com.android.quickstep.task.viewmodel.TaskThumbnailViewModelImpl
 import com.android.quickstep.task.viewmodel.TaskViewData
 import com.android.quickstep.task.viewmodel.TaskViewModel
 import com.android.quickstep.views.TaskViewType
@@ -62,11 +64,12 @@
             val recentsCoroutineScope =
                 CoroutineScope(SupervisorJob() + Dispatchers.Main + CoroutineName("RecentsView"))
             set(CoroutineScope::class.java.simpleName, recentsCoroutineScope)
+            set(DispatcherProvider::class.java.simpleName, ProductionDispatchers)
             val recentsModel = RecentsModel.INSTANCE.get(appContext)
             val taskVisualsChangedDelegate =
                 TaskVisualsChangedDelegateImpl(
                     recentsModel,
-                    recentsModel.thumbnailCache.highResLoadingState
+                    recentsModel.thumbnailCache.highResLoadingState,
                 )
             set(TaskVisualsChangedDelegate::class.java.simpleName, taskVisualsChangedDelegate)
 
@@ -79,7 +82,7 @@
                         iconCache,
                         taskVisualsChangedDelegate,
                         recentsCoroutineScope,
-                        ProductionDispatchers
+                        ProductionDispatchers,
                     )
                 }
             set(RecentTasksRepository::class.java.simpleName, recentTasksRepository)
@@ -112,6 +115,10 @@
                 instance =
                     factory?.invoke(extras) as T ?: createDependency(modelClass, scopeId, extras)
                 scope[modelClass.simpleName] = instance!!
+                log(
+                    "instance of $modelClass" +
+                        " (${instance.hashCode()}) added to scope ${scope.scopeId}"
+                )
             }
         }
         return instance!!
@@ -147,6 +154,13 @@
     fun getScope(scopeId: RecentsScopeId): RecentsDependenciesScope =
         scopes[scopeId] ?: createScope(scopeId)
 
+    fun removeScope(scope: Any) {
+        val scopeId: RecentsScopeId = scope as? RecentsScopeId ?: scope.hashCode().toString()
+        scopes[scopeId]?.close()
+        scopes.remove(scopeId)
+        log("Scope $scopeId removed")
+    }
+
     // TODO(b/353912757): Create a factory so we can prevent this method of growing indefinitely.
     //  Each class should be responsible for providing a factory function to create a new instance.
     @Suppress("UNCHECKED_CAST")
@@ -155,7 +169,8 @@
         scopeId: RecentsScopeId,
         extras: RecentsDependenciesExtras,
     ): T {
-        log("createDependency ${modelClass.simpleName} with $scopeId and $extras", Log.WARN)
+        log("createDependency ${modelClass.simpleName} with $scopeId and $extras started", Log.WARN)
+        log("linked scopes: ${getScope(scopeId).scopeIdsLinked}")
         val instance: Any =
             when (modelClass) {
                 RecentTasksRepository::class.java -> {
@@ -166,7 +181,7 @@
                             iconCache,
                             get(),
                             get(),
-                            ProductionDispatchers
+                            ProductionDispatchers,
                         )
                     }
                 }
@@ -179,10 +194,11 @@
                 TaskContainerData::class.java -> TaskContainerData()
                 TaskThumbnailViewData::class.java -> TaskThumbnailViewData()
                 TaskThumbnailViewModel::class.java ->
-                    TaskThumbnailViewModel(
+                    TaskThumbnailViewModelImpl(
                         recentsViewData = inject(),
                         taskViewData = inject(scopeId, extras),
                         taskContainerData = inject(scopeId),
+                        dispatcherProvider = inject(),
                         getThumbnailPositionUseCase = inject(),
                         tasksRepository = inject(),
                         splashAlphaUseCase = inject(scopeId),
@@ -193,7 +209,7 @@
                         task = task,
                         recentsViewData = inject(),
                         recentTasksRepository = inject(),
-                        getThumbnailPositionUseCase = inject()
+                        getThumbnailPositionUseCase = inject(),
                     )
                 }
                 GetThumbnailUseCase::class.java -> GetThumbnailUseCase(taskRepository = inject())
@@ -203,7 +219,7 @@
                     GetThumbnailPositionUseCase(
                         deviceProfileRepository = inject(),
                         rotationStateRepository = inject(),
-                        tasksRepository = inject()
+                        tasksRepository = inject(),
                     )
                 SplashAlphaUseCase::class.java ->
                     SplashAlphaUseCase(
@@ -218,7 +234,12 @@
                     error("Factory for ${modelClass.simpleName} not defined!")
                 }
             }
-        return instance as T
+        return (instance as T).also {
+            log(
+                "createDependency ${modelClass.simpleName} with $scopeId and $extras completed",
+                Log.WARN,
+            )
+        }
     }
 
     private fun createScope(scopeId: RecentsScopeId): RecentsDependenciesScope {
@@ -247,11 +268,7 @@
         fun initialize(view: View): RecentsDependencies = initialize(view.context)
 
         fun initialize(context: Context): RecentsDependencies {
-            synchronized(this) {
-                if (!Companion::instance.isInitialized) {
-                    instance = RecentsDependencies(context.applicationContext)
-                }
-            }
+            synchronized(this) { instance = RecentsDependencies(context.applicationContext) }
             return instance
         }
 
diff --git a/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt
index 5cf6823..c511005 100644
--- a/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt
+++ b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt
@@ -31,7 +31,7 @@
     }
 
     fun updateVisibleTasks(visibleTaskIdList: List<Int>) {
-        recentsTasksRepository.setVisibleTasks(visibleTaskIdList)
+        recentsTasksRepository.setVisibleTasks(visibleTaskIdList.toSet())
     }
 
     fun updateScale(scale: Float) {
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
index 0279818..a8c8659 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
@@ -22,6 +22,7 @@
 import android.graphics.Outline
 import android.graphics.Rect
 import android.util.AttributeSet
+import android.util.Log
 import android.view.View
 import android.view.ViewOutlineProvider
 import androidx.annotation.ColorInt
@@ -31,7 +32,7 @@
 import com.android.launcher3.Utilities
 import com.android.launcher3.util.ViewPool
 import com.android.quickstep.recents.di.RecentsDependencies
-import com.android.quickstep.recents.di.inject
+import com.android.quickstep.recents.di.get
 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.BackgroundOnly
 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.LiveTile
 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot
@@ -41,7 +42,6 @@
 import com.android.quickstep.util.TaskCornerRadius
 import com.android.quickstep.views.FixedSizeImageView
 import com.android.systemui.shared.system.QuickStepContract
-import kotlin.math.abs
 import kotlinx.coroutines.CoroutineName
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
@@ -51,9 +51,8 @@
 import kotlinx.coroutines.flow.onEach
 
 class TaskThumbnailView : ConstraintLayout, ViewPool.Reusable {
-
-    private val viewData: TaskThumbnailViewData by RecentsDependencies.inject(this)
-    private val viewModel: TaskThumbnailViewModel by RecentsDependencies.inject(this)
+    private lateinit var viewData: TaskThumbnailViewData
+    private lateinit var viewModel: TaskThumbnailViewModel
 
     private lateinit var viewAttachedScope: CoroutineScope
 
@@ -90,8 +89,12 @@
         super.onAttachedToWindow()
         viewAttachedScope =
             CoroutineScope(SupervisorJob() + Dispatchers.Main + CoroutineName("TaskThumbnailView"))
+        viewData = RecentsDependencies.get(this)
+        updateViewDataValues()
+        viewModel = RecentsDependencies.get(this)
         viewModel.uiState
             .onEach { viewModelUiState ->
+                Log.d(TAG, "viewModelUiState changed from: $uiState to: $viewModelUiState")
                 uiState = viewModelUiState
                 resetViews()
                 when (viewModelUiState) {
@@ -140,11 +143,15 @@
     override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
         super.onLayout(changed, left, top, right, bottom)
         if (changed) {
-            viewData.width.value = abs(right - left)
-            viewData.height.value = abs(bottom - top)
+            updateViewDataValues()
         }
     }
 
+    private fun updateViewDataValues() {
+        viewData.width.value = width
+        viewData.height.value = height
+    }
+
     override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
         super.onSizeChanged(w, h, oldw, oldh)
         if (uiState is SnapshotSplash) {
@@ -211,6 +218,10 @@
         Utilities.mapRange(
             viewModel.cornerRadiusProgress.value,
             overviewCornerRadius,
-            fullscreenCornerRadius
+            fullscreenCornerRadius,
         ) / inheritedScale
+
+    private companion object {
+        const val TAG = "TaskThumbnailView"
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/task/util/TaskOverlayHelper.kt b/quickstep/src/com/android/quickstep/task/util/TaskOverlayHelper.kt
index 9253dbf..203177a 100644
--- a/quickstep/src/com/android/quickstep/task/util/TaskOverlayHelper.kt
+++ b/quickstep/src/com/android/quickstep/task/util/TaskOverlayHelper.kt
@@ -17,6 +17,7 @@
 package com.android.quickstep.task.util
 
 import android.util.Log
+import android.view.View.OnLayoutChangeListener
 import com.android.quickstep.TaskOverlayFactory
 import com.android.quickstep.recents.di.RecentsDependencies
 import com.android.quickstep.recents.di.get
@@ -41,31 +42,35 @@
     private lateinit var overlayInitializedScope: CoroutineScope
     private var uiState: TaskOverlayUiState = Disabled
 
-    private val viewModel: TaskOverlayViewModel by lazy {
-        TaskOverlayViewModel(
-            task = task,
-            recentsViewData = RecentsDependencies.get(),
-            getThumbnailPositionUseCase = RecentsDependencies.get(),
-            recentTasksRepository = RecentsDependencies.get()
-        )
-    }
+    private lateinit var viewModel: TaskOverlayViewModel
 
     // TODO(b/331753115): TaskOverlay should listen for state changes and react.
     val enabledState: Enabled
         get() = uiState as Enabled
 
+    private val snapshotLayoutChangeListener = OnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
+        (uiState as? Enabled)?.let { initOverlay(it) }
+    }
+
     fun getThumbnailMatrix() = getThumbnailPositionState().matrix
 
     private fun getThumbnailPositionState() =
         viewModel.getThumbnailPositionState(
             overlay.snapshotView.width,
             overlay.snapshotView.height,
-            overlay.snapshotView.isLayoutRtl
+            overlay.snapshotView.isLayoutRtl,
         )
 
     fun init() {
         overlayInitializedScope =
             CoroutineScope(SupervisorJob() + Dispatchers.Main + CoroutineName("TaskOverlayHelper"))
+        viewModel =
+            TaskOverlayViewModel(
+                task = task,
+                recentsViewData = RecentsDependencies.get(),
+                getThumbnailPositionUseCase = RecentsDependencies.get(),
+                recentTasksRepository = RecentsDependencies.get(),
+            )
         viewModel.overlayState
             .onEach {
                 uiState = it
@@ -76,30 +81,34 @@
                 }
             }
             .launchIn(overlayInitializedScope)
-        overlay.snapshotView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
-            (uiState as? Enabled)?.let { initOverlay(it) }
-        }
+        overlay.snapshotView.addOnLayoutChangeListener(snapshotLayoutChangeListener)
     }
 
     private fun initOverlay(enabledState: Enabled) {
-        Log.d(TAG, "initOverlay - taskId: ${task.key.id}, thumbnail: ${enabledState.thumbnail}")
+        if (DEBUG) {
+            Log.d(TAG, "initOverlay - taskId: ${task.key.id}, thumbnail: ${enabledState.thumbnail}")
+        }
         with(getThumbnailPositionState()) {
             overlay.initOverlay(task, enabledState.thumbnail, matrix, isRotated)
         }
     }
 
     private fun reset() {
-        Log.d(TAG, "reset - taskId: ${task.key.id}")
+        if (DEBUG) {
+            Log.d(TAG, "reset - taskId: ${task.key.id}")
+        }
         overlay.reset()
     }
 
     fun destroy() {
         overlayInitializedScope.cancel()
         uiState = Disabled
+        overlay.snapshotView.removeOnLayoutChangeListener(snapshotLayoutChangeListener)
         reset()
     }
 
     companion object {
         private const val TAG = "TaskOverlayHelper"
+        private const val DEBUG = false
     }
 }
diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt
index b1bb65e..f55462a 100644
--- a/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt
+++ b/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt
@@ -10,134 +10,40 @@
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language goveryning permissions and
+ * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 package com.android.quickstep.task.viewmodel
 
-import android.annotation.ColorInt
-import android.app.ActivityTaskManager.INVALID_TASK_ID
 import android.graphics.Matrix
-import androidx.core.graphics.ColorUtils
-import com.android.quickstep.recents.data.RecentTasksRepository
-import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
-import com.android.quickstep.recents.usecase.ThumbnailPositionState
-import com.android.quickstep.recents.viewmodel.RecentsViewData
-import com.android.quickstep.task.thumbnail.SplashAlphaUseCase
 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState
-import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.BackgroundOnly
-import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.LiveTile
-import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot
-import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.SnapshotSplash
-import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
-import com.android.systemui.shared.recents.model.Task
-import kotlin.math.max
-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
-import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.flow.StateFlow
 
-@OptIn(ExperimentalCoroutinesApi::class)
-class TaskThumbnailViewModel(
-    recentsViewData: RecentsViewData,
-    taskViewData: TaskViewData,
-    taskContainerData: TaskContainerData,
-    private val tasksRepository: RecentTasksRepository,
-    private val getThumbnailPositionUseCase: GetThumbnailPositionUseCase,
-    private val splashAlphaUseCase: SplashAlphaUseCase,
-) {
-    private val task = MutableStateFlow<Flow<Task?>>(flowOf(null))
-    private val splashProgress = MutableStateFlow(flowOf(0f))
-    private var taskId: Int = INVALID_TASK_ID
-
+/** ViewModel for representing TaskThumbnails */
+interface TaskThumbnailViewModel {
     /**
      * 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 cornerRadiusProgress: StateFlow<Float>
 
-    val inheritedScale =
-        combine(recentsViewData.scale, taskViewData.scale) { recentsScale, taskScale ->
-            recentsScale * taskScale
-        }
+    /** The accumulated View.scale value for parent Views up to and including RecentsView */
+    val inheritedScale: Flow<Float>
 
-    val dimProgress: Flow<Float> =
-        combine(taskContainerData.taskMenuOpenProgress, recentsViewData.tintAmount) {
-            taskMenuOpenProgress,
-            tintAmount ->
-            max(taskMenuOpenProgress * MAX_SCRIM_ALPHA, tintAmount)
-        }
-    val splashAlpha = splashProgress.flatMapLatest { it }
+    /** Provides the level of dimming that the View should have */
+    val dimProgress: Flow<Float>
 
-    private val isLiveTile =
-        combine(
-                task.flatMapLatest { it }.map { it?.key?.id }.distinctUntilChanged(),
-                recentsViewData.runningTaskIds,
-                recentsViewData.runningTaskShowScreenshot
-            ) { taskId, runningTaskIds, runningTaskShowScreenshot ->
-                runningTaskIds.contains(taskId) && !runningTaskShowScreenshot
-            }
-            .distinctUntilChanged()
+    /** Provides the alpha of the splash icon */
+    val splashAlpha: Flow<Float>
 
-    val uiState: Flow<TaskThumbnailUiState> =
-        combine(task.flatMapLatest { it }, isLiveTile) { taskVal, isRunning ->
-                when {
-                    taskVal == null -> Uninitialized
-                    isRunning -> LiveTile
-                    isBackgroundOnly(taskVal) ->
-                        BackgroundOnly(taskVal.colorBackground.removeAlpha())
-                    isSnapshotSplashState(taskVal) ->
-                        SnapshotSplash(createSnapshotState(taskVal), taskVal.icon)
-                    else -> Uninitialized
-                }
-            }
-            .distinctUntilChanged()
+    /** Provides the UiState by which the task thumbnail can be represented */
+    val uiState: Flow<TaskThumbnailUiState>
 
-    fun bind(taskId: Int) {
-        this.taskId = taskId
-        task.value = tasksRepository.getTaskDataById(taskId)
-        splashProgress.value = splashAlphaUseCase.execute(taskId)
-    }
+    /** Attaches this ViewModel to a specific task id for it to provide data from. */
+    fun bind(taskId: Int)
 
-    fun getThumbnailPositionState(width: Int, height: Int, isRtl: Boolean): Matrix {
-        return runBlocking {
-            when (
-                val thumbnailPositionState =
-                    getThumbnailPositionUseCase.run(taskId, width, height, isRtl)
-            ) {
-                is ThumbnailPositionState.MatrixScaling -> thumbnailPositionState.matrix
-                is ThumbnailPositionState.MissingThumbnail -> Matrix.IDENTITY_MATRIX
-            }
-        }
-    }
-
-    private fun isBackgroundOnly(task: Task): Boolean = task.isLocked || task.thumbnail == null
-
-    private fun isSnapshotSplashState(task: Task): Boolean {
-        val thumbnailPresent = task.thumbnail?.thumbnail != null
-        val taskLocked = task.isLocked
-
-        return thumbnailPresent && !taskLocked
-    }
-
-    private fun createSnapshotState(task: Task): Snapshot {
-        val thumbnailData = task.thumbnail
-        val bitmap = thumbnailData?.thumbnail!!
-        return Snapshot(bitmap, thumbnailData.rotation, task.colorBackground.removeAlpha())
-    }
-
-    @ColorInt private fun Int.removeAlpha(): Int = ColorUtils.setAlphaComponent(this, 0xff)
-
-    private companion object {
-        const val MAX_SCRIM_ALPHA = 0.4f
-    }
+    /** Returns a Matrix which can be applied to the snapshot */
+    fun getThumbnailPositionState(width: Int, height: Int, isRtl: Boolean): Matrix
 }
diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModelImpl.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModelImpl.kt
new file mode 100644
index 0000000..8b15a82
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModelImpl.kt
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language goveryning permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.task.viewmodel
+
+import android.annotation.ColorInt
+import android.app.ActivityTaskManager.INVALID_TASK_ID
+import android.graphics.Matrix
+import android.util.Log
+import androidx.core.graphics.ColorUtils
+import com.android.launcher3.util.coroutines.DispatcherProvider
+import com.android.quickstep.recents.data.RecentTasksRepository
+import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
+import com.android.quickstep.recents.usecase.ThumbnailPositionState
+import com.android.quickstep.recents.viewmodel.RecentsViewData
+import com.android.quickstep.task.thumbnail.SplashAlphaUseCase
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.BackgroundOnly
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.LiveTile
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.SnapshotSplash
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
+import com.android.systemui.shared.recents.model.Task
+import kotlin.math.max
+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
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.runBlocking
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class TaskThumbnailViewModelImpl(
+    recentsViewData: RecentsViewData,
+    taskViewData: TaskViewData,
+    taskContainerData: TaskContainerData,
+    dispatcherProvider: DispatcherProvider,
+    private val tasksRepository: RecentTasksRepository,
+    private val getThumbnailPositionUseCase: GetThumbnailPositionUseCase,
+    private val splashAlphaUseCase: SplashAlphaUseCase,
+) : TaskThumbnailViewModel {
+    private val task = MutableStateFlow<Flow<Task?>>(flowOf(null))
+    private val splashProgress = MutableStateFlow(flowOf(0f))
+    private var taskId: Int = INVALID_TASK_ID
+
+    override val cornerRadiusProgress =
+        if (taskViewData.isOutlineFormedByThumbnailView) recentsViewData.fullscreenProgress
+        else MutableStateFlow(1f).asStateFlow()
+
+    override val inheritedScale =
+        combine(recentsViewData.scale, taskViewData.scale) { recentsScale, taskScale ->
+            recentsScale * taskScale
+        }
+
+    override val dimProgress: Flow<Float> =
+        combine(taskContainerData.taskMenuOpenProgress, recentsViewData.tintAmount) {
+            taskMenuOpenProgress,
+            tintAmount ->
+            max(taskMenuOpenProgress * MAX_SCRIM_ALPHA, tintAmount)
+        }
+
+    override val splashAlpha = splashProgress.flatMapLatest { it }
+
+    private val isLiveTile =
+        combine(
+                task.flatMapLatest { it }.map { it?.key?.id }.distinctUntilChanged(),
+                recentsViewData.runningTaskIds,
+                recentsViewData.runningTaskShowScreenshot,
+            ) { taskId, runningTaskIds, runningTaskShowScreenshot ->
+                runningTaskIds.contains(taskId) && !runningTaskShowScreenshot
+            }
+            .distinctUntilChanged()
+            .flowOn(dispatcherProvider.default)
+
+    override val uiState: Flow<TaskThumbnailUiState> =
+        combine(task.flatMapLatest { it }, isLiveTile) { taskVal, isRunning ->
+                // TODO(b/369339561) This log is firing a lot. Reduce emissions from TasksRepository
+                //  then re-enable this log.
+                //                Log.d(
+                //                    TAG,
+                //                    "Received task and / or live tile update. taskVal: $taskVal"
+                //                    + " isRunning: $isRunning.",
+                //                )
+                when {
+                    taskVal == null -> Uninitialized
+                    isRunning -> LiveTile
+                    isBackgroundOnly(taskVal) ->
+                        BackgroundOnly(taskVal.colorBackground.removeAlpha())
+                    isSnapshotSplashState(taskVal) ->
+                        SnapshotSplash(createSnapshotState(taskVal), taskVal.icon)
+                    else -> Uninitialized
+                }
+            }
+            .distinctUntilChanged()
+            .flowOn(dispatcherProvider.default)
+
+    override fun bind(taskId: Int) {
+        Log.d(TAG, "bind taskId: $taskId")
+        this.taskId = taskId
+        task.value = tasksRepository.getTaskDataById(taskId)
+        splashProgress.value = splashAlphaUseCase.execute(taskId)
+    }
+
+    override fun getThumbnailPositionState(width: Int, height: Int, isRtl: Boolean): Matrix {
+        return runBlocking {
+            when (
+                val thumbnailPositionState =
+                    getThumbnailPositionUseCase.run(taskId, width, height, isRtl)
+            ) {
+                is ThumbnailPositionState.MatrixScaling -> thumbnailPositionState.matrix
+                is ThumbnailPositionState.MissingThumbnail -> Matrix.IDENTITY_MATRIX
+            }
+        }
+    }
+
+    private fun isBackgroundOnly(task: Task): Boolean = task.isLocked || task.thumbnail == null
+
+    private fun isSnapshotSplashState(task: Task): Boolean {
+        val thumbnailPresent = task.thumbnail?.thumbnail != null
+        val taskLocked = task.isLocked
+
+        return thumbnailPresent && !taskLocked
+    }
+
+    private fun createSnapshotState(task: Task): Snapshot {
+        val thumbnailData = task.thumbnail
+        val bitmap = thumbnailData?.thumbnail!!
+        return Snapshot(bitmap, thumbnailData.rotation, task.colorBackground.removeAlpha())
+    }
+
+    @ColorInt private fun Int.removeAlpha(): Int = ColorUtils.setAlphaComponent(this, 0xff)
+
+    private companion object {
+        const val MAX_SCRIM_ALPHA = 0.4f
+        const val TAG = "TaskThumbnailViewModel"
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/util/AppPairsController.java b/quickstep/src/com/android/quickstep/util/AppPairsController.java
index e1013db..1312aa4 100644
--- a/quickstep/src/com/android/quickstep/util/AppPairsController.java
+++ b/quickstep/src/com/android/quickstep/util/AppPairsController.java
@@ -26,7 +26,7 @@
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT;
-import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_50_50;
+import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50;
 import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_NONE;
 import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
 import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
@@ -189,7 +189,7 @@
         @PersistentSnapPosition int snapPosition = gtv.getSnapPosition();
         if (snapPosition == SNAP_TO_NONE) {
             // Free snap mode is enabled, just save it as 50/50 split.
-            snapPosition = SNAP_TO_50_50;
+            snapPosition = SNAP_TO_2_50_50;
         }
         if (!isPersistentSnapPosition(snapPosition)) {
             // If we received an illegal snap position, log an error and do not create the app pair
@@ -409,8 +409,8 @@
             );
         } else {
             // Tapped an app pair while in a single app
-            int runningTaskId = topTaskTracker
-                    .getCachedTopTask(false /* filterOnlyVisibleRecents */).getTaskId();
+            final TopTaskTracker.CachedTaskInfo runningTask = topTaskTracker
+                    .getCachedTopTask(false /* filterOnlyVisibleRecents */);
 
             mSplitSelectStateController.findLastActiveTasksAndRunCallback(
                     componentKeys,
@@ -418,10 +418,21 @@
                     foundTasks -> {
                         Task foundTask1 = foundTasks[0];
                         Task foundTask2 = foundTasks[1];
-                        boolean task1IsOnScreen =
-                                foundTask1 != null && foundTask1.getKey().getId() == runningTaskId;
-                        boolean task2IsOnScreen =
-                                foundTask2 != null && foundTask2.getKey().getId() == runningTaskId;
+                        boolean task1IsOnScreen;
+                        boolean task2IsOnScreen;
+                        if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+                            task1IsOnScreen = foundTask1 != null
+                                    && runningTask.topGroupedTaskContainsTask(
+                                    foundTask1.getKey().getId());
+                            task2IsOnScreen = foundTask2 != null
+                                    && runningTask.topGroupedTaskContainsTask(
+                                    foundTask2.getKey().getId());
+                        } else {
+                            task1IsOnScreen = foundTask1 != null && foundTask1.getKey().getId()
+                                    == runningTask.getTaskId();
+                            task2IsOnScreen = foundTask2 != null && foundTask2.getKey().getId()
+                                    == runningTask.getTaskId();
+                        }
 
                         if (!task1IsOnScreen && !task2IsOnScreen) {
                             // Neither App A nor App B are on-screen, launch the app pair normally.
diff --git a/quickstep/src/com/android/quickstep/util/AssistStateManager.java b/quickstep/src/com/android/quickstep/util/AssistStateManager.java
deleted file mode 100644
index 7acb28d..0000000
--- a/quickstep/src/com/android/quickstep/util/AssistStateManager.java
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.util;
-
-import static com.android.launcher3.util.MainThreadInitializedObject.forOverride;
-
-import com.android.launcher3.R;
-import com.android.launcher3.util.MainThreadInitializedObject;
-import com.android.launcher3.util.ResourceBasedOverride;
-import com.android.launcher3.util.SafeCloseable;
-
-import java.io.PrintWriter;
-import java.util.Optional;
-
-/** Class to manage Assistant states. */
-public class AssistStateManager implements ResourceBasedOverride, SafeCloseable {
-
-    public static final MainThreadInitializedObject<AssistStateManager> INSTANCE =
-            forOverride(AssistStateManager.class, R.string.assist_state_manager_class);
-
-    public AssistStateManager() {}
-
-    /** Return {@code true} if the Settings toggle is enabled. */
-    public boolean isSettingsAllEntrypointsEnabled() {
-        return false;
-    }
-
-    /** Whether search supports showing on the lockscreen. */
-    public boolean supportsShowWhenLocked() {
-        return false;
-    }
-
-    /** Whether ContextualSearchService invocation path is available. */
-    public boolean isContextualSearchServiceAvailable() {
-        return false;
-    }
-
-    /** Get the Launcher overridden long press nav handle duration to trigger Assistant. */
-    public Optional<Long> getLPNHDurationMillis() {
-        return Optional.empty();
-    }
-
-    /**
-     * Get the Launcher overridden long press nav handle touch slop multiplier to trigger Assistant.
-     */
-    public Optional<Float> getLPNHCustomSlopMultiplier() {
-        return Optional.empty();
-    }
-
-    /** Get the Launcher overridden long press home duration to trigger Assistant. */
-    public Optional<Long> getLPHDurationMillis() {
-        return Optional.empty();
-    }
-
-    /** Get the Launcher overridden long press home touch slop multiplier to trigger Assistant. */
-    public Optional<Float> getLPHCustomSlopMultiplier() {
-        return Optional.empty();
-    }
-
-    /** Get the long press duration data source. */
-    public int getDurationDataSource() {
-        return 0;
-    }
-
-    /** Get the long press touch slop multiplier data source. */
-    public int getSlopDataSource() {
-        return 0;
-    }
-
-    /** Get the haptic bit overridden by AGSA. */
-    public Optional<Boolean> getShouldPlayHapticOverride() {
-        return Optional.empty();
-    }
-
-    /** Dump states. */
-    public void dump(String prefix, PrintWriter writer) {}
-
-    @Override
-    public void close() {}
-}
diff --git a/quickstep/src/com/android/quickstep/util/AssistUtils.java b/quickstep/src/com/android/quickstep/util/AssistUtils.java
deleted file mode 100644
index 11b6ea7..0000000
--- a/quickstep/src/com/android/quickstep/util/AssistUtils.java
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.util;
-
-import android.content.Context;
-
-import com.android.launcher3.R;
-import com.android.launcher3.util.ResourceBasedOverride;
-
-/** Utilities to work with Assistant functionality. */
-public class AssistUtils implements ResourceBasedOverride {
-
-    public AssistUtils() {}
-
-    /** Creates AssistUtils as specified by overrides */
-    public static AssistUtils newInstance(Context context) {
-        return Overrides.getObject(AssistUtils.class, context, R.string.assist_utils_class);
-    }
-
-    /** @return Array of AssistUtils.INVOCATION_TYPE_* that we want to handle instead of SysUI. */
-    public int[] getSysUiAssistOverrideInvocationTypes() {
-        return new int[0];
-    }
-
-    /**
-     * @return {@code true} if the override was handled, i.e. an assist surface was shown or the
-     * request should be ignored. {@code false} means the caller should start assist another way.
-     */
-    public boolean tryStartAssistOverride(int invocationType) {
-        return false;
-    }
-}
diff --git a/quickstep/src/com/android/quickstep/util/AsyncClockEventDelegate.java b/quickstep/src/com/android/quickstep/util/AsyncClockEventDelegate.java
index 38ae303..54f6443 100644
--- a/quickstep/src/com/android/quickstep/util/AsyncClockEventDelegate.java
+++ b/quickstep/src/com/android/quickstep/util/AsyncClockEventDelegate.java
@@ -32,25 +32,33 @@
 
 import androidx.annotation.WorkerThread;
 
-import com.android.launcher3.util.MainThreadInitializedObject;
+import com.android.launcher3.dagger.ApplicationContext;
+import com.android.launcher3.dagger.LauncherAppSingleton;
+import com.android.launcher3.util.DaggerSingletonObject;
+import com.android.launcher3.util.DaggerSingletonTracker;
 import com.android.launcher3.util.SafeCloseable;
 import com.android.launcher3.util.SettingsCache;
 import com.android.launcher3.util.SettingsCache.OnChangeListener;
 import com.android.launcher3.util.SimpleBroadcastReceiver;
+import com.android.quickstep.dagger.QuickstepBaseAppComponent;
 
 import java.util.ArrayList;
 import java.util.List;
 
+import javax.inject.Inject;
+
 /**
  * Extension of {@link ClockEventDelegate} to support async event registration
  */
+@LauncherAppSingleton
 public class AsyncClockEventDelegate extends ClockEventDelegate
         implements OnChangeListener, SafeCloseable {
 
-    public static final MainThreadInitializedObject<AsyncClockEventDelegate> INSTANCE =
-            new MainThreadInitializedObject<>(AsyncClockEventDelegate::new);
+    public static final DaggerSingletonObject<AsyncClockEventDelegate> INSTANCE =
+            new DaggerSingletonObject<>(QuickstepBaseAppComponent::getAsyncClockEventDelegate);
 
     private final Context mContext;
+    private final SettingsCache mSettingsCache;
     private final SimpleBroadcastReceiver mReceiver =
             new SimpleBroadcastReceiver(UI_HELPER_EXECUTOR, this::onClockEventReceived);
 
@@ -61,10 +69,15 @@
     private boolean mFormatRegistered = false;
     private boolean mDestroyed = false;
 
-    private AsyncClockEventDelegate(Context context) {
+    @Inject
+    AsyncClockEventDelegate(@ApplicationContext Context context,
+            DaggerSingletonTracker tracker,
+            SettingsCache settingsCache) {
         super(context);
         mContext = context;
+        mSettingsCache = settingsCache;
         mReceiver.register(mContext, ACTION_TIME_CHANGED, ACTION_TIMEZONE_CHANGED);
+        tracker.addCloseable(this);
     }
 
     @Override
@@ -88,7 +101,7 @@
         }
         synchronized (mFormatObservers) {
             if (!mFormatRegistered && !mDestroyed) {
-                SettingsCache.INSTANCE.get(mContext).register(mFormatUri, this);
+                mSettingsCache.register(mFormatUri, this);
                 mFormatRegistered = true;
             }
             mFormatObservers.add(observer);
@@ -124,7 +137,7 @@
     @Override
     public void close() {
         mDestroyed = true;
-        SettingsCache.INSTANCE.get(mContext).unregister(mFormatUri, this);
+        mSettingsCache.unregister(mFormatUri, this);
         mReceiver.unregisterReceiverSafely(mContext);
     }
 }
diff --git a/quickstep/src/com/android/quickstep/util/ActivityInitListener.java b/quickstep/src/com/android/quickstep/util/ContextInitListener.java
similarity index 64%
rename from quickstep/src/com/android/quickstep/util/ActivityInitListener.java
rename to quickstep/src/com/android/quickstep/util/ContextInitListener.java
index 5efbb40..49f1463 100644
--- a/quickstep/src/com/android/quickstep/util/ActivityInitListener.java
+++ b/quickstep/src/com/android/quickstep/util/ContextInitListener.java
@@ -15,17 +15,17 @@
  */
 package com.android.quickstep.util;
 
-import com.android.launcher3.BaseActivity;
-import com.android.launcher3.util.ActivityTracker;
-import com.android.launcher3.util.ActivityTracker.SchedulerCallback;
+import com.android.launcher3.util.ContextTracker;
+import com.android.launcher3.util.ContextTracker.SchedulerCallback;
+import com.android.launcher3.views.ActivityContext;
 
 import java.util.function.BiPredicate;
 
-public class ActivityInitListener<T extends BaseActivity> implements
-        SchedulerCallback<T> {
+public class ContextInitListener<CONTEXT extends ActivityContext> implements
+        SchedulerCallback<CONTEXT> {
 
-    private BiPredicate<T, Boolean> mOnInitListener;
-    private final ActivityTracker<T> mActivityTracker;
+    private BiPredicate<CONTEXT, Boolean> mOnInitListener;
+    private final ContextTracker<CONTEXT> mContextTracker;
 
     private boolean mIsRegistered = false;
 
@@ -34,23 +34,23 @@
      *                       return true to continue receiving callbacks (ie. for if the activity is
      *                       recreated).
      */
-    public ActivityInitListener(BiPredicate<T, Boolean> onInitListener,
-            ActivityTracker<T> tracker) {
+    public ContextInitListener(BiPredicate<CONTEXT, Boolean> onInitListener,
+            ContextTracker<CONTEXT> tracker) {
         mOnInitListener = onInitListener;
-        mActivityTracker = tracker;
+        mContextTracker = tracker;
     }
 
     @Override
-    public final boolean init(T activity, boolean alreadyOnHome) {
+    public final boolean init(CONTEXT activity, boolean isHomeStarted) {
         if (!mIsRegistered) {
             // Don't receive any more updates
             return false;
         }
-        return handleInit(activity, alreadyOnHome);
+        return handleInit(activity, isHomeStarted);
     }
 
-    protected boolean handleInit(T activity, boolean alreadyOnHome) {
-        return mOnInitListener.test(activity, alreadyOnHome);
+    protected boolean handleInit(CONTEXT activity, boolean isHomeStarted) {
+        return mOnInitListener.test(activity, isHomeStarted);
     }
 
     /**
@@ -59,14 +59,14 @@
      */
     public void register(String reasonString) {
         mIsRegistered = true;
-        mActivityTracker.registerCallback(this, reasonString);
+        mContextTracker.registerCallback(this, reasonString);
     }
 
     /**
      * After calling this, we won't {@link #init} even when the activity is ready.
      */
     public void unregister(String reasonString) {
-        mActivityTracker.unregisterCallback(this, reasonString);
+        mContextTracker.unregisterCallback(this, reasonString);
         mIsRegistered = false;
         mOnInitListener = null;
     }
diff --git a/quickstep/src/com/android/quickstep/util/ContextualSearchHapticManager.kt b/quickstep/src/com/android/quickstep/util/ContextualSearchHapticManager.kt
new file mode 100644
index 0000000..286b77a
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/ContextualSearchHapticManager.kt
@@ -0,0 +1,105 @@
+/*
+ * 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.Context
+import android.os.VibrationEffect
+import android.os.VibrationEffect.Composition
+import android.os.Vibrator
+import com.android.launcher3.dagger.ApplicationContext
+import com.android.launcher3.util.MainThreadInitializedObject
+import com.android.launcher3.util.SafeCloseable
+import com.android.launcher3.util.VibratorWrapper
+import com.android.quickstep.DeviceConfigWrapper.Companion.get
+import kotlin.math.pow
+
+/** Manages haptics relating to Contextual Search invocations. */
+class ContextualSearchHapticManager
+internal constructor(@ApplicationContext private val context: Context) : SafeCloseable {
+
+    private var searchEffect = createSearchEffect()
+    private var contextualSearchStateManager = ContextualSearchStateManager.INSTANCE[context]
+
+    private fun createSearchEffect() =
+        if (
+            context
+                .getSystemService(Vibrator::class.java)!!
+                .areAllPrimitivesSupported(Composition.PRIMITIVE_TICK)
+        ) {
+            VibrationEffect.startComposition()
+                .addPrimitive(Composition.PRIMITIVE_TICK, 1f)
+                .compose()
+        } else {
+            // fallback for devices without composition support
+            VibrationEffect.createPredefined(VibrationEffect.EFFECT_HEAVY_CLICK)
+        }
+
+    /** Indicates that search has been invoked. */
+    fun vibrateForSearch() {
+        searchEffect.let { VibratorWrapper.INSTANCE[context].vibrate(it) }
+    }
+
+    /** Indicates that search will be invoked if the current gesture is maintained. */
+    fun vibrateForSearchHint() {
+        val navbarConfig = get()
+        // Whether we should play the hint (ramp up) haptic
+        val shouldVibrate: Boolean =
+            if (
+                context
+                    .getSystemService(Vibrator::class.java)!!
+                    .areAllPrimitivesSupported(Composition.PRIMITIVE_LOW_TICK)
+            ) {
+                if (contextualSearchStateManager.shouldPlayHapticOverride.isPresent) {
+                    contextualSearchStateManager.shouldPlayHapticOverride.get()
+                } else {
+                    navbarConfig.enableSearchHapticHint
+                }
+            } else {
+                false
+            }
+
+        if (shouldVibrate) {
+            val startScale = navbarConfig.lpnhHapticHintStartScalePercent / 100f
+            val endScale = navbarConfig.lpnhHapticHintEndScalePercent / 100f
+            val scaleExponent = navbarConfig.lpnhHapticHintScaleExponent
+            val iterations = navbarConfig.lpnhHapticHintIterations
+            val delayMs = navbarConfig.lpnhHapticHintDelay
+            val composition = VibrationEffect.startComposition()
+            for (i in 0 until iterations) {
+                val t = i / (iterations - 1f)
+                val scale =
+                    ((1 - t) * startScale + t * endScale)
+                        .toDouble()
+                        .pow(scaleExponent.toDouble())
+                        .toFloat()
+                if (i == 0) {
+                    // Adds a delay before the ramp starts
+                    composition.addPrimitive(Composition.PRIMITIVE_LOW_TICK, scale, delayMs)
+                } else {
+                    composition.addPrimitive(Composition.PRIMITIVE_LOW_TICK, scale)
+                }
+            }
+            VibratorWrapper.INSTANCE[context].vibrate(composition.compose())
+        }
+    }
+
+    override fun close() {}
+
+    companion object {
+        @JvmField val INSTANCE = MainThreadInitializedObject { ContextualSearchHapticManager(it) }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/util/ContextualSearchInvoker.kt b/quickstep/src/com/android/quickstep/util/ContextualSearchInvoker.kt
new file mode 100644
index 0000000..bd454c0
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/ContextualSearchInvoker.kt
@@ -0,0 +1,221 @@
+/*
+ * 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.app.contextualsearch.ContextualSearchManager
+import android.app.contextualsearch.ContextualSearchManager.ENTRYPOINT_LONG_PRESS_HOME
+import android.app.contextualsearch.ContextualSearchManager.FEATURE_CONTEXTUAL_SEARCH
+import android.content.Context
+import android.util.Log
+import com.android.internal.app.AssistUtils
+import com.android.launcher3.R
+import com.android.launcher3.logging.StatsLogManager
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_ASSISTANT_FAILED_SERVICE_ERROR
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_ATTEMPTED_OVER_KEYGUARD
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_ATTEMPTED_OVER_NOTIFICATION_SHADE
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_ATTEMPTED_SPLITSCREEN
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_FAILED_NOT_AVAILABLE
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_FAILED_SETTING_DISABLED
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_SUCCESSFUL_HOME
+import com.android.launcher3.util.ResourceBasedOverride
+import com.android.quickstep.DeviceConfigWrapper
+import com.android.quickstep.SystemUiProxy
+import com.android.quickstep.TopTaskTracker
+import com.android.systemui.shared.system.QuickStepContract
+
+/** Handles invocations and checks for Contextual Search. */
+open class ContextualSearchInvoker
+internal constructor(
+    protected val context: Context,
+    private val contextualSearchStateManager: ContextualSearchStateManager,
+    private val topTaskTracker: TopTaskTracker,
+    private val systemUiProxy: SystemUiProxy,
+    protected val statsLogManager: StatsLogManager,
+    private val contextualSearchHapticManager: ContextualSearchHapticManager,
+    private val contextualSearchManager: ContextualSearchManager?,
+) : ResourceBasedOverride {
+    constructor(
+        context: Context
+    ) : this(
+        context,
+        ContextualSearchStateManager.INSTANCE[context],
+        TopTaskTracker.INSTANCE[context],
+        SystemUiProxy.INSTANCE[context],
+        StatsLogManager.newInstance(context),
+        ContextualSearchHapticManager.INSTANCE[context],
+        context.getSystemService(ContextualSearchManager::class.java),
+    )
+
+    /** @return Array of AssistUtils.INVOCATION_TYPE_* that we want to handle instead of SysUI. */
+    open fun getSysUiAssistOverrideInvocationTypes(): IntArray {
+        val overrideInvocationTypes = com.android.launcher3.util.IntArray()
+        if (context.packageManager.hasSystemFeature(FEATURE_CONTEXTUAL_SEARCH)) {
+            overrideInvocationTypes.add(AssistUtils.INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS)
+        }
+        return overrideInvocationTypes.toArray()
+    }
+
+    /**
+     * @return `true` if the override was handled, i.e. an assist surface was shown or the request
+     *   should be ignored. `false` means the caller should start assist another way.
+     */
+    fun tryStartAssistOverride(invocationType: Int): Boolean {
+        if (invocationType == AssistUtils.INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS) {
+            if (!context.packageManager.hasSystemFeature(FEATURE_CONTEXTUAL_SEARCH)) {
+                // When Contextual Search is disabled, fall back to Assistant.
+                return false
+            }
+
+            val success = show(ENTRYPOINT_LONG_PRESS_HOME)
+            if (success) {
+                val runningPackage =
+                    TopTaskTracker.INSTANCE[context].getCachedTopTask(
+                            /* filterOnlyVisibleRecents */ true
+                        )
+                        .getPackageName()
+                statsLogManager
+                    .logger()
+                    .withPackageName(runningPackage)
+                    .log(LAUNCHER_LAUNCH_OMNI_SUCCESSFUL_HOME)
+            }
+
+            // Regardless of success, do not fall back to other assistant.
+            return true
+        }
+        return false
+    }
+
+    /**
+     * Invoke Contextual Search via ContextualSearchService if availability checks are successful
+     *
+     * @param entryPoint one of the ENTRY_POINT_* constants defined in this class
+     * @return true if invocation was successful, false otherwise
+     */
+    fun show(entryPoint: Int): Boolean {
+        return if (!runContextualSearchInvocationChecksAndLogFailures()) false
+        else invokeContextualSearchUnchecked(entryPoint)
+    }
+
+    /**
+     * Run availability checks and log errors to WW. If successful the caller is expected to call
+     * {@link invokeContextualSearchUnchecked}
+     *
+     * @return true if availability checks were successful, false otherwise.
+     */
+    fun runContextualSearchInvocationChecksAndLogFailures(): Boolean {
+        if (
+            contextualSearchManager == null ||
+                !context.packageManager.hasSystemFeature(FEATURE_CONTEXTUAL_SEARCH)
+        ) {
+            Log.i(TAG, "Contextual Search invocation failed: no ContextualSearchManager")
+            statsLogManager.logger().log(LAUNCHER_LAUNCH_ASSISTANT_FAILED_SERVICE_ERROR)
+            return false
+        }
+        if (!contextualSearchStateManager.isContextualSearchSettingEnabled) {
+            Log.i(TAG, "Contextual Search invocation failed: setting disabled")
+            statsLogManager.logger().log(LAUNCHER_LAUNCH_OMNI_FAILED_SETTING_DISABLED)
+            return false
+        }
+        if (isNotificationShadeShowing()) {
+            Log.i(TAG, "Contextual Search invocation failed: notification shade")
+            statsLogManager.logger().log(LAUNCHER_LAUNCH_OMNI_ATTEMPTED_OVER_NOTIFICATION_SHADE)
+            return false
+        }
+        if (isKeyguardShowing()) {
+            Log.i(TAG, "Contextual Search invocation attempted: keyguard")
+            statsLogManager.logger().log(LAUNCHER_LAUNCH_OMNI_ATTEMPTED_OVER_KEYGUARD)
+            if (!contextualSearchStateManager.isInvocationAllowedOnKeyguard) {
+                Log.i(TAG, "Contextual Search invocation failed: keyguard not allowed")
+                return false
+            } else if (!contextualSearchStateManager.supportsShowWhenLocked()) {
+                Log.i(TAG, "Contextual Search invocation failed: AGA doesn't support keyguard")
+                return false
+            }
+        }
+        if (isInSplitscreen()) {
+            Log.i(TAG, "Contextual Search invocation attempted: splitscreen")
+            statsLogManager.logger().log(LAUNCHER_LAUNCH_OMNI_ATTEMPTED_SPLITSCREEN)
+            if (!contextualSearchStateManager.isInvocationAllowedInSplitscreen) {
+                Log.i(TAG, "Contextual Search invocation failed: splitscreen not allowed")
+                return false
+            }
+        }
+        if (!contextualSearchStateManager.isContextualSearchIntentAvailable) {
+            Log.i(TAG, "Contextual Search invocation failed: no matching CSS intent filter")
+            statsLogManager.logger().log(LAUNCHER_LAUNCH_OMNI_FAILED_NOT_AVAILABLE)
+            return false
+        }
+
+        return true
+    }
+
+    /**
+     * Invoke Contextual Search via ContextualSearchService and do haptic
+     *
+     * @param entryPoint Entry point identifier, passed to ContextualSearchService.
+     * @return true if invocation was successful, false otherwise
+     */
+    fun invokeContextualSearchUncheckedWithHaptic(entryPoint: Int): Boolean {
+        return invokeContextualSearchUnchecked(entryPoint, withHaptic = true)
+    }
+
+    private fun invokeContextualSearchUnchecked(
+        entryPoint: Int,
+        withHaptic: Boolean = false,
+    ): Boolean {
+        if (withHaptic && DeviceConfigWrapper.get().enableSearchHapticCommit) {
+            contextualSearchHapticManager.vibrateForSearch()
+        }
+        if (contextualSearchManager == null) {
+            return false
+        }
+        contextualSearchManager.startContextualSearch(entryPoint)
+        return true
+    }
+
+    private fun isInSplitscreen(): Boolean {
+        return topTaskTracker.getRunningSplitTaskIds().isNotEmpty()
+    }
+
+    private fun isNotificationShadeShowing(): Boolean {
+        return systemUiProxy.lastSystemUiStateFlags and SHADE_EXPANDED_SYSUI_FLAGS != 0L
+    }
+
+    private fun isKeyguardShowing(): Boolean {
+        return systemUiProxy.lastSystemUiStateFlags and KEYGUARD_SHOWING_SYSUI_FLAGS != 0L
+    }
+
+    companion object {
+        private const val TAG = "ContextualSearchInvoker"
+        const val SHADE_EXPANDED_SYSUI_FLAGS =
+            QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED or
+                QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED
+        const val KEYGUARD_SHOWING_SYSUI_FLAGS =
+            (QuickStepContract.SYSUI_STATE_BOUNCER_SHOWING or
+                QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING or
+                QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED)
+
+        @JvmStatic
+        fun newInstance(context: Context): ContextualSearchInvoker {
+            return ResourceBasedOverride.Overrides.getObject(
+                ContextualSearchInvoker::class.java,
+                context,
+                R.string.contextual_search_invoker_class,
+            )
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/util/ContextualSearchStateManager.java b/quickstep/src/com/android/quickstep/util/ContextualSearchStateManager.java
new file mode 100644
index 0000000..083f192
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/ContextualSearchStateManager.java
@@ -0,0 +1,288 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.util;
+
+import static android.app.contextualsearch.ContextualSearchManager.ACTION_LAUNCH_CONTEXTUAL_SEARCH;
+import static android.app.contextualsearch.ContextualSearchManager.ENTRYPOINT_SYSTEM_ACTION;
+import static android.app.contextualsearch.ContextualSearchManager.FEATURE_CONTEXTUAL_SEARCH;
+
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_SUCCESSFUL_SYSTEM_ACTION;
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+import static com.android.launcher3.util.MainThreadInitializedObject.forOverride;
+import static com.android.quickstep.util.SystemActionConstants.SYSTEM_ACTION_ID_SEARCH_SCREEN;
+
+import android.annotation.Nullable;
+import android.app.PendingIntent;
+import android.app.RemoteAction;
+import android.content.Context;
+import android.content.IIntentReceiver;
+import android.content.IIntentSender;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.provider.Settings;
+import android.util.Log;
+import android.view.accessibility.AccessibilityManager;
+
+import androidx.annotation.CallSuper;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.launcher3.R;
+import com.android.launcher3.logging.StatsLogManager;
+import com.android.launcher3.util.EventLogArray;
+import com.android.launcher3.util.MainThreadInitializedObject;
+import com.android.launcher3.util.ResourceBasedOverride;
+import com.android.launcher3.util.SafeCloseable;
+import com.android.launcher3.util.SettingsCache;
+import com.android.launcher3.util.SimpleBroadcastReceiver;
+import com.android.quickstep.DeviceConfigWrapper;
+import com.android.quickstep.SystemUiProxy;
+import com.android.quickstep.TopTaskTracker;
+
+import java.io.PrintWriter;
+import java.util.Optional;
+
+/** Long-lived class to manage Contextual Search states like the user setting and availability. */
+public class ContextualSearchStateManager implements ResourceBasedOverride, SafeCloseable {
+
+    public static final MainThreadInitializedObject<ContextualSearchStateManager> INSTANCE =
+            forOverride(ContextualSearchStateManager.class,
+                    R.string.contextual_search_state_manager_class);
+
+    private static final String TAG = "ContextualSearchStMgr";
+    private static final int MAX_DEBUG_EVENT_SIZE = 20;
+    private static final Uri SEARCH_ALL_ENTRYPOINTS_ENABLED_URI =
+            Settings.Secure.getUriFor(Settings.Secure.SEARCH_ALL_ENTRYPOINTS_ENABLED);
+
+    private final Runnable mSysUiStateChangeListener = this::updateOverridesToSysUi;
+    private final SimpleBroadcastReceiver mContextualSearchPackageReceiver =
+            new SimpleBroadcastReceiver(UI_HELPER_EXECUTOR, (unused) -> requestUpdateProperties());
+    private final SettingsCache.OnChangeListener mContextualSearchSettingChangedListener =
+            this::onContextualSearchSettingChanged;
+    protected final EventLogArray mEventLogArray = new EventLogArray(TAG, MAX_DEBUG_EVENT_SIZE);
+
+    @Nullable private SettingsCache mSettingsCache;
+    // Cached value whether the ContextualSearch intent filter matched any enabled components.
+    private boolean mIsContextualSearchIntentAvailable;
+    private boolean mIsContextualSearchSettingEnabled;
+
+    protected Context mContext;
+    protected String mContextualSearchPackage;
+
+    public ContextualSearchStateManager() {}
+
+    public ContextualSearchStateManager(Context context) {
+        mContext = context;
+        mContextualSearchPackage = mContext.getResources().getString(
+                com.android.internal.R.string.config_defaultContextualSearchPackageName);
+
+        if (areAllContextualSearchFlagsDisabled()
+                || !context.getPackageManager().hasSystemFeature(FEATURE_CONTEXTUAL_SEARCH)) {
+            // If we had previously registered a SystemAction which is no longer valid, we need to
+            // unregister it here.
+            unregisterSearchScreenSystemAction();
+            // Don't listen for stuff we aren't gonna use.
+            return;
+        }
+
+        requestUpdateProperties();
+        registerSearchScreenSystemAction();
+        mContextualSearchPackageReceiver.registerPkgActions(
+                context, mContextualSearchPackage, Intent.ACTION_PACKAGE_ADDED,
+                Intent.ACTION_PACKAGE_CHANGED, Intent.ACTION_PACKAGE_REMOVED);
+
+        mSettingsCache = SettingsCache.INSTANCE.get(context);
+        mSettingsCache.register(SEARCH_ALL_ENTRYPOINTS_ENABLED_URI,
+                mContextualSearchSettingChangedListener);
+        onContextualSearchSettingChanged(
+                mSettingsCache.getValue(SEARCH_ALL_ENTRYPOINTS_ENABLED_URI));
+        SystemUiProxy.INSTANCE.get(mContext).addOnStateChangeListener(mSysUiStateChangeListener);
+    }
+
+    /** Return {@code true} if the Settings toggle is enabled. */
+    public final boolean isContextualSearchSettingEnabled() {
+        return mIsContextualSearchSettingEnabled;
+    }
+
+    private void onContextualSearchSettingChanged(boolean isEnabled) {
+        mIsContextualSearchSettingEnabled = isEnabled;
+    }
+
+    /** Whether search supports showing on the lockscreen. */
+    protected boolean supportsShowWhenLocked() {
+        return false;
+    }
+
+    /** Whether ContextualSearchService invocation path is available. */
+    @VisibleForTesting
+    protected final boolean isContextualSearchIntentAvailable() {
+        return mIsContextualSearchIntentAvailable;
+    }
+
+    /** Get the Launcher overridden long press nav handle duration to trigger Assistant. */
+    public Optional<Long> getLPNHDurationMillis() {
+        return Optional.empty();
+    }
+
+    /**
+     * Get the Launcher overridden long press nav handle touch slop multiplier to trigger Assistant.
+     */
+    public Optional<Float> getLPNHCustomSlopMultiplier() {
+        return Optional.empty();
+    }
+
+    /** Get the Launcher overridden long press home duration to trigger Assistant. */
+    public Optional<Long> getLPHDurationMillis() {
+        return Optional.empty();
+    }
+
+    /** Get the Launcher overridden long press home touch slop multiplier to trigger Assistant. */
+    public Optional<Float> getLPHCustomSlopMultiplier() {
+        return Optional.empty();
+    }
+
+    /** Get the long press duration data source. */
+    public int getDurationDataSource() {
+        return 0;
+    }
+
+    /** Get the long press touch slop multiplier data source. */
+    public int getSlopDataSource() {
+        return 0;
+    }
+
+    /**
+     * Get the User group based on the behavior to trigger Assistant.
+     */
+    public Optional<Integer> getLPUserGroup() {
+        return Optional.empty();
+    }
+
+    /** Get the haptic bit overridden by AGSA. */
+    public Optional<Boolean> getShouldPlayHapticOverride() {
+        return Optional.empty();
+    }
+
+    protected boolean isInvocationAllowedOnKeyguard() {
+        return false;
+    }
+
+    protected boolean isInvocationAllowedInSplitscreen() {
+        return true;
+    }
+
+    @CallSuper
+    protected boolean areAllContextualSearchFlagsDisabled() {
+        return !DeviceConfigWrapper.get().getEnableLongPressNavHandle();
+    }
+
+    @CallSuper
+    protected void requestUpdateProperties() {
+        UI_HELPER_EXECUTOR.execute(() -> {
+            // Check that Contextual Search intent filters are enabled.
+            Intent csIntent = new Intent(ACTION_LAUNCH_CONTEXTUAL_SEARCH).setPackage(
+                    mContextualSearchPackage);
+            mIsContextualSearchIntentAvailable =
+                    !mContext.getPackageManager().queryIntentActivities(csIntent,
+                            PackageManager.MATCH_DIRECT_BOOT_AWARE
+                                    | PackageManager.MATCH_DIRECT_BOOT_UNAWARE).isEmpty();
+
+            addEventLog("Updated isContextualSearchIntentAvailable",
+                    mIsContextualSearchIntentAvailable);
+        });
+    }
+
+    protected final void updateOverridesToSysUi() {
+        // LPH commit haptic is always enabled
+        SystemUiProxy.INSTANCE.get(mContext).setOverrideHomeButtonLongPress(
+                getLPHDurationMillis().orElse(0L), getLPHCustomSlopMultiplier().orElse(0f), true);
+        Log.i(TAG, "Sent LPH override to sysui: " + getLPHDurationMillis().orElse(0L) + ";"
+                + getLPHCustomSlopMultiplier().orElse(0f));
+    }
+
+    private void registerSearchScreenSystemAction() {
+        PendingIntent searchScreenPendingIntent = new PendingIntent(new IIntentSender.Stub() {
+            @Override
+            public void send(int i, Intent intent, String s, IBinder iBinder,
+                    IIntentReceiver iIntentReceiver, String s1, Bundle bundle)
+                    throws RemoteException {
+                // Delayed slightly to minimize chance of capturing the System Actions dialog.
+                UI_HELPER_EXECUTOR.getHandler().postDelayed(
+                        () -> {
+                            boolean contextualSearchInvoked =
+                                    ContextualSearchInvoker.newInstance(mContext).show(
+                                            ENTRYPOINT_SYSTEM_ACTION);
+                            if (contextualSearchInvoked) {
+                                String runningPackage =
+                                        TopTaskTracker.INSTANCE.get(mContext).getCachedTopTask(
+                                                /* filterOnlyVisibleRecents */
+                                                true).getPackageName();
+                                StatsLogManager.newInstance(mContext).logger()
+                                        .withPackageName(runningPackage)
+                                        .log(LAUNCHER_LAUNCH_OMNI_SUCCESSFUL_SYSTEM_ACTION);
+                            }
+                        }, 200);
+            }
+        });
+
+        mContext.getSystemService(AccessibilityManager.class).registerSystemAction(new RemoteAction(
+                        Icon.createWithResource(mContext, R.drawable.ic_allapps_search),
+                        mContext.getString(R.string.search_gesture_feature_title),
+                        mContext.getString(R.string.search_gesture_feature_title),
+                        searchScreenPendingIntent),
+                SYSTEM_ACTION_ID_SEARCH_SCREEN);
+    }
+
+    private void unregisterSearchScreenSystemAction() {
+        mContext.getSystemService(AccessibilityManager.class).unregisterSystemAction(
+                SYSTEM_ACTION_ID_SEARCH_SCREEN);
+    }
+
+    /** Dump states. */
+    public final void dump(String prefix, PrintWriter writer) {
+        synchronized (mEventLogArray) {
+            mEventLogArray.dump(prefix, writer);
+        }
+    }
+
+    @Override
+    public void close() {
+        mContextualSearchPackageReceiver.unregisterReceiverSafely(mContext);
+        unregisterSearchScreenSystemAction();
+
+        if (mSettingsCache != null) {
+            mSettingsCache.unregister(SEARCH_ALL_ENTRYPOINTS_ENABLED_URI,
+                    mContextualSearchSettingChangedListener);
+        }
+        SystemUiProxy.INSTANCE.get(mContext).removeOnStateChangeListener(mSysUiStateChangeListener);
+    }
+
+    protected final void addEventLog(String event) {
+        synchronized (mEventLogArray) {
+            mEventLogArray.addLog(event);
+        }
+    }
+
+    protected final void addEventLog(String event, boolean extras) {
+        synchronized (mEventLogArray) {
+            mEventLogArray.addLog(event, extras);
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/util/DesktopTask.java b/quickstep/src/com/android/quickstep/util/DesktopTask.java
index a727aa2..fc4fc4d 100644
--- a/quickstep/src/com/android/quickstep/util/DesktopTask.java
+++ b/quickstep/src/com/android/quickstep/util/DesktopTask.java
@@ -50,6 +50,11 @@
 
     @Override
     public boolean hasMultipleTasks() {
+        return tasks.size() > 1;
+    }
+
+    @Override
+    public boolean supportsMultipleTasks() {
         return true;
     }
 
diff --git a/quickstep/src/com/android/quickstep/util/GroupTask.java b/quickstep/src/com/android/quickstep/util/GroupTask.java
index fba08a9..7aeeb2f 100644
--- a/quickstep/src/com/android/quickstep/util/GroupTask.java
+++ b/quickstep/src/com/android/quickstep/util/GroupTask.java
@@ -66,6 +66,13 @@
     }
 
     /**
+     * Returns whether this task supports multiple tasks or not.
+     */
+    public boolean supportsMultipleTasks() {
+        return taskViewType == TaskViewType.GROUPED;
+    }
+
+    /**
      * Returns a List of all the Tasks in this GroupTask
      */
     public List<Task> getTasks() {
diff --git a/quickstep/src/com/android/quickstep/util/ImageActionUtils.java b/quickstep/src/com/android/quickstep/util/ImageActionUtils.java
index 3a1c99b..64e46d8 100644
--- a/quickstep/src/com/android/quickstep/util/ImageActionUtils.java
+++ b/quickstep/src/com/android/quickstep/util/ImageActionUtils.java
@@ -85,6 +85,7 @@
                 .setBitmap(screenshot)
                 .setBoundsOnScreen(screenshotBounds)
                 .setInsets(visibleInsets)
+                .setDisplayId(task.displayId)
                 .build();
         systemUiProxy.takeScreenshot(request);
     }
diff --git a/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java b/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java
index 15081da..623bc53 100644
--- a/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java
+++ b/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java
@@ -39,6 +39,7 @@
     // The percentage of the previous speed that determines whether this is a rapid deceleration.
     // The bigger this number, the easier it is to trigger the first pause.
     private static final float RAPID_DECELERATION_FACTOR = 0.6f;
+    private static final float RAPID_DECELERATION_FACTOR_TRACKPAD = 0.85f;
 
     /** If no motion is added for this amount of time, assume the motion has paused. */
     private static final long FORCE_PAUSE_TIMEOUT = 300;
@@ -57,6 +58,7 @@
     private final float mSpeedVerySlow;
     private final float mSpeedSlow;
     private final float mSpeedSomewhatFast;
+    private final float mSpeedTrackpadSomewhatFast;
     private final float mSpeedFast;
     private final Alarm mForcePauseTimeout;
     private final boolean mMakePauseHarderToTrigger;
@@ -95,13 +97,13 @@
         mSpeedVerySlow = res.getDimension(R.dimen.motion_pause_detector_speed_very_slow);
         mSpeedSlow = res.getDimension(R.dimen.motion_pause_detector_speed_slow);
         mSpeedSomewhatFast = res.getDimension(R.dimen.motion_pause_detector_speed_somewhat_fast);
+        mSpeedTrackpadSomewhatFast = res.getDimension(
+                R.dimen.motion_pause_detector_speed_trackpad_somewhat_fast);
         mSpeedFast = res.getDimension(R.dimen.motion_pause_detector_speed_fast);
         mForcePauseTimeout = new Alarm();
         mForcePauseTimeout.setOnAlarmListener(alarm -> {
-            ActiveGestureLog.CompoundString log =
-                    new ActiveGestureLog.CompoundString("Force pause timeout after ")
-                            .append(alarm.getLastSetTimeout())
-                            .append("ms");
+            ActiveGestureLog.CompoundString log = new ActiveGestureLog.CompoundString(
+                    "Force pause timeout after %dms", alarm.getLastSetTimeout());
             addLogs(log);
             updatePaused(true /* isPaused */, log);
         });
@@ -124,9 +126,8 @@
      * @param disallowPause If true, we will not detect any pauses until this is set to false again.
      */
     public void setDisallowPause(boolean disallowPause) {
-        ActiveGestureLog.CompoundString log =
-                new ActiveGestureLog.CompoundString("Set disallowPause=")
-                        .append(disallowPause);
+        ActiveGestureLog.CompoundString log = new ActiveGestureLog.CompoundString(
+                "Set disallowPause=%b", disallowPause);
         if (mDisallowPause != disallowPause) {
             addLogs(log);
         }
@@ -186,10 +187,12 @@
                     // takes too long, so also check for a rapid deceleration.
                     boolean isRapidDeceleration =
                             speed < previousSpeed * getRapidDecelerationFactor();
-                    isPaused = isRapidDeceleration && speed < mSpeedSomewhatFast;
+                    boolean notSuperFast = speed < mSpeedSomewhatFast
+                            || (mIsTrackpadGesture && speed < mSpeedTrackpadSomewhatFast);
+                    isPaused = isRapidDeceleration && notSuperFast;
                     isPausedReason = new ActiveGestureLog.CompoundString(
-                            "Didn't have back to back slow speeds, checking for rapid ")
-                            .append(" deceleration on first pause only");
+                            "Didn't have back to back slow speeds, checking for rapid "
+                                    + " deceleration on first pause only");
                 }
                 if (mMakePauseHarderToTrigger) {
                     if (speed < mSpeedSlow) {
@@ -198,8 +201,8 @@
                         }
                         isPaused = time - mSlowStartTime >= HARDER_TRIGGER_TIMEOUT;
                         isPausedReason = new ActiveGestureLog.CompoundString(
-                                "Maintained slow speed for sufficient duration when making")
-                                .append(" pause harder to trigger");
+                                "Maintained slow speed for sufficient duration when making"
+                                        + " pause harder to trigger");
                     } else {
                         mSlowStartTime = 0;
                         isPaused = false;
@@ -215,17 +218,14 @@
     private void updatePaused(boolean isPaused, ActiveGestureLog.CompoundString reason) {
         if (mDisallowPause) {
             reason = new ActiveGestureLog.CompoundString(
-                    "Disallow pause; otherwise, would have been ")
-                    .append(isPaused)
-                    .append(" due to reason:")
+                    "Disallow pause; otherwise, would have been %b due to reason: ", isPaused)
                     .append(reason);
             isPaused = false;
         }
         if (mIsPaused != isPaused) {
             mIsPaused = isPaused;
-            addLogs(new ActiveGestureLog.CompoundString("onMotionPauseChanged triggered; paused=")
-                    .append(mIsPaused)
-                    .append(", reason=")
+            addLogs(new ActiveGestureLog.CompoundString(
+                    "onMotionPauseChanged triggered; paused=%b, reason=", mIsPaused)
                     .append(reason));
             boolean isFirstDetectedPause = !mHasEverBeenPaused && mIsPaused;
             if (mIsPaused) {
@@ -245,14 +245,13 @@
         }
     }
 
-    private void addLogs(ActiveGestureLog.CompoundString compoundString) {
-        ActiveGestureLog.CompoundString logString =
-                new ActiveGestureLog.CompoundString("MotionPauseDetector: ")
-                        .append(compoundString);
+    private void addLogs(ActiveGestureLog.CompoundString event) {
         if (Utilities.isRunningInTestHarness()) {
-            Log.d(TAG, logString.toString());
+            Log.d(TAG, new ActiveGestureLog.CompoundString("MotionPauseDetector: ")
+                    .append(event)
+                    .toString());
         }
-        ActiveGestureLog.INSTANCE.addLog(logString);
+        ActiveGestureProtoLogProxy.logMotionPauseDetectorEvent(event);
     }
 
     public void clear() {
@@ -272,7 +271,8 @@
     private float getRapidDecelerationFactor() {
         return mIsTrackpadGesture ? Float.parseFloat(
                 Utilities.getSystemProperty("trackpad_in_app_swipe_up_deceleration_factor",
-                        String.valueOf(RAPID_DECELERATION_FACTOR))) : RAPID_DECELERATION_FACTOR;
+                        String.valueOf(RAPID_DECELERATION_FACTOR_TRACKPAD)))
+                : RAPID_DECELERATION_FACTOR;
     }
 
     public interface OnMotionPauseListener {
diff --git a/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java b/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java
index 9335e7e..a5be89a 100644
--- a/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java
+++ b/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java
@@ -30,7 +30,6 @@
 import static java.lang.annotation.RetentionPolicy.SOURCE;
 
 import android.content.Context;
-import android.content.SharedPreferences;
 import android.graphics.Matrix;
 import android.graphics.Point;
 import android.graphics.PointF;
@@ -45,6 +44,7 @@
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.LauncherPrefChangeListener;
 import com.android.launcher3.LauncherPrefs;
 import com.android.launcher3.testing.shared.TestProtocol;
 import com.android.launcher3.touch.PagedOrientationHandler;
@@ -66,8 +66,7 @@
  * This class has initial default state assuming the device and foreground app have
  * no ({@link Surface#ROTATION_0} rotation.
  */
-public class RecentsOrientedState implements
-        SharedPreferences.OnSharedPreferenceChangeListener {
+public class RecentsOrientedState implements LauncherPrefChangeListener {
 
     private static final String TAG = "RecentsOrientedState";
     private static final boolean DEBUG = false;
@@ -283,7 +282,7 @@
     }
 
     @Override
-    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String s) {
+    public void onPrefChanged(String s) {
         if (LauncherPrefs.ALLOW_ROTATION.getSharedPrefKey().equals(s)) {
             updateHomeRotationSetting();
         }
diff --git a/quickstep/src/com/android/quickstep/util/RecentsViewUtils.kt b/quickstep/src/com/android/quickstep/util/RecentsViewUtils.kt
index be1af64..c3b072d 100644
--- a/quickstep/src/com/android/quickstep/util/RecentsViewUtils.kt
+++ b/quickstep/src/com/android/quickstep/util/RecentsViewUtils.kt
@@ -66,13 +66,28 @@
     fun getLargeTaskViewIds(taskViews: Iterable<TaskView>): List<Int> =
         taskViews.filter { it.isLargeTile }.map { it.taskViewId }
 
+    /** Counts [TaskView]s that are large tiles. */
+    fun getLargeTileCount(taskViews: Iterable<TaskView>): Int = taskViews.count { it.isLargeTile }
+
     /**
      * Returns the first TaskView that should be displayed as a large tile.
      *
      * @param taskViews List of [TaskView]s
+     * @param splitSelectActive current split state
      */
-    fun getFirstLargeTaskView(taskViews: Iterable<TaskView>): TaskView? =
-        taskViews.firstOrNull { it.isLargeTile }
+    fun getFirstLargeTaskView(
+        taskViews: MutableIterable<TaskView>,
+        splitSelectActive: Boolean,
+    ): TaskView? =
+        taskViews.firstOrNull { it.isLargeTile && !(splitSelectActive && it is DesktopTaskView) }
+
+    /**
+     * Returns the first TaskView that is not large
+     *
+     * @param taskViews List of [TaskView]s
+     */
+    fun getFirstSmallTaskView(taskViews: MutableIterable<TaskView>): TaskView? =
+        taskViews.firstOrNull { !it.isLargeTile }
 
     /** Returns the last TaskView that should be displayed as a large tile. */
     fun getLastLargeTaskView(taskViews: Iterable<TaskView>): TaskView? =
@@ -80,24 +95,30 @@
 
     /** Returns the first [TaskView], with some tasks possibly hidden in the carousel. */
     fun getFirstTaskViewInCarousel(
-        nonRunningTaskCategoryHidden: Boolean,
+        nonRunningTaskCarouselHidden: Boolean,
         taskViews: Iterable<TaskView>,
         runningTaskView: TaskView?,
     ): TaskView? =
         taskViews.firstOrNull {
-            it.isVisibleInCarousel(runningTaskView, nonRunningTaskCategoryHidden)
+            it.isVisibleInCarousel(runningTaskView, nonRunningTaskCarouselHidden)
         }
 
     /** Returns the last [TaskView], with some tasks possibly hidden in the carousel. */
     fun getLastTaskViewInCarousel(
-        nonRunningTaskCategoryHidden: Boolean,
+        nonRunningTaskCarouselHidden: Boolean,
         taskViews: Iterable<TaskView>,
         runningTaskView: TaskView?,
     ): TaskView? =
         taskViews.lastOrNull {
-            it.isVisibleInCarousel(runningTaskView, nonRunningTaskCategoryHidden)
+            it.isVisibleInCarousel(runningTaskView, nonRunningTaskCarouselHidden)
         }
 
+    /** Returns if any small tasks are fully visible */
+    fun isAnySmallTaskFullyVisible(
+        taskViews: Iterable<TaskView>,
+        isTaskViewFullyVisible: (TaskView) -> Boolean,
+    ): Boolean = taskViews.any { !it.isLargeTile && isTaskViewFullyVisible(it) }
+
     /** Returns the current list of [TaskView] children. */
     fun getTaskViews(taskViewCount: Int, requireTaskViewAt: (Int) -> TaskView): Iterable<TaskView> =
         (0 until taskViewCount).map(requireTaskViewAt)
@@ -106,28 +127,33 @@
     fun applyAttachAlpha(
         taskViews: Iterable<TaskView>,
         runningTaskView: TaskView?,
-        runningTaskTileHidden: Boolean,
-        nonRunningTaskCategoryHidden: Boolean,
+        runningTaskAttachAlpha: Float,
+        nonRunningTaskCarouselHidden: Boolean,
     ) {
         taskViews.forEach { taskView ->
-            val isVisible =
-                if (taskView == runningTaskView) !runningTaskTileHidden
-                else taskView.isVisibleInCarousel(runningTaskView, nonRunningTaskCategoryHidden)
-            taskView.attachAlpha = if (isVisible) 1f else 0f
+            taskView.attachAlpha =
+                if (taskView == runningTaskView) {
+                    runningTaskAttachAlpha
+                } else {
+                    if (taskView.isVisibleInCarousel(runningTaskView, nonRunningTaskCarouselHidden))
+                        1f
+                    else 0f
+                }
         }
     }
 
-    private fun TaskView.isVisibleInCarousel(
+    fun TaskView.isVisibleInCarousel(
         runningTaskView: TaskView?,
-        nonRunningTaskCategoryHidden: Boolean,
+        nonRunningTaskCarouselHidden: Boolean,
     ): Boolean =
-        if (!nonRunningTaskCategoryHidden) true
-        else if (runningTaskView == null) true else getCategory() == runningTaskView.getCategory()
+        if (!nonRunningTaskCarouselHidden) true
+        else getCarouselType() == runningTaskView.getCarouselType()
 
-    private fun TaskView.getCategory(): TaskViewCategory =
-        if (this is DesktopTaskView) TaskViewCategory.DESKTOP else TaskViewCategory.FULL_SCREEN
+    /** Returns the carousel type of the TaskView, and default to fullscreen if it's null. */
+    private fun TaskView?.getCarouselType(): TaskViewCarousel =
+        if (this is DesktopTaskView) TaskViewCarousel.DESKTOP else TaskViewCarousel.FULL_SCREEN
 
-    private enum class TaskViewCategory {
+    private enum class TaskViewCarousel {
         FULL_SCREEN,
         DESKTOP,
     }
diff --git a/quickstep/src/com/android/quickstep/util/ScalingWorkspaceRevealAnim.kt b/quickstep/src/com/android/quickstep/util/ScalingWorkspaceRevealAnim.kt
index f547a7fb..f719bed 100644
--- a/quickstep/src/com/android/quickstep/util/ScalingWorkspaceRevealAnim.kt
+++ b/quickstep/src/com/android/quickstep/util/ScalingWorkspaceRevealAnim.kt
@@ -16,15 +16,20 @@
 
 package com.android.quickstep.util
 
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
 import android.animation.AnimatorSet
 import android.graphics.Matrix
 import android.graphics.Path
 import android.graphics.RectF
+import android.util.Log
 import android.view.View
 import android.view.animation.PathInterpolator
 import androidx.core.graphics.transform
+import com.android.app.animation.Animations
 import com.android.app.animation.Interpolators
 import com.android.app.animation.Interpolators.LINEAR
+import com.android.launcher3.Flags
 import com.android.launcher3.LauncherAnimUtils.HOTSEAT_SCALE_PROPERTY_FACTORY
 import com.android.launcher3.LauncherAnimUtils.SCALE_INDEX_WORKSPACE_STATE
 import com.android.launcher3.LauncherAnimUtils.WORKSPACE_SCALE_PROPERTY_FACTORY
@@ -39,14 +44,16 @@
 import com.android.launcher3.uioverrides.QuickstepLauncher
 import com.android.quickstep.views.RecentsView
 
+const val TAG = "ScalingWorkspaceRevealAnim"
+
 /**
  * Creates an animation where the workspace and hotseat fade in while revealing from the center of
  * the screen outwards radially. This is used in conjunction with the swipe up to home animation.
  */
 class ScalingWorkspaceRevealAnim(
-    launcher: QuickstepLauncher,
+    private val launcher: QuickstepLauncher,
     siblingAnimation: RectFSpringAnim?,
-    windowTargetRect: RectF?
+    windowTargetRect: RectF?,
 ) {
     companion object {
         private const val FADE_DURATION_MS = 200L
@@ -60,7 +67,8 @@
          * Custom interpolator for both the home and wallpaper scaling. Necessary because EMPHASIZED
          * is too aggressive, but EMPHASIZED_DECELERATE is too soft.
          */
-        private val SCALE_INTERPOLATOR =
+        @JvmField
+        val SCALE_INTERPOLATOR =
             PathInterpolator(
                 Path().apply {
                     moveTo(0f, 0f)
@@ -86,25 +94,40 @@
         launcher.workspace.stateTransitionAnimation.setScrim(
             PropertySetter.NO_ANIM_PROPERTY_SETTER,
             LauncherState.BACKGROUND_APP,
-            setupConfig
+            setupConfig,
         )
 
         val workspace = launcher.workspace
         val hotseat = launcher.hotseat
 
+        var fromSize =
+            if (Flags.coordinateWorkspaceScale()) {
+                // Interrupt the current animation, if any.
+                Animations.cancelOngoingAnimation(workspace)
+                Animations.cancelOngoingAnimation(hotseat)
+
+                if (workspace.scaleX != MAX_SIZE) {
+                    workspace.scaleX
+                } else {
+                    MIN_SIZE
+                }
+            } else {
+                MIN_SIZE
+            }
+
         // Scale the Workspace and Hotseat around the same pivot.
         workspace.setPivotToScaleWithSelf(hotseat)
         animation.addFloat(
             workspace,
             WORKSPACE_SCALE_PROPERTY_FACTORY[SCALE_INDEX_WORKSPACE_STATE],
-            MIN_SIZE,
+            fromSize,
             MAX_SIZE,
             SCALE_INTERPOLATOR,
         )
         animation.addFloat(
             hotseat,
             HOTSEAT_SCALE_PROPERTY_FACTORY[SCALE_INDEX_WORKSPACE_STATE],
-            MIN_SIZE,
+            fromSize,
             MAX_SIZE,
             SCALE_INTERPOLATOR,
         )
@@ -116,13 +139,13 @@
         animation.setViewAlpha(
             workspace,
             MAX_ALPHA,
-            Interpolators.clampToProgress(LINEAR, 0f, fadeClamp)
+            Interpolators.clampToProgress(LINEAR, 0f, fadeClamp),
         )
         hotseat.alpha = MIN_ALPHA
         animation.setViewAlpha(
             hotseat,
             MAX_ALPHA,
-            Interpolators.clampToProgress(LINEAR, 0f, fadeClamp)
+            Interpolators.clampToProgress(LINEAR, 0f, fadeClamp),
         )
 
         val transitionConfig = StateAnimationConfig()
@@ -137,7 +160,7 @@
         launcher.workspace.stateTransitionAnimation.setScrim(
             animation,
             LauncherState.NORMAL,
-            transitionConfig
+            transitionConfig,
         )
 
         // To avoid awkward jumps in icon position, we want the sibling animation to always be
@@ -164,7 +187,7 @@
                         1 / workspace.scaleX,
                         1 / workspace.scaleY,
                         transformed.centerX(),
-                        transformed.centerY()
+                        transformed.centerY(),
                     )
                 }
             )
@@ -179,10 +202,36 @@
         workspace.setLayerType(View.LAYER_TYPE_HARDWARE, null)
         hotseat.setLayerType(View.LAYER_TYPE_HARDWARE, null)
         animation.addListener(
+            object : AnimatorListenerAdapter() {
+                override fun onAnimationCancel(animation: Animator) {
+                    super.onAnimationCancel(animation)
+                    Log.d(TAG, "onAnimationCancel")
+                }
+
+                override fun onAnimationPause(animation: Animator) {
+                    super.onAnimationPause(animation)
+                    Log.d(TAG, "onAnimationPause")
+                }
+            }
+        )
+        animation.addListener(
             AnimatorListeners.forEndCallback(
                 Runnable {
+                    // The workspace might stay at a transparent state when the animation is
+                    // cancelled, and the alpha will not be recovered (this doesn't apply to scales
+                    // somehow). Resetting the alpha for the workspace here.
+                    workspace.alpha = 1.0F
+
                     workspace.setLayerType(View.LAYER_TYPE_NONE, null)
                     hotseat.setLayerType(View.LAYER_TYPE_NONE, null)
+
+                    if (Flags.coordinateWorkspaceScale()) {
+                        // Reset the cached animations.
+                        Animations.setOngoingAnimation(workspace, animation = null)
+                        Animations.setOngoingAnimation(hotseat, animation = null)
+                    }
+
+                    Log.d(TAG, "alpha of workspace at the end of animation: ${workspace.alpha}")
                 }
             )
         )
@@ -193,6 +242,14 @@
     }
 
     fun start() {
-        getAnimators().start()
+        val animators = getAnimators()
+        if (Flags.coordinateWorkspaceScale()) {
+            // Make sure to cache the current animation, so it can be properly interrupted.
+            // TODO(b/367591368): ideally these animations would be refactored to be controlled
+            //  centrally so each instances doesn't need to care about this coordination.
+            Animations.setOngoingAnimation(launcher.workspace, animators)
+            Animations.setOngoingAnimation(launcher.hotseat, animators)
+        }
+        animators.start()
     }
 }
diff --git a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
index 3449cf2..f708f4b 100644
--- a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
+++ b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
@@ -238,7 +238,7 @@
                 taskViewHeight,
             )
         val snapshotViewSize =
-            if (isPrimaryTaskSplitting) primarySnapshotViewSize else secondarySnapshotViewSize
+            if (isPrimaryTaskSplitting) secondarySnapshotViewSize else primarySnapshotViewSize
         if (deviceProfile.isLeftRightSplit) {
             // Center view first so scaling happens uniformly, alternatively we can move pivotX to 0
             val centerThumbnailTranslationX: Float = (taskViewWidth - snapshotViewSize.x) / 2f
diff --git a/quickstep/src/com/android/quickstep/util/SplitAnimationTimings.java b/quickstep/src/com/android/quickstep/util/SplitAnimationTimings.java
index b618546..90569b4 100644
--- a/quickstep/src/com/android/quickstep/util/SplitAnimationTimings.java
+++ b/quickstep/src/com/android/quickstep/util/SplitAnimationTimings.java
@@ -105,6 +105,10 @@
     default Interpolator getGridSlidePrimaryInterpolator() { return LINEAR; }
     default Interpolator getGridSlideSecondaryInterpolator() { return LINEAR; }
 
+    default Interpolator getDesktopTaskFadeInterpolator() {
+        return LINEAR;
+    }
+
     // Defaults for HomeToSplit
     default float getScrimFadeInStartOffset() { return 0; }
     default float getScrimFadeInEndOffset() { return 0; }
diff --git a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
index 1af12f1..d35a36a 100644
--- a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
@@ -35,7 +35,7 @@
 import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_TASK_SHORTCUT;
 import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_TASK_TASK;
 import static com.android.wm.shell.shared.split.SplitScreenConstants.KEY_EXTRA_WIDGET_INTENT;
-import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_50_50;
+import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -52,7 +52,6 @@
 import android.graphics.RectF;
 import android.graphics.drawable.Drawable;
 import android.os.Bundle;
-import android.os.Handler;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.os.UserHandle;
@@ -78,6 +77,7 @@
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.statehandlers.DepthController;
 import com.android.launcher3.statemanager.StateManager;
+import com.android.launcher3.taskbar.LauncherTaskbarUIController;
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.shared.TestProtocol;
 import com.android.launcher3.uioverrides.QuickstepLauncher;
@@ -116,7 +116,6 @@
     private static final String TAG = "SplitSelectStateCtor";
 
     private RecentsViewContainer mContainer;
-    private final Handler mHandler;
     private final RecentsModel mRecentTasksModel;
     @Nullable
     private Runnable mActivityBackCallback;
@@ -164,6 +163,8 @@
      */
     private Pair<InstanceId, com.android.launcher3.logging.InstanceId> mSessionInstanceIds;
 
+    private boolean mIsDestroyed = false;
+
     private final BackPressHandler mSplitBackHandler = new BackPressHandler() {
         @Override
         public boolean canHandleBack() {
@@ -182,12 +183,11 @@
         }
     };
 
-    public SplitSelectStateController(RecentsViewContainer container, Handler handler,
+    public SplitSelectStateController(RecentsViewContainer container,
             StateManager stateManager, DepthController depthController,
             StatsLogManager statsLogManager, SystemUiProxy systemUiProxy, RecentsModel recentsModel,
             Runnable activityBackCallback) {
         mContainer = container;
-        mHandler = handler;
         mStatsLogManager = statsLogManager;
         mSystemUiProxy = systemUiProxy;
         mStateManager = stateManager;
@@ -201,6 +201,7 @@
 
     public void onDestroy() {
         mContainer = null;
+        mIsDestroyed = true;
         mActivityBackCallback = null;
         mAppPairsController.onDestroy();
         mSplitSelectDataHolder.onDestroy();
@@ -366,7 +367,7 @@
      * A version of {@link #launchSplitTasks(int, Consumer)} that launches with default split ratio.
      */
     public void launchSplitTasks(@Nullable Consumer<Boolean> callback) {
-        launchSplitTasks(SNAP_TO_50_50, callback);
+        launchSplitTasks(SNAP_TO_2_50_50, callback);
     }
 
     /**
@@ -374,7 +375,7 @@
      * ratio and no callback.
      */
     public void launchSplitTasks() {
-        launchSplitTasks(SNAP_TO_50_50, null);
+        launchSplitTasks(SNAP_TO_2_50_50, null);
     }
 
     /**
@@ -568,13 +569,13 @@
         switch (launchData.getSplitLaunchType()) {
             case SPLIT_SINGLE_TASK_FULLSCREEN -> mSystemUiProxy.startTasks(firstTaskId,
                     optionsBundle, secondTaskId, null /* options2 */, initialStagePosition,
-                    SNAP_TO_50_50, remoteTransition, instanceId);
+                    SNAP_TO_2_50_50, remoteTransition, instanceId);
             case SPLIT_SINGLE_INTENT_FULLSCREEN -> mSystemUiProxy.startIntentAndTask(firstPI,
                     firstUserId, optionsBundle, secondTaskId, null /*options2*/,
-                    initialStagePosition, SNAP_TO_50_50, remoteTransition, instanceId);
+                    initialStagePosition, SNAP_TO_2_50_50, remoteTransition, instanceId);
             case SPLIT_SINGLE_SHORTCUT_FULLSCREEN -> mSystemUiProxy.startShortcutAndTask(
                     initialShortcut, optionsBundle, firstTaskId, null /* options2 */,
-                    initialStagePosition, SNAP_TO_50_50, remoteTransition, instanceId);
+                    initialStagePosition, SNAP_TO_2_50_50, remoteTransition, instanceId);
         }
     }
 
@@ -746,6 +747,9 @@
      */
     public void resetState() {
         mSplitSelectDataHolder.resetState();
+        if (!mIsDestroyed) {
+            mContainer.<RecentsView>getOverviewPanel().resetDesktopTaskFromSplitSelectState();
+        }
         dispatchOnSplitSelectionExit();
         mRecentsAnimationRunning = false;
         mLaunchingTaskView = null;
@@ -901,7 +905,7 @@
                 SystemUiProxy.INSTANCE.get(mLauncher.getApplicationContext())
                         .startRecentsActivity(
                                 mOverviewComponentObserver.getOverviewIntent(), options,
-                                callbacks);
+                                callbacks, false /* useSyntheticRecentsTransition */);
             });
         }
 
@@ -945,7 +949,16 @@
                 anim.addListener(new AnimatorListenerAdapter() {
                     @Override
                     public void onAnimationStart(Animator animation) {
-                        controller.finish(true /* toRecents */, null /* onFinishComplete */,
+                        controller.finish(
+                                true /* toRecents */,
+                                () -> {
+                                    LauncherTaskbarUIController controller =
+                                            mLauncher.getTaskbarUIController();
+                                    if (controller != null) {
+                                        controller.updateTaskbarLauncherStateGoingHome();
+                                    }
+
+                                },
                                 false /* sendUserLeaveHint */);
                     }
                     @Override
@@ -953,7 +966,16 @@
                         SystemUiProxy.INSTANCE.get(mLauncher.getApplicationContext())
                                 .onDesktopSplitSelectAnimComplete(mTaskInfo);
                     }
+                    @Override
+                    public void onAnimationCancel(Animator animation) {
+                        mLauncher.getDragLayer().removeView(floatingTaskView);
+                        getSplitAnimationController()
+                                .removeSplitInstructionsView(mLauncher);
+                        resetState();
+                    }
                 });
+                anim.add(getSplitAnimationController()
+                        .getShowSplitInstructionsAnim(mLauncher).buildAnim());
                 anim.buildAnim().start();
             }
         }
diff --git a/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java b/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java
index 4962367..bdfaa48 100644
--- a/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java
@@ -48,8 +48,11 @@
 import com.android.launcher3.uioverrides.QuickstepLauncher;
 import com.android.quickstep.views.FloatingTaskView;
 import com.android.quickstep.views.RecentsView;
+import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
 
+import java.util.Collections;
+
 /** Handles when the stage split lands on the home screen. */
 public class SplitToWorkspaceController {
 
@@ -133,10 +136,20 @@
             // Use Launcher's default click handler
             return false;
         }
-
-        mController.setSecondTask(intent, user, (ItemInfo) tag);
-
-        startWorkspaceAnimation(view, null /*bitmap*/, bitmapInfo.newIcon(mLauncher));
+        // Check for background task matching this tag; if we find one, set second task
+        // via task instead of intent so the bounds and windowing mode will be corrected.
+        mController.findLastActiveTasksAndRunCallback(
+                Collections.singletonList(((ItemInfo) tag).getComponentKey()),
+                false /* findExactPairMatch */,
+                foundTasks -> {
+                    Task foundTask = foundTasks[0];
+                    if (foundTask != null) {
+                        mController.setSecondTask(foundTask, (ItemInfo) tag);
+                    } else {
+                        mController.setSecondTask(intent, user, (ItemInfo) tag);
+                    }
+                    startWorkspaceAnimation(view, null /*bitmap*/, bitmapInfo.newIcon(mLauncher));
+                });
         return true;
     }
 
diff --git a/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java b/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java
index 4c6e4ff..744c08c 100644
--- a/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java
@@ -99,7 +99,8 @@
                 options.setTransientLaunch();
                 SystemUiProxy.INSTANCE.get(mLauncher.getApplicationContext())
                         .startRecentsActivity(mOverviewComponentObserver.getOverviewIntent(),
-                                ActivityOptions.makeBasic(), callbacks);
+                                ActivityOptions.makeBasic(), callbacks,
+                                false /* useSyntheticRecentsTransition */);
             });
         });
     }
diff --git a/quickstep/src/com/android/quickstep/util/SystemUiFlagUtils.kt b/quickstep/src/com/android/quickstep/util/SystemUiFlagUtils.kt
index 5f4388c..1ff05da 100644
--- a/quickstep/src/com/android/quickstep/util/SystemUiFlagUtils.kt
+++ b/quickstep/src/com/android/quickstep/util/SystemUiFlagUtils.kt
@@ -47,6 +47,22 @@
             !hasAnyFlag(flags, QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_GOING_AWAY)
     }
 
+    /**
+     * Taskbar is hidden whenever the device is dreaming. The dreaming state includes the
+     * interactive dreams, AoD, screen off. Since the SYSUI_STATE_DEVICE_DREAMING only kicks in when
+     * the device is asleep, the second condition extends ensures that the transition from and to
+     * the WAKEFULNESS_ASLEEP state also hide the taskbar, and improves the taskbar hide/reveal
+     * animation timings. The Taskbar can show when dreaming if the glanceable hub is showing on
+     * top.
+     */
+    @JvmStatic
+    fun isTaskbarHidden(@SystemUiStateFlags flags: Long): Boolean {
+        return ((hasAnyFlag(flags, QuickStepContract.SYSUI_STATE_DEVICE_DREAMING) &&
+            !hasAnyFlag(flags, QuickStepContract.SYSUI_STATE_COMMUNAL_HUB_SHOWING)) ||
+            (flags and QuickStepContract.SYSUI_STATE_WAKEFULNESS_MASK) !=
+                QuickStepContract.WAKEFULNESS_AWAKE)
+    }
+
     private fun hasAnyFlag(@SystemUiStateFlags flags: Long, flagMask: Long): Boolean {
         return (flags and flagMask) != 0L
     }
diff --git a/quickstep/src/com/android/quickstep/util/TaskRemovedDuringLaunchListener.java b/quickstep/src/com/android/quickstep/util/TaskRemovedDuringLaunchListener.java
index e80d2a6..40a328c 100644
--- a/quickstep/src/com/android/quickstep/util/TaskRemovedDuringLaunchListener.java
+++ b/quickstep/src/com/android/quickstep/util/TaskRemovedDuringLaunchListener.java
@@ -98,10 +98,7 @@
             final Runnable taskLaunchFailedCallback = mTaskLaunchFailedCallback;
             RecentsModel.INSTANCE.get(mContext).isTaskRemoved(mLaunchedTaskId, (taskRemoved) -> {
                 if (taskRemoved) {
-                    ActiveGestureLog.INSTANCE.addLog(
-                            new ActiveGestureLog.CompoundString("Launch failed, task (id=")
-                                    .append(launchedTaskId)
-                                    .append(") finished mid transition"));
+                    ActiveGestureProtoLogProxy.logTaskLaunchFailed(launchedTaskId);
                     taskLaunchFailedCallback.run();
                 }
             }, (task) -> true /* filter */);
diff --git a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
index c7777d8..a4b8fec 100644
--- a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
+++ b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
@@ -120,6 +120,7 @@
     private boolean mScaleToCarouselTaskSize = false;
     private int mTaskRectTranslationX;
     private int mTaskRectTranslationY;
+    private int mDesktopTaskIndex = 0;
 
     public TaskViewSimulator(Context context, BaseContainerInterface sizeStrategy) {
         mContext = context;
@@ -290,8 +291,9 @@
     /**
      * Sets whether this task is part of desktop tasks in overview.
      */
-    public void setIsDesktopTask(boolean desktop) {
+    public void setIsDesktopTask(boolean desktop, int index) {
         mIsDesktopTask = desktop;
+        mDesktopTaskIndex = index;
     }
 
     /**
@@ -305,6 +307,14 @@
     }
 
     /**
+     * Override the pivot used to apply scale changes.
+     */
+    public void setPivotOverride(PointF pivotOverride) {
+        mPivotOverride = pivotOverride;
+        getFullScreenScale();
+    }
+
+    /**
      * Adds animation for all the components corresponding to transition from an app to overview.
      */
     public void addAppToOverviewAnim(PendingAnimation pa, Interpolator interpolator) {
@@ -537,9 +547,9 @@
             // In shell transitions, the animation leashes are reparented to an animation container
             // so we can bump layers as needed.
             builder.setLayer(mDrawsBelowRecents
-                    ? Integer.MIN_VALUE + app.prefixOrderIndex
                     // 1000 is an arbitrary number to give room for multiple layers.
-                    : Integer.MAX_VALUE - 1000 + app.prefixOrderIndex);
+                    ? Integer.MIN_VALUE + 1000 + app.prefixOrderIndex - mDesktopTaskIndex
+                    : Integer.MAX_VALUE - 1000 + app.prefixOrderIndex - mDesktopTaskIndex);
         }
     }
 
diff --git a/quickstep/src/com/android/quickstep/util/TransformParams.java b/quickstep/src/com/android/quickstep/util/TransformParams.java
index ebcef30..401eccc 100644
--- a/quickstep/src/com/android/quickstep/util/TransformParams.java
+++ b/quickstep/src/com/android/quickstep/util/TransformParams.java
@@ -143,18 +143,15 @@
         for (int i = 0; i < targets.unfilteredApps.length; i++) {
             RemoteAnimationTarget app = targets.unfilteredApps[i];
             SurfaceProperties builder = transaction.forSurface(app.leash);
+            BuilderProxy targetProxy =
+                    app.windowConfiguration.getActivityType() == ACTIVITY_TYPE_HOME
+                            ? mHomeBuilderProxy
+                            : (app.mode == targets.targetMode ? proxy : mBaseBuilderProxy);
 
             if (app.mode == targets.targetMode) {
-                int activityType = app.windowConfiguration.getActivityType();
-                if (activityType == ACTIVITY_TYPE_HOME) {
-                    mHomeBuilderProxy.onBuildTargetParams(builder, app, this);
-                } else {
-                    builder.setAlpha(getTargetAlpha());
-                    proxy.onBuildTargetParams(builder, app, this);
-                }
-            } else {
-                mBaseBuilderProxy.onBuildTargetParams(builder, app, this);
+                builder.setAlpha(getTargetAlpha());
             }
+            targetProxy.onBuildTargetParams(builder, app, this);
         }
 
         // always put wallpaper layer to bottom.
diff --git a/quickstep/src/com/android/quickstep/util/WorkspaceRevealAnim.java b/quickstep/src/com/android/quickstep/util/WorkspaceRevealAnim.java
index 0a97793..32e0e13 100644
--- a/quickstep/src/com/android/quickstep/util/WorkspaceRevealAnim.java
+++ b/quickstep/src/com/android/quickstep/util/WorkspaceRevealAnim.java
@@ -30,6 +30,7 @@
 import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
 import android.util.FloatProperty;
+import android.util.Log;
 import android.view.View;
 
 import com.android.app.animation.Interpolators;
@@ -51,6 +52,8 @@
  */
 public class WorkspaceRevealAnim {
 
+    private static final String TAG = "WorkspaceRevealAnim";
+
     // Should be used for animations running alongside this WorkspaceRevealAnim.
     public static final int DURATION_MS = 350;
     private static final FloatProperty<Workspace<?>> WORKSPACE_SCALE_PROPERTY =
@@ -97,6 +100,19 @@
 
         mAnimators.setDuration(DURATION_MS);
         mAnimators.setInterpolator(Interpolators.DECELERATED_EASE);
+        mAnimators.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationCancel(Animator animation) {
+                super.onAnimationCancel(animation);
+                Log.d(TAG, "onAnimationCancel");
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                super.onAnimationEnd(animation);
+                Log.d(TAG, "onAnimationEnd: workspace alpha = " + workspace.getAlpha());
+            }
+        });
     }
 
     private <T extends View>  void addRevealAnimatorsForView(T v, FloatProperty<T> scaleProperty) {
diff --git a/quickstep/src/com/android/quickstep/views/DesktopTaskContentView.kt b/quickstep/src/com/android/quickstep/views/DesktopTaskContentView.kt
new file mode 100644
index 0000000..481acac
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/DesktopTaskContentView.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.views
+
+import android.content.Context
+import android.graphics.Outline
+import android.graphics.Rect
+import android.util.AttributeSet
+import android.view.View
+import android.view.ViewOutlineProvider
+import android.widget.FrameLayout
+import com.android.quickstep.views.TaskView.FullscreenDrawParams
+
+class DesktopTaskContentView
+@JvmOverloads
+constructor(context: Context, attrs: AttributeSet? = null) : FrameLayout(context, attrs) {
+    private val currentFullscreenParams = FullscreenDrawParams(context)
+    private val taskCornerRadius: Float
+        get() = currentFullscreenParams.cornerRadius
+
+    private val bounds = Rect()
+
+    init {
+        clipToOutline = true
+        outlineProvider =
+            object : ViewOutlineProvider() {
+                override fun getOutline(view: View, outline: Outline) {
+                    outline.setRoundRect(bounds, taskCornerRadius)
+                }
+            }
+    }
+
+    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
+        super.onSizeChanged(w, h, oldw, oldh)
+        bounds.set(0, 0, w, h)
+        invalidateOutline()
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
index 6db0923..5e842aa 100644
--- a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
@@ -25,8 +25,8 @@
 import android.util.AttributeSet
 import android.util.Log
 import android.view.Gravity
-import android.view.LayoutInflater
 import android.view.View
+import android.widget.FrameLayout
 import androidx.core.content.res.ResourcesCompat
 import androidx.core.view.updateLayoutParams
 import com.android.launcher3.Flags.enableRefactorTaskThumbnail
@@ -40,6 +40,8 @@
 import com.android.launcher3.util.rects.set
 import com.android.quickstep.BaseContainerInterface
 import com.android.quickstep.TaskOverlayFactory
+import com.android.quickstep.ViewUtils
+import com.android.quickstep.task.thumbnail.TaskThumbnailView
 import com.android.quickstep.util.RecentsOrientedState
 import com.android.systemui.shared.recents.model.Task
 
@@ -53,24 +55,39 @@
             override fun computeTaskCornerRadius(context: Context) =
                 computeWindowCornerRadius(context)
         }
+
     private val taskThumbnailViewDeprecatedPool =
-        ViewPool<TaskThumbnailViewDeprecated>(
-            context,
-            this,
-            R.layout.task_thumbnail_deprecated,
-            VIEW_POOL_MAX_SIZE,
-            VIEW_POOL_INITIAL_SIZE
-        )
+        if (!enableRefactorTaskThumbnail()) {
+            ViewPool<TaskThumbnailViewDeprecated>(
+                context,
+                this,
+                R.layout.task_thumbnail_deprecated,
+                VIEW_POOL_MAX_SIZE,
+                VIEW_POOL_INITIAL_SIZE,
+            )
+        } else null
+
+    private val taskThumbnailViewPool =
+        if (enableRefactorTaskThumbnail()) {
+            ViewPool<TaskThumbnailView>(
+                context,
+                this,
+                R.layout.task_thumbnail,
+                VIEW_POOL_MAX_SIZE,
+                VIEW_POOL_INITIAL_SIZE,
+            )
+        } else null
+
     private val tempPointF = PointF()
     private val tempRect = Rect()
     private lateinit var backgroundView: View
     private lateinit var iconView: TaskViewIcon
-    private var childCountAtInflation = 0
+    private lateinit var contentView: FrameLayout
 
     override fun onFinishInflate() {
         super.onFinishInflate()
         backgroundView =
-            findViewById<View>(R.id.background)!!.apply {
+            findViewById<View>(R.id.background).apply {
                 updateLayoutParams<LayoutParams> {
                     topMargin = container.deviceProfile.overviewTaskThumbnailTopMarginPx
                 }
@@ -80,7 +97,7 @@
                             setTint(
                                 resources.getColor(
                                     android.R.color.system_neutral2_300,
-                                    context.theme
+                                    context.theme,
                                 )
                             )
                         }
@@ -92,19 +109,24 @@
                     ResourcesCompat.getDrawable(
                         context.resources,
                         R.drawable.ic_desktop_with_bg,
-                        context.theme
-                    )
+                        context.theme,
+                    ),
                 )
                 setText(resources.getText(R.string.recent_task_desktop))
             }
-        childCountAtInflation = childCount
+        contentView =
+            findViewById<FrameLayout>(R.id.desktop_content).apply {
+                updateLayoutParams<LayoutParams> {
+                    topMargin = container.deviceProfile.overviewTaskThumbnailTopMarginPx
+                }
+            }
     }
 
     /** Updates this desktop task to the gives task list defined in `tasks` */
     fun bind(
         tasks: List<Task>,
         orientedState: RecentsOrientedState,
-        taskOverlayFactory: TaskOverlayFactory
+        taskOverlayFactory: TaskOverlayFactory,
     ) {
         if (DEBUG) {
             val sb = StringBuilder()
@@ -117,17 +139,12 @@
             tasks.map { task ->
                 val snapshotView =
                     if (enableRefactorTaskThumbnail()) {
-                        LayoutInflater.from(context).inflate(R.layout.task_thumbnail, this, false)
+                        taskThumbnailViewPool!!.view
                     } else {
-                        taskThumbnailViewDeprecatedPool.view
+                        taskThumbnailViewDeprecatedPool!!.view
                     }
+                contentView.addView(snapshotView, 0)
 
-                addView(
-                    snapshotView,
-                    // Add snapshotView to the front after initial views e.g. icon and
-                    // background.
-                    childCountAtInflation
-                )
                 TaskContainer(
                     this,
                     task,
@@ -137,7 +154,7 @@
                     SplitConfigurationOptions.STAGE_POSITION_UNDEFINED,
                     digitalWellBeingToast = null,
                     showWindowsView = null,
-                    taskOverlayFactory
+                    taskOverlayFactory,
                 )
             }
         taskContainers.forEach { it.bind() }
@@ -148,9 +165,11 @@
         super.onRecycle()
         visibility = VISIBLE
         taskContainers.forEach {
-            if (!enableRefactorTaskThumbnail()) {
-                removeView(it.thumbnailViewDeprecated)
-                taskThumbnailViewDeprecatedPool.recycle(it.thumbnailViewDeprecated)
+            contentView.removeView(it.snapshotView)
+            if (enableRefactorTaskThumbnail()) {
+                taskThumbnailViewPool!!.recycle(it.thumbnailView)
+            } else {
+                taskThumbnailViewDeprecatedPool!!.recycle(it.thumbnailViewDeprecated)
             }
         }
     }
@@ -159,12 +178,12 @@
     override fun updateTaskSize(
         lastComputedTaskSize: Rect,
         lastComputedGridTaskSize: Rect,
-        lastComputedCarouselTaskSize: Rect
+        lastComputedCarouselTaskSize: Rect,
     ) {
         super.updateTaskSize(
             lastComputedTaskSize,
             lastComputedGridTaskSize,
-            lastComputedCarouselTaskSize
+            lastComputedCarouselTaskSize,
         )
         if (taskContainers.isEmpty()) {
             return
@@ -186,7 +205,7 @@
             Log.d(
                 TAG,
                 "onMeasure: container=[$containerWidth,$containerHeight]" +
-                    "window=[$windowWidth,$windowHeight] scale=[$scaleWidth,$scaleHeight]"
+                    "window=[$windowWidth,$windowHeight] scale=[$scaleWidth,$scaleHeight]",
             )
         }
 
@@ -209,16 +228,14 @@
                 width = (taskSize.width() * scaleWidth).toInt()
                 height = (taskSize.height() * scaleHeight).toInt()
                 leftMargin = (positionInParent.x * scaleWidth).toInt()
-                topMargin =
-                    (positionInParent.y * scaleHeight).toInt() +
-                        container.deviceProfile.overviewTaskThumbnailTopMarginPx
+                topMargin = (positionInParent.y * scaleHeight).toInt()
             }
             if (DEBUG) {
                 with(it.snapshotView.layoutParams as LayoutParams) {
                     Log.d(
                         TAG,
                         "onMeasure: task=${it.task.key} size=[$width,$height]" +
-                            " margin=[$leftMargin,$topMargin]"
+                            " margin=[$leftMargin,$topMargin]",
                     )
                 }
             }
@@ -252,7 +269,7 @@
         TestLogging.recordEvent(
             TestProtocol.SEQUENCE_MAIN,
             "launchDesktopFromRecents",
-            taskIds.contentToString()
+            taskIds.contentToString(),
         )
         val endCallback = RunnableList()
         val desktopController = recentsView.desktopRecentsController
@@ -262,7 +279,7 @@
         }
         Log.d(
             TAG,
-            "launchTaskWithDesktopController: ${taskIds.contentToString()}, withRemoteTransition: $animated"
+            "launchTaskWithDesktopController: ${taskIds.contentToString()}, withRemoteTransition: $animated",
         )
 
         // Callbacks get run from recentsView for case when recents animation already running
@@ -274,11 +291,13 @@
 
     override fun launchWithoutAnimation(
         isQuickSwitch: Boolean,
-        callback: (launched: Boolean) -> Unit
+        callback: (launched: Boolean) -> Unit,
     ) = launchTaskWithDesktopController(animated = false)?.add { callback(true) } ?: callback(false)
 
-    // Desktop tile can't be in split screen
-    override fun confirmSecondSplitSelectApp(): Boolean = false
+    // Return true when Task cannot be launched as fullscreen (i.e. in split select state) to skip
+    // putting DesktopTaskView to split as it's not supported.
+    override fun confirmSecondSplitSelectApp(): Boolean =
+        recentsView?.canLaunchFullscreenTask() != true
 
     // TODO(b/330685808) support overlay for Screenshot action
     override fun setOverlayEnabled(overlayEnabled: Boolean) {}
@@ -294,10 +313,15 @@
 
     override fun getThumbnailFullscreenParams() = snapshotDrawParams
 
+    override fun addChildrenForAccessibility(outChildren: ArrayList<View>) {
+        super.addChildrenForAccessibility(outChildren)
+        ViewUtils.addAccessibleChildToList(backgroundView, outChildren)
+    }
+
     companion object {
         private const val TAG = "DesktopTaskView"
         private const val DEBUG = false
-        private const val VIEW_POOL_MAX_SIZE = 10
+        private const val VIEW_POOL_MAX_SIZE = 5
 
         // As DesktopTaskView is inflated in background, use initialSize=0 to avoid initPool.
         private const val VIEW_POOL_INITIAL_SIZE = 0
diff --git a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.kt b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.kt
index 7b97c23..c07b7fb 100644
--- a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.kt
+++ b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.kt
@@ -59,10 +59,10 @@
     context: Context,
     attrs: AttributeSet? = null,
     defStyleAttr: Int = 0,
-    defStyleRes: Int = 0
+    defStyleRes: Int = 0,
 ) : TextView(context, attrs, defStyleAttr, defStyleRes) {
-    private val recentsViewContainer =
-        RecentsViewContainer.containerFromContext<RecentsViewContainer>(context)
+    private val recentsViewContainer: RecentsViewContainer =
+        RecentsViewContainer.containerFromContext(context)
 
     private val launcherApps: LauncherApps? = context.getSystemService(LauncherApps::class.java)
 
@@ -138,7 +138,7 @@
                 usageLimit =
                     launcherApps?.getAppUsageLimit(
                         task.topComponent.packageName,
-                        UserHandle.of(task.key.userId)
+                        UserHandle.of(task.key.userId),
                     )
             } catch (e: Exception) {
                 Log.e(TAG, "Error initializing digital well being toast", e)
@@ -162,7 +162,7 @@
         task: Task,
         taskView: TaskView,
         snapshotView: View,
-        @StagePosition stagePosition: Int
+        @StagePosition stagePosition: Int,
     ) {
         this.task = task
         this.taskView = taskView
@@ -201,7 +201,7 @@
 
     private fun getReadableDuration(
         duration: Duration,
-        @StringRes durationLessThanOneMinuteStringId: Int
+        @StringRes durationLessThanOneMinuteStringId: Int,
     ): String {
         val hours = Math.toIntExact(duration.toHours())
         val minutes = Math.toIntExact(duration.minusHours(hours.toLong()).toMinutes())
@@ -211,7 +211,7 @@
                 MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.NARROW)
                     .formatMeasures(
                         Measure(hours, MeasureUnit.HOUR),
-                        Measure(minutes, MeasureUnit.MINUTE)
+                        Measure(minutes, MeasureUnit.MINUTE),
                     )
             // Apply FormatWidth.WIDE if only the hour part is non-zero (unless forced).
             hours > 0 ->
@@ -239,7 +239,7 @@
     @VisibleForTesting
     fun getBannerText(
         remainingTime: Long = appRemainingTimeMs,
-        forContentDesc: Boolean = false
+        forContentDesc: Boolean = false,
     ): String {
         val duration =
             Duration.ofMillis(
@@ -250,7 +250,7 @@
         val readableDuration =
             getReadableDuration(
                 duration,
-                R.string.shorter_duration_less_than_one_minute /* forceFormatWidth */
+                R.string.shorter_duration_less_than_one_minute, /* forceFormatWidth */
             )
         val splitBannerConfig = getSplitBannerConfig()
         return when {
@@ -277,7 +277,7 @@
             Log.e(
                 TAG,
                 "Failed to open app usage settings for task " + task.topComponent.packageName,
-                e
+                e,
             )
         }
     }
@@ -285,13 +285,13 @@
     private fun getContentDescriptionForTask(
         task: Task,
         appUsageLimitTimeMs: Long,
-        appRemainingTimeMs: Long
+        appRemainingTimeMs: Long,
     ): String? =
         if (appUsageLimitTimeMs >= 0 && appRemainingTimeMs >= 0)
             context.getString(
                 R.string.task_contents_description_with_remaining_time,
                 task.titleDescription,
-                getBannerText(appRemainingTimeMs, true /* forContentDesc */)
+                getBannerText(appRemainingTimeMs, true /* forContentDesc */),
             )
         else task.titleDescription
 
@@ -310,7 +310,7 @@
                     recentsViewContainer.deviceProfile,
                     splitBounds,
                     taskView.layoutParams.width,
-                    taskView.layoutParams.height
+                    taskView.layoutParams.height,
                 )
             if (stagePosition == STAGE_POSITION_TOP_OR_LEFT) {
                 snapshotWidth = groupedTaskSize.first.x
@@ -327,7 +327,7 @@
             recentsViewContainer.deviceProfile,
             snapshotWidth,
             snapshotHeight,
-            this
+            this,
         )
     }
 
@@ -340,7 +340,7 @@
                 recentsViewContainer.deviceProfile,
                 taskView.snapshotViews,
                 task.key.id,
-                this
+                this,
             )
         this.translationX = translationX
         this.splitOffsetTranslationY = translationY
@@ -372,7 +372,7 @@
             if (taskView.containsMultipleTasks())
                 context.getString(
                     R.string.split_app_usage_settings,
-                    TaskUtils.getTitle(context, task)
+                    TaskUtils.getTitle(context, task),
                 )
             else context.getString(R.string.accessibility_app_usage_settings)
         return AccessibilityNodeInfo.AccessibilityAction(getAccessibilityActionId(), label)
@@ -394,7 +394,7 @@
             /** Used for grid task view, only showing icon and time */
             SPLIT_GRID_BANNER_LARGE,
             /** Used for grid task view, only showing icon */
-            SPLIT_GRID_BANNER_SMALL
+            SPLIT_GRID_BANNER_SMALL,
         }
 
         val OPEN_APP_USAGE_SETTINGS_TEMPLATE: Intent = Intent(Settings.ACTION_APP_USAGE_SETTINGS)
diff --git a/quickstep/src/com/android/quickstep/views/FloatingWidgetView.java b/quickstep/src/com/android/quickstep/views/FloatingWidgetView.java
index bdca596..b719ee5 100644
--- a/quickstep/src/com/android/quickstep/views/FloatingWidgetView.java
+++ b/quickstep/src/com/android/quickstep/views/FloatingWidgetView.java
@@ -18,6 +18,7 @@
 import android.animation.Animator;
 import android.animation.Animator.AnimatorListener;
 import android.annotation.TargetApi;
+import android.app.TaskInfo;
 import android.content.Context;
 import android.graphics.Matrix;
 import android.graphics.RectF;
@@ -333,11 +334,16 @@
      * context's theme background color.
      */
     public static int getDefaultBackgroundColor(
-            Context context, RemoteAnimationTarget target) {
-        return (target != null && target.taskInfo != null
-                && target.taskInfo.taskDescription != null)
-                ? target.taskInfo.taskDescription.getBackgroundColor()
-                : Themes.getColorBackground(context);
+            Context context, @Nullable RemoteAnimationTarget target) {
+        final int fallbackColor = Themes.getColorBackground(context);
+        if (target == null) {
+            return fallbackColor;
+        }
+        final TaskInfo taskInfo = target.taskInfo;
+        if (taskInfo == null) {
+            return fallbackColor;
+        }
+        return taskInfo.taskDescription.getBackgroundColor();
     }
 
     private static void getRelativePosition(View descendant, View ancestor, RectF position) {
diff --git a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
index 73edb9e..bbb8cc8 100644
--- a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
@@ -16,16 +16,13 @@
 package com.android.quickstep.views;
 
 import static android.app.ActivityTaskManager.INVALID_TASK_ID;
-import static android.window.flags.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY;
+import static android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY;
 
-import static com.android.launcher3.LauncherState.ALL_APPS;
 import static com.android.launcher3.LauncherState.CLEAR_ALL_BUTTON;
-import static com.android.launcher3.LauncherState.EDIT_MODE;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.LauncherState.OVERVIEW_MODAL_TASK;
 import static com.android.launcher3.LauncherState.OVERVIEW_SPLIT_SELECT;
-import static com.android.launcher3.LauncherState.SPRING_LOADED;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTION_EXIT_HOME;
 
 import android.annotation.TargetApi;
@@ -173,8 +170,7 @@
 
     @Override
     public void onStateTransitionComplete(LauncherState finalState) {
-        if (finalState == NORMAL || finalState == SPRING_LOADED  || finalState == EDIT_MODE
-                || finalState == ALL_APPS) {
+        if (!finalState.isRecentsViewVisible) {
             // Clean-up logic that occurs when recents is no longer in use/visible.
             reset();
         }
@@ -254,7 +250,7 @@
     }
 
     @Override
-    protected boolean canLaunchFullscreenTask() {
+    public boolean canLaunchFullscreenTask() {
         if (FeatureFlags.enableSplitContextually()) {
             return !mSplitSelectStateController.isSplitSelectActive();
         } else {
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 287a34d..9d74bfb 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -129,6 +129,7 @@
 import android.widget.ListView;
 import android.widget.OverScroller;
 import android.widget.Toast;
+import android.window.DesktopModeFlags;
 import android.window.PictureInPictureSurfaceTransaction;
 
 import androidx.annotation.NonNull;
@@ -205,8 +206,7 @@
 import com.android.quickstep.recents.di.RecentsDependencies;
 import com.android.quickstep.recents.viewmodel.RecentsViewData;
 import com.android.quickstep.recents.viewmodel.RecentsViewModel;
-import com.android.quickstep.util.ActiveGestureErrorDetector;
-import com.android.quickstep.util.ActiveGestureLog;
+import com.android.quickstep.util.ActiveGestureProtoLogProxy;
 import com.android.quickstep.util.AnimUtils;
 import com.android.quickstep.util.DesktopTask;
 import com.android.quickstep.util.GroupTask;
@@ -258,7 +258,7 @@
  * @param <STATE_TYPE>     : the type of base state that will be used
  */
 public abstract class RecentsView<
-        CONTAINER_TYPE extends Context & RecentsViewContainer,
+        CONTAINER_TYPE extends Context & RecentsViewContainer & StatefulContainer<STATE_TYPE>,
         STATE_TYPE extends BaseState<STATE_TYPE>> extends PagedView implements Insettable,
         TaskThumbnailCache.HighResLoadingState.HighResLoadingStateChangedCallback,
         TaskVisualsChangeListener {
@@ -325,20 +325,13 @@
             new FloatProperty<RecentsView>("runningTaskAttachAlpha") {
                 @Override
                 public void setValue(RecentsView recentsView, float v) {
-                    TaskView runningTask = recentsView.getRunningTaskView();
-                    if (runningTask == null) {
-                        return;
-                    }
-                    runningTask.setAttachAlpha(v);
+                    recentsView.mRunningTaskAttachAlpha = v;
+                    recentsView.applyAttachAlpha();
                 }
 
                 @Override
                 public Float get(RecentsView recentsView) {
-                    TaskView runningTask = recentsView.getRunningTaskView();
-                    if (runningTask == null) {
-                        return null;
-                    }
-                    return runningTask.getAttachAlpha();
+                    return recentsView.mRunningTaskAttachAlpha;
                 }
             };
 
@@ -464,6 +457,21 @@
                 }
             };
 
+    public static final FloatProperty<RecentsView> DESKTOP_CAROUSEL_DETACH_PROGRESS =
+            new FloatProperty<>("desktopCarouselDetachProgress") {
+                @Override
+                public void setValue(RecentsView view, float offset) {
+                    view.mDesktopCarouselDetachProgress = offset;
+                    view.applyAttachAlpha();
+                    view.updatePageOffsets();
+                }
+
+                @Override
+                public Float get(RecentsView view) {
+                    return view.mDesktopCarouselDetachProgress;
+                }
+            };
+
     /**
      * Alpha of the task thumbnail splash, where being in BackgroundAppState has a value of 1, and
      * being in any other state has a value of 0.
@@ -575,6 +583,7 @@
     private int mClampedScrollOffsetBound;
 
     private float mAdjacentPageHorizontalOffset = 0;
+    private float mDesktopCarouselDetachProgress = 0;
     protected float mTaskViewsSecondaryTranslation = 0;
     protected float mTaskViewsPrimarySplitTranslation = 0;
     protected float mTaskViewsSecondarySplitTranslation = 0;
@@ -681,13 +690,13 @@
     protected int mRunningTaskViewId = -1;
     private int mTaskViewIdCount;
     protected boolean mRunningTaskTileHidden;
-    private boolean mNonRunningTaskCategoryHidden;
     @Nullable
     private Task[] mTmpRunningTasks;
     protected int mFocusedTaskViewId = INVALID_TASK_ID;
 
     private boolean mTaskIconScaledDown = false;
     private boolean mRunningTaskShowScreenshot = false;
+    private float mRunningTaskAttachAlpha;
 
     private boolean mOverviewStateEnabled;
     private boolean mHandleTaskStackChanges;
@@ -799,12 +808,6 @@
     @Nullable
     private DesktopRecentsTransitionController mDesktopRecentsTransitionController;
 
-    /**
-     * Keeps track of the desktop task. Optional and only present when the feature flag is enabled.
-     */
-    @Nullable
-    private DesktopTaskView mDesktopTaskView;
-
     private MultiWindowModeChangedListener mMultiWindowModeChangedListener =
             new MultiWindowModeChangedListener() {
                 @Override
@@ -1178,7 +1181,7 @@
      *
      * @return {@code true} if child TaskViews can be launched when user taps on them
      */
-    protected boolean canLaunchFullscreenTask() {
+    public boolean canLaunchFullscreenTask() {
         return true;
     }
 
@@ -1209,6 +1212,7 @@
     @Override
     protected void onDetachedFromWindow() {
         super.onDetachedFromWindow();
+
         updateTaskStackListenerState();
         mModel.getThumbnailCache().getHighResLoadingState().removeCallback(this);
         mContainer.removeMultiWindowModeChangedListener(mMultiWindowModeChangedListener);
@@ -1241,21 +1245,24 @@
         // - It's the focused task to be moved to the front, we immediately re-add the task
         if (child instanceof TaskView && child != mSplitHiddenTaskView
                 && child != mMovingTaskView) {
-            TaskView taskView = (TaskView) child;
-            for (int i : taskView.getTaskIds()) {
-                mHasVisibleTaskData.delete(i);
-            }
-            if (child instanceof GroupedTaskView) {
-                mGroupedTaskViewPool.recycle((GroupedTaskView) taskView);
-            } else if (child instanceof DesktopTaskView) {
-                mDesktopTaskViewPool.recycle((DesktopTaskView) taskView);
-            } else {
-                mTaskViewPool.recycle(taskView);
-            }
-            mActionsView.updateHiddenFlags(HIDDEN_NO_TASKS, getTaskViewCount() == 0);
+            clearAndRecycleTaskView((TaskView) child);
         }
     }
 
+    private void clearAndRecycleTaskView(TaskView taskView) {
+        for (int taskId : taskView.getTaskIds()) {
+            mHasVisibleTaskData.delete(taskId);
+        }
+        if (taskView instanceof GroupedTaskView) {
+            mGroupedTaskViewPool.recycle((GroupedTaskView) taskView);
+        } else if (taskView instanceof DesktopTaskView) {
+            mDesktopTaskViewPool.recycle((DesktopTaskView) taskView);
+        } else {
+            mTaskViewPool.recycle(taskView);
+        }
+        mActionsView.updateHiddenFlags(HIDDEN_NO_TASKS, getTaskViewCount() == 0);
+    }
+
     @Override
     public void onViewAdded(View child) {
         super.onViewAdded(child);
@@ -1586,8 +1593,7 @@
     @Override
     protected void onPageEndTransition() {
         super.onPageEndTransition();
-        ActiveGestureLog.INSTANCE.addLog(
-                "onPageEndTransition: current page index updated", getNextPage());
+        ActiveGestureProtoLogProxy.logOnPageEndTransition(getNextPage());
         if (isClearAllHidden() && !mContainer.getDeviceProfile().isTablet) {
             mActionsView.updateDisabledFlags(OverviewActionsView.DISABLED_SCROLLING, false);
         }
@@ -1704,10 +1710,11 @@
                     return;
                 }
                 TaskView taskView = getTaskViewAt(mNextPage);
-                // Snap to fully visible focused task and clear all button.
                 boolean shouldSnapToLargeTask = taskView != null && taskView.isLargeTile()
-                        && isTaskViewFullyVisible(taskView);
+                        && !mUtils.isAnySmallTaskFullyVisible(getTaskViews(),
+                        this::isTaskViewFullyVisible);
                 boolean shouldSnapToClearAll = mNextPage == indexOfChild(mClearAllButton);
+                // Snap to large tile when grid tasks aren't fully visible or the clear all button.
                 if (!shouldSnapToLargeTask && !shouldSnapToClearAll) {
                     return;
                 }
@@ -1791,8 +1798,7 @@
 
     @Override
     protected void onScrollerAnimationAborted() {
-        ActiveGestureLog.INSTANCE.addLog("scroller animation aborted",
-                ActiveGestureErrorDetector.GestureEvent.SCROLLER_ANIMATION_ABORTED);
+        ActiveGestureProtoLogProxy.logOnScrollerAnimationAborted();
     }
 
     @Override
@@ -1872,7 +1878,6 @@
         mFilterState.updateInstanceCountMap(taskGroups);
 
         // Clear out desktop view if it is set
-        mDesktopTaskView = null;
 
         // Move Desktop Tasks to the end of the list
         if (enableLargeDesktopWindowingTile()) {
@@ -1883,19 +1888,22 @@
         // taskGroups backwards populates the thumbnail grid from least recent to most recent.
         for (int i = taskGroups.size() - 1; i >= 0; i--) {
             GroupTask groupTask = taskGroups.get(i);
-            boolean isRemovalNeeded = stagedTaskIdToBeRemoved != INVALID_TASK_ID
+            boolean containsStagedTask = stagedTaskIdToBeRemoved != INVALID_TASK_ID
                     && groupTask.containsTask(stagedTaskIdToBeRemoved);
+            boolean shouldSkipGroupTask = containsStagedTask && !groupTask.hasMultipleTasks();
 
-            if (isRemovalNeeded && !groupTask.hasMultipleTasks()) {
-                // If the task we need to remove is not part of a pair, avoiding creating the
-                // TaskView.
+            if ((isSplitSelectionActive() && groupTask.taskViewType == TaskViewType.DESKTOP)
+                    || shouldSkipGroupTask) {
+                // To avoid these tasks from being chosen as the app pair, the creation of a
+                // TaskView is bypassed. The staged task is already selected for the app pair,
+                // and the Desktop task should be hidden when selecting a pair.
                 continue;
             }
 
             // 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 ? TaskViewType.SINGLE : groupTask.taskViewType);
+                    containsStagedTask ? TaskViewType.SINGLE : groupTask.taskViewType);
             if (taskView instanceof GroupedTaskView) {
                 boolean firstTaskIsLeftTopTask =
                         groupTask.mSplitBounds.leftTopTaskId == groupTask.task1.key.id;
@@ -1911,7 +1919,6 @@
                                 .toList();
                 ((DesktopTaskView) taskView).bind(nonMinimizedTasks, mOrientationState,
                         mTaskOverlayFactory);
-                mDesktopTaskView = (DesktopTaskView) taskView;
             } else {
                 Task task = groupTask.task1.key.id == stagedTaskIdToBeRemoved ? groupTask.task2
                         : groupTask.task1;
@@ -1931,6 +1938,9 @@
 
         // Keep same previous focused task
         TaskView newFocusedTaskView = getTaskViewByTaskIds(focusedTaskIds);
+        if (enableLargeDesktopWindowingTile() && newFocusedTaskView instanceof DesktopTaskView) {
+            newFocusedTaskView = null;
+        }
         // If the list changed, maybe the focused task doesn't exist anymore
         int newFocusedTaskViewIndex = mUtils.getFocusedTaskIndex(taskGroups);
         if (newFocusedTaskView == null && getTaskViewCount() > newFocusedTaskViewIndex) {
@@ -1956,7 +1966,7 @@
                     // We try to avoid this because it can cause a scroll jump, but it is needed
                     // for cases where the running task isn't included in this load plan (e.g. if
                     // the current running task is excludedFromRecents.)
-                    showCurrentTask(mActiveGestureRunningTasks);
+                    showCurrentTask(mActiveGestureRunningTasks, "applyLoadPlan");
                 } else {
                     setRunningTaskViewId(INVALID_TASK_ID);
                 }
@@ -2739,13 +2749,10 @@
             updateSizeAndPadding();
         }
 
-        showCurrentTask(mActiveGestureRunningTasks);
+        showCurrentTask(mActiveGestureRunningTasks, "onGestureAnimationStart");
         setEnableFreeScroll(false);
         setEnableDrawingLiveTile(false);
         setRunningTaskHidden(true);
-        if (enableLargeDesktopWindowingTile()) {
-            setNonRunningTaskCategoryHidden(true);
-        }
         setTaskIconScaledDown(true);
     }
 
@@ -2869,6 +2876,14 @@
             animatorSet.play(
                     ObjectAnimator.ofFloat(this, TASK_THUMBNAIL_SPLASH_ALPHA, splashAlpha));
         }
+        if (enableLargeDesktopWindowingTile()) {
+            if (animatorSet != null) {
+                animatorSet.play(
+                        ObjectAnimator.ofFloat(this, DESKTOP_CAROUSEL_DETACH_PROGRESS, 0f));
+            } else {
+                DESKTOP_CAROUSEL_DETACH_PROGRESS.set(this, 0f);
+            }
+        }
     }
 
     /**
@@ -2884,9 +2899,6 @@
         setEnableDrawingLiveTile(mCurrentGestureEndTarget == GestureState.GestureEndTarget.RECENTS);
         Log.d(TAG, "onGestureAnimationEnd - mEnableDrawingLiveTile: " + mEnableDrawingLiveTile);
         setRunningTaskHidden(false);
-        if (enableLargeDesktopWindowingTile()) {
-            setNonRunningTaskCategoryHidden(false);
-        }
         animateUpTaskIconScale();
         animateActionsViewIn();
 
@@ -2918,8 +2930,9 @@
      * All subsequent calls to reload will keep the task as the first item until {@link #reset()}
      * is called.  Also scrolls the view to this task.
      */
-    private void showCurrentTask(Task[] runningTasks) {
-        Log.d(TAG, "showCurrentTask - runningTasks: " + Arrays.toString(runningTasks));
+    private void showCurrentTask(Task[] runningTasks, String caller) {
+        Log.d(TAG, "showCurrentTask(" + caller + ") - runningTasks: "
+                + Arrays.toString(runningTasks));
         if (runningTasks.length == 0) {
             return;
         }
@@ -2969,10 +2982,19 @@
         boolean runningTaskTileHidden = mRunningTaskTileHidden;
         setCurrentTask(runningTaskViewId);
 
-        boolean shouldFocusRunningTask = !(enableGridOnlyOverview()
-                || (enableLargeDesktopWindowingTile()
-                && getRunningTaskView() instanceof DesktopTaskView));
-        setFocusedTaskViewId(shouldFocusRunningTask ? runningTaskViewId : INVALID_TASK_ID);
+        int focusedTaskViewId;
+        if (enableGridOnlyOverview()) {
+            focusedTaskViewId = INVALID_TASK_ID;
+        } else if (enableLargeDesktopWindowingTile()
+                && getRunningTaskView() instanceof DesktopTaskView) {
+            TaskView focusedTaskView = getTaskViewAt(getDesktopTaskViewCount());
+            focusedTaskViewId =
+                    focusedTaskView != null ? focusedTaskView.getTaskViewId() : INVALID_TASK_ID;
+        } else {
+            focusedTaskViewId = runningTaskViewId;
+        }
+        setFocusedTaskViewId(focusedTaskViewId);
+
         runOnPageScrollsInitialized(() -> setCurrentPage(getRunningTaskIndex()));
         setRunningTaskViewShowScreenshot(false);
         setRunningTaskHidden(runningTaskTileHidden);
@@ -3038,6 +3060,9 @@
      */
     public void setRunningTaskHidden(boolean isHidden) {
         mRunningTaskTileHidden = isHidden;
+        // mRunningTaskAttachAlpha can be changed by RUNNING_TASK_ATTACH_ALPHA animation without
+        // changing mRunningTaskTileHidden.
+        mRunningTaskAttachAlpha = isHidden ? 0f : 1f;
         TaskView runningTask = getRunningTaskView();
         if (runningTask == null) {
             return;
@@ -3049,18 +3074,11 @@
         }
     }
 
-    /**
-     * Hides the tasks that has a different category (Fullscreen/Desktop) from the running task.
-     */
-    public void setNonRunningTaskCategoryHidden(boolean isHidden) {
-        mNonRunningTaskCategoryHidden = isHidden;
-        updateMinAndMaxScrollX();
-        applyAttachAlpha();
-    }
-
     private void applyAttachAlpha() {
-        mUtils.applyAttachAlpha(getTaskViews(), getRunningTaskView(), mRunningTaskTileHidden,
-                mNonRunningTaskCategoryHidden);
+        // Only hide non running task carousel when it's fully off screen, otherwise it needs to
+        // be visible to move to on screen.
+        mUtils.applyAttachAlpha(getTaskViews(), getRunningTaskView(), mRunningTaskAttachAlpha,
+                /*nonRunningTaskCarouselHidden=*/mDesktopCarouselDetachProgress == 1f);
     }
 
     private void setRunningTaskViewShowScreenshot(boolean showScreenshot) {
@@ -3143,7 +3161,9 @@
         // Horizontal grid translation for each task
         float[] gridTranslations = new float[taskCount];
 
-        int focusedTaskIndex = Integer.MAX_VALUE;
+        TaskView lastLargeTaskView = mUtils.getLastLargeTaskView(getTaskViews());
+        int lastLargeTaskIndex =
+                (lastLargeTaskView == null) ? Integer.MAX_VALUE : indexOfChild(lastLargeTaskView);
         Set<Integer> largeTasksIndices = new HashSet<>();
         int focusedTaskShift = 0;
         int largeTaskWidthAndSpacing = 0;
@@ -3166,8 +3186,12 @@
             boolean isLargeTile = taskView.isLargeTile();
 
             if (isLargeTile) {
-                topRowWidth += taskWidthAndSpacing;
-                bottomRowWidth += taskWidthAndSpacing;
+                // DesktopTaskView`s are hidden during split select state, so we shouldn't count
+                // them when calculating row width.
+                if (!(taskView instanceof DesktopTaskView && isSplitSelectionActive())) {
+                    topRowWidth += taskWidthAndSpacing;
+                    bottomRowWidth += taskWidthAndSpacing;
+                }
                 gridTranslations[i] += focusedTaskShift;
                 gridTranslations[i] += mIsRtl ? taskWidthAndSpacing : -taskWidthAndSpacing;
 
@@ -3175,9 +3199,6 @@
                 taskView.setGridTranslationY((mLastComputedTaskSize.height() + taskTopMargin
                         - taskView.getLayoutParams().height) / 2f);
 
-                if (taskView.getTaskViewId() == mFocusedTaskViewId) {
-                    focusedTaskIndex = i;
-                }
                 largeTasksIndices.add(i);
                 largeTaskWidthAndSpacing = taskWidthAndSpacing;
 
@@ -3186,8 +3207,8 @@
                     snappedTaskRowWidth = taskWidthAndSpacing;
                 }
             } else {
-                if (i > focusedTaskIndex) {
-                    // For tasks after the focused task, shift by focused task's width and spacing.
+                if (i > lastLargeTaskIndex) {
+                    // For tasks after the last large task, shift by large task's width and spacing.
                     gridTranslations[i] +=
                             mIsRtl ? largeTaskWidthAndSpacing : -largeTaskWidthAndSpacing;
                 } else {
@@ -3587,7 +3608,8 @@
      * @param dismissingForSplitSelection task dismiss animation is used for entering split
      *                                    selection state from app icon
      */
-    public void createTaskDismissAnimation(PendingAnimation anim, TaskView dismissedTaskView,
+    public void createTaskDismissAnimation(PendingAnimation anim,
+            @Nullable TaskView dismissedTaskView,
             boolean animateTaskView, boolean shouldRemoveTask, long duration,
             boolean dismissingForSplitSelection) {
         if (mPendingAnimation != null) {
@@ -3602,7 +3624,8 @@
         boolean showAsGrid = showAsGrid();
         int taskCount = getTaskViewCount();
         int dismissedIndex = indexOfChild(dismissedTaskView);
-        int dismissedTaskViewId = dismissedTaskView.getTaskViewId();
+        int dismissedTaskViewId =
+                dismissedTaskView != null ? dismissedTaskView.getTaskViewId() : INVALID_TASK_ID;
 
         // Grid specific properties.
         boolean isFocusedTaskDismissed = false;
@@ -3612,15 +3635,23 @@
         float dismissedTaskWidth = 0;
         float nextFocusedTaskWidth = 0;
 
-        // Non-grid specific properties.
         int[] oldScroll = new int[count];
         int[] newScroll = new int[count];
         int scrollDiffPerPage = 0;
+        // Non-grid specific properties.
         boolean needsCurveUpdates = false;
+        boolean areAllDesktopTasksDismissed = false;
 
         if (showAsGrid) {
-            dismissedTaskWidth = dismissedTaskView.getLayoutParams().width + mPageSpacing;
-            isFocusedTaskDismissed = dismissedTaskViewId == mFocusedTaskViewId;
+            if (dismissedTaskView != null) {
+                dismissedTaskWidth = dismissedTaskView.getLayoutParams().width + mPageSpacing;
+            }
+            isFocusedTaskDismissed = dismissedTaskViewId != INVALID_TASK_ID
+                    && dismissedTaskViewId == mFocusedTaskViewId;
+            if (dismissingForSplitSelection && getTaskViewAt(
+                    mCurrentPage) instanceof DesktopTaskView) {
+                areAllDesktopTasksDismissed = true;
+            }
             if (isFocusedTaskDismissed) {
                 if (isSplitSelectionActive()) {
                     isStagingFocusedTask = true;
@@ -3645,13 +3676,13 @@
                     }
                 }
             }
-        } else {
-            getPageScrolls(oldScroll, false, SIMPLE_SCROLL_LOGIC);
-            getPageScrolls(newScroll, false,
-                    v -> v.getVisibility() != GONE && v != dismissedTaskView);
-            if (count > 1) {
-                scrollDiffPerPage = Math.abs(oldScroll[1] - oldScroll[0]);
-            }
+        }
+
+        getPageScrolls(oldScroll, false, SIMPLE_SCROLL_LOGIC);
+        getPageScrolls(newScroll, false,
+                v -> v.getVisibility() != GONE && v != dismissedTaskView);
+        if (count > 1) {
+            scrollDiffPerPage = Math.abs(oldScroll[1] - oldScroll[0]);
         }
 
         float dismissTranslationInterpolationEnd = 1;
@@ -3664,31 +3695,43 @@
         int currentPageScroll = getScrollForPage(mCurrentPage);
         int lastGridTaskScroll = getScrollForPage(indexOfChild(lastGridTaskView));
         boolean currentPageSnapsToEndOfGrid = currentPageScroll == lastGridTaskScroll;
+
+        int topGridRowSize = mTopRowIdSet.size();
+        int numLargeTiles = mUtils.getLargeTileCount(getTaskViews());
+        int bottomGridRowSize = taskCount - mTopRowIdSet.size() - numLargeTiles;
+        boolean topRowLonger = topGridRowSize > bottomGridRowSize;
+        boolean bottomRowLonger = bottomGridRowSize > topGridRowSize;
+        boolean dismissedTaskFromTop = mTopRowIdSet.contains(dismissedTaskViewId);
+        boolean dismissedTaskFromBottom = !dismissedTaskFromTop && !isFocusedTaskDismissed;
+        if (dismissedTaskFromTop || (isFocusedTaskDismissed && nextFocusedTaskFromTop)) {
+            topGridRowSize--;
+        }
+        if (dismissedTaskFromBottom || (isFocusedTaskDismissed && !nextFocusedTaskFromTop)) {
+            bottomGridRowSize--;
+        }
+        int longRowWidth = Math.max(topGridRowSize, bottomGridRowSize)
+                * (mLastComputedGridTaskSize.width() + mPageSpacing);
+        if (!enableGridOnlyOverview() && !isStagingFocusedTask) {
+            longRowWidth += mLastComputedTaskSize.width() + mPageSpacing;
+        }
+        // Compensate the removed gap if we don't already have shortTotalCompensation,
+        // and adjust accordingly to the new shortTotalCompensation after dismiss.
+        int newClearAllShortTotalWidthTranslation = 0;
+        if (mClearAllShortTotalWidthTranslation == 0) {
+            // If first task is not in the expected position (mLastComputedTaskSize) and being too
+            // close  to ClearAllButton, then apply extra translation to ClearAllButton.
+            int firstTaskStart = mLastComputedGridSize.left + longRowWidth;
+            int expectedFirstTaskStart = mLastComputedTaskSize.right;
+            if (firstTaskStart < expectedFirstTaskStart) {
+                newClearAllShortTotalWidthTranslation = expectedFirstTaskStart - firstTaskStart;
+            }
+        }
         if (lastGridTaskView != null && lastGridTaskView.isVisibleToUser()) {
             // After dismissal, animate translation of the remaining tasks to fill any gap left
             // between the end of the grid and the clear all button. Only animate if the clear
             // all button is visible or would become visible after dismissal.
             float longGridRowWidthDiff = 0;
 
-            int topGridRowSize = mTopRowIdSet.size();
-            int bottomGridRowSize = taskCount - mTopRowIdSet.size()
-                    - (enableGridOnlyOverview() ? 0 : 1);
-            boolean topRowLonger = topGridRowSize > bottomGridRowSize;
-            boolean bottomRowLonger = bottomGridRowSize > topGridRowSize;
-            boolean dismissedTaskFromTop = mTopRowIdSet.contains(dismissedTaskViewId);
-            boolean dismissedTaskFromBottom = !dismissedTaskFromTop && !isFocusedTaskDismissed;
-            if (dismissedTaskFromTop || (isFocusedTaskDismissed && nextFocusedTaskFromTop)) {
-                topGridRowSize--;
-            }
-            if (dismissedTaskFromBottom || (isFocusedTaskDismissed && !nextFocusedTaskFromTop)) {
-                bottomGridRowSize--;
-            }
-            int longRowWidth = Math.max(topGridRowSize, bottomGridRowSize)
-                    * (mLastComputedGridTaskSize.width() + mPageSpacing);
-            if (!enableGridOnlyOverview() && !isStagingFocusedTask) {
-                longRowWidth += mLastComputedTaskSize.width() + mPageSpacing;
-            }
-
             float gapWidth = 0;
             if ((topRowLonger && dismissedTaskFromTop)
                     || (bottomRowLonger && dismissedTaskFromBottom)) {
@@ -3700,17 +3743,6 @@
             }
             if (gapWidth > 0) {
                 if (mClearAllShortTotalWidthTranslation == 0) {
-                    // Compensate the removed gap if we don't already have shortTotalCompensation,
-                    // and adjust accordingly to the new shortTotalCompensation after dismiss.
-                    int newClearAllShortTotalWidthTranslation = 0;
-                    if (longRowWidth < mLastComputedGridSize.width()) {
-                        DeviceProfile deviceProfile = mContainer.getDeviceProfile();
-                        newClearAllShortTotalWidthTranslation =
-                                (mIsRtl
-                                        ? mLastComputedTaskSize.right
-                                        : deviceProfile.widthPx - mLastComputedTaskSize.left)
-                                        - longRowWidth - deviceProfile.overviewGridSideMargin;
-                    }
                     float gapCompensation = gapWidth - newClearAllShortTotalWidthTranslation;
                     longGridRowWidthDiff += mIsRtl ? -gapCompensation : gapCompensation;
                 }
@@ -3792,104 +3824,41 @@
         SplitAnimationTimings splitTimings =
                 AnimUtils.getDeviceOverviewToSplitTimings(mContainer.getDeviceProfile().isTablet);
 
-        int distanceFromDismissedTask = 0;
+        int distanceFromDismissedTask = 1;
+        int stagingTranslation = 0;
+        if (isStagingFocusedTask || areAllDesktopTasksDismissed) {
+            int nextSnappedPage = isStagingFocusedTask
+                    ? indexOfChild(mUtils.getFirstSmallTaskView(getTaskViews()))
+                    : mUtils.getDesktopTaskViewCount(getTaskViews());
+            stagingTranslation = getPagedOrientationHandler().getPrimaryScroll(this)
+                    - getScrollForPage(nextSnappedPage);
+            stagingTranslation += mIsRtl ? newClearAllShortTotalWidthTranslation
+                    : -newClearAllShortTotalWidthTranslation;
+        }
         for (int i = 0; i < count; i++) {
             View child = getChildAt(i);
             if (child == dismissedTaskView) {
-                if (animateTaskView) {
-                    if (dismissingForSplitSelection) {
-                        createInitialSplitSelectAnimation(anim);
-                    } else {
-                        addDismissedTaskAnimations(dismissedTaskView, duration, anim);
-                    }
+                if (animateTaskView && !dismissingForSplitSelection) {
+                    addDismissedTaskAnimations(dismissedTaskView, duration, anim);
                 }
-            } else if (!showAsGrid) {
-                // Compute scroll offsets from task dismissal for animation.
-                // If we just take newScroll - oldScroll, everything to the right of dragged task
-                // translates to the left. We need to offset this in some cases:
-                // - In RTL, add page offset to all pages, since we want pages to move to the right
-                // Additionally, add a page offset if:
-                // - Current page is rightmost page (leftmost for RTL)
-                // - Dragging an adjacent page on the left side (right side for RTL)
-                int offset = mIsRtl ? scrollDiffPerPage : 0;
-                if (mCurrentPage == dismissedIndex) {
-                    int lastPage = taskCount - 1;
-                    if (mCurrentPage == lastPage) {
-                        offset += mIsRtl ? -scrollDiffPerPage : scrollDiffPerPage;
-                    }
-                } else {
-                    // Dismissing an adjacent page.
-                    int negativeAdjacent = mCurrentPage - 1; // (Right in RTL, left in LTR)
-                    if (dismissedIndex == negativeAdjacent) {
-                        offset += mIsRtl ? -scrollDiffPerPage : scrollDiffPerPage;
-                    }
-                }
-
+            } else if (!showAsGrid || (enableLargeDesktopWindowingTile()
+                    && dismissedTaskView != null && dismissedTaskView.isLargeTile()
+                    && nextFocusedTaskView == null && !dismissingForSplitSelection)) {
+                int offset = getOffsetToDismissedTask(scrollDiffPerPage, dismissedIndex, taskCount);
                 int scrollDiff = newScroll[i] - oldScroll[i] + offset;
                 if (scrollDiff != 0) {
-                    FloatProperty translationProperty = child instanceof TaskView
-                            ? ((TaskView) child).getPrimaryDismissTranslationProperty()
-                            : getPagedOrientationHandler().getPrimaryViewTranslate();
-
-                    float additionalDismissDuration =
-                            ADDITIONAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET * Math.abs(
-                                    i - dismissedIndex);
-
-                    // We are in non-grid layout.
-                    // If dismissing for split select, use split timings.
-                    // If not, use dismiss timings.
-                    float animationStartProgress = isSplitSelectionActive()
-                            ? Utilities.boundToRange(splitTimings.getGridSlideStartOffset(), 0f, 1f)
-                            : Utilities.boundToRange(
-                                    INITIAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET
-                                            + additionalDismissDuration, 0f, 1f);
-
-                    float animationEndProgress = isSplitSelectionActive()
-                            ? Utilities.boundToRange(splitTimings.getGridSlideStartOffset()
-                            + splitTimings.getGridSlideDurationOffset(), 0f, 1f)
-                            : 1f;
-
-                    // Slide tiles in horizontally to fill dismissed area
-                    anim.setFloat(child, translationProperty, scrollDiff,
-                            clampToProgress(
-                                    splitTimings.getGridSlidePrimaryInterpolator(),
-                                    animationStartProgress,
-                                    animationEndProgress
-                            )
-                    );
-
-                    if (mEnableDrawingLiveTile && child instanceof TaskView
-                            && ((TaskView) child).isRunningTask()) {
-                        anim.addOnFrameCallback(() -> {
-                            runActionOnRemoteHandles(
-                                    remoteTargetHandle ->
-                                            remoteTargetHandle.getTaskViewSimulator()
-                                                    .taskPrimaryTranslation.value =
-                                                    getPagedOrientationHandler().getPrimaryValue(
-                                                            child.getTranslationX(),
-                                                            child.getTranslationY()
-                                                    ));
-                            redrawLiveTile();
-                        });
-                    }
+                    translateTaskWhenDismissed(
+                            child,
+                            Math.abs(i - dismissedIndex),
+                            scrollDiff,
+                            anim,
+                            splitTimings);
                     needsCurveUpdates = true;
                 }
-            } else if (child instanceof TaskView) {
-                TaskView taskView = (TaskView) child;
-                if (isFocusedTaskDismissed) {
-                    if (nextFocusedTaskView != null &&
-                            !isSameGridRow(taskView, nextFocusedTaskView)) {
-                        continue;
-                    }
-                } else {
-                    if (i < dismissedIndex || !isSameGridRow(taskView, dismissedTaskView)) {
-                        continue;
-                    }
-                }
+            } else if (child instanceof TaskView taskView) {
                 // Animate task with index >= dismissed index and in the same row as the
                 // dismissed index or next focused index. Offset successive task dismissal
                 // durations for a staggered effect.
-                distanceFromDismissedTask++;
                 int staggerColumn = isStagingFocusedTask
                         ? (int) Math.ceil(distanceFromDismissedTask / 2f)
                         : distanceFromDismissedTask;
@@ -3916,16 +3885,15 @@
                         : dismissTranslationInterpolationEnd;
                 Interpolator dismissInterpolator = isStagingFocusedTask ? OVERSHOOT_0_75 : LINEAR;
 
+                float primaryTranslation = 0;
                 if (taskView == nextFocusedTaskView) {
                     // Enlarge the task to be focused next, and translate into focus position.
                     float scale = mTaskWidth / (float) mLastComputedGridTaskSize.width();
                     anim.setFloat(taskView, TaskView.DISMISS_SCALE, scale,
                             clampToProgress(LINEAR, animationStartProgress,
                                     dismissTranslationInterpolationEnd));
-                    anim.setFloat(taskView, taskView.getPrimaryDismissTranslationProperty(),
-                            mIsRtl ? dismissedTaskWidth : -dismissedTaskWidth,
-                            clampToProgress(LINEAR, animationStartProgress,
-                                    dismissTranslationInterpolationEnd));
+                    primaryTranslation += dismissedTaskWidth;
+                    animationEndProgress = dismissTranslationInterpolationEnd;
                     float secondaryTranslation = -mTaskGridVerticalDiff;
                     if (!nextFocusedTaskFromTop) {
                         secondaryTranslation -= mTopBottomRowHeightDiff;
@@ -3935,25 +3903,27 @@
                                     dismissTranslationInterpolationEnd));
                     anim.add(taskView.getFocusTransitionScaleAndDimOutAnimator(),
                             clampToProgress(LINEAR, 0f, ANIMATION_DISMISS_PROGRESS_MIDPOINT));
-                } else {
-                    float primaryTranslation =
+                } else if ((isFocusedTaskDismissed && nextFocusedTaskView != null && isSameGridRow(
+                        taskView, nextFocusedTaskView))
+                        || (!isFocusedTaskDismissed && i >= dismissedIndex && isSameGridRow(
+                        taskView, dismissedTaskView))) {
+                    primaryTranslation +=
                             nextFocusedTaskView != null ? nextFocusedTaskWidth : dismissedTaskWidth;
-                    if (isStagingFocusedTask) {
-                        // Moves less if focused task is not in scroll position.
-                        int focusedTaskScroll = getScrollForPage(dismissedIndex);
-                        int primaryScroll = getPagedOrientationHandler().getPrimaryScroll(this);
-                        int focusedTaskScrollDiff = primaryScroll - focusedTaskScroll;
-                        primaryTranslation +=
-                                mIsRtl ? focusedTaskScrollDiff : -focusedTaskScrollDiff;
-                    }
+                }
+                primaryTranslation += mIsRtl ? stagingTranslation : -stagingTranslation;
 
+                if (primaryTranslation != 0) {
                     anim.setFloat(taskView, taskView.getPrimaryDismissTranslationProperty(),
                             mIsRtl ? primaryTranslation : -primaryTranslation,
                             clampToProgress(dismissInterpolator, animationStartProgress,
                                     animationEndProgress));
+                    distanceFromDismissedTask++;
                 }
             }
         }
+        if (dismissingForSplitSelection) {
+            createInitialSplitSelectAnimation(anim);
+        }
 
         if (needsCurveUpdates) {
             anim.addOnFrameCallback(this::updateCurveProperties);
@@ -3962,7 +3932,7 @@
         // Add a tiny bit of translation Z, so that it draws on top of other views. This is relevant
         // (e.g.) when we dismiss a task by sliding it upward: if there is a row of icons above, we
         // want the dragged task to stay above all other views.
-        if (animateTaskView) {
+        if (animateTaskView && dismissedTaskView != null) {
             dismissedTaskView.setTranslationZ(0.1f);
         }
 
@@ -3971,12 +3941,13 @@
         final boolean finalCloseGapBetweenClearAll = closeGapBetweenClearAll;
         final boolean finalSnapToLastTask = snapToLastTask;
         final boolean finalIsFocusedTaskDismissed = isFocusedTaskDismissed;
-        mPendingAnimation.addEndListener(new Consumer<Boolean>() {
+        mPendingAnimation.addEndListener(new Consumer<>() {
             @Override
             public void accept(Boolean success) {
-                if (mEnableDrawingLiveTile && dismissedTaskView.isRunningTask() && success) {
+                if (mEnableDrawingLiveTile && dismissedTaskView != null
+                        && dismissedTaskView.isRunningTask() && success) {
                     finishRecentsAnimation(true /* toRecents */, false /* shouldPip */,
-                            () -> onEnd(success));
+                            () -> onEnd(true));
                 } else {
                     onEnd(success);
                 }
@@ -3990,12 +3961,12 @@
 
                 if (success) {
                     mAnyTaskHasBeenDismissed = true;
-                    if (shouldRemoveTask) {
+                    if (shouldRemoveTask && dismissedTaskView != null) {
                         if (dismissedTaskView.isRunningTask()) {
                             finishRecentsAnimation(true /* toRecents */, false /* shouldPip */,
-                                    () -> removeTaskInternal(dismissedTaskViewId));
+                                    () -> removeTaskInternal(dismissedTaskView));
                         } else {
-                            removeTaskInternal(dismissedTaskViewId);
+                            removeTaskInternal(dismissedTaskView);
                         }
                         announceForAccessibility(
                                 getResources().getString(R.string.task_view_closed));
@@ -4146,6 +4117,14 @@
                                 // If snapping to last task, find the last task after dismissal.
                                 pageToSnapTo = indexOfChild(
                                         getLastGridTaskView(topRowIdArray, bottomRowIdArray));
+
+                                if (pageToSnapTo == INVALID_PAGE) {
+                                    // Snap to latest large tile page after dismissing the
+                                    // last grid task. This will prevent snapping to page 0 when
+                                    // desktop task is visible as large tile.
+                                    pageToSnapTo = indexOfChild(
+                                            mUtils.getLastLargeTaskView(getTaskViews()));
+                                }
                             } else if (taskViewIdToSnapTo != -1) {
                                 // If snapping to another page due to indices rearranging, find
                                 // the new index after dismissal & rearrange using the task view id.
@@ -4179,6 +4158,90 @@
     }
 
     /**
+     * Compute scroll offsets from task dismissal for animation.
+     * If we just take newScroll - oldScroll, everything to the right of dragged task
+     * translates to the left. We need to offset this in some cases:
+     * - In RTL, add page offset to all pages, since we want pages to move to the right
+     * Additionally, add a page offset if:
+     * - Current page is rightmost page (leftmost for RTL)
+     * - Dragging an adjacent page on the left side (right side for RTL)
+     */
+    private int getOffsetToDismissedTask(int scrollDiffPerPage, int dismissedIndex, int taskCount) {
+        // When mCurrentPage is ClearAllButton, use the last TaskView instead to calculate
+        // offset.
+        int currentPage = mCurrentPage == taskCount ? taskCount - 1 : mCurrentPage;
+        int offset = mIsRtl ? scrollDiffPerPage : 0;
+        if (currentPage == dismissedIndex) {
+            int lastPage = taskCount - 1;
+            if (currentPage == lastPage) {
+                offset += mIsRtl ? -scrollDiffPerPage : scrollDiffPerPage;
+            }
+        } else {
+            // Dismissing an adjacent page.
+            int negativeAdjacent = currentPage - 1; // (Right in RTL, left in LTR)
+            if (dismissedIndex == negativeAdjacent) {
+                offset += mIsRtl ? -scrollDiffPerPage : scrollDiffPerPage;
+            }
+        }
+        return offset;
+    }
+
+    private void translateTaskWhenDismissed(
+            View view,
+            int indexDiff,
+            int scrollDiffPerPage,
+            PendingAnimation pendingAnimation,
+            SplitAnimationTimings splitTimings) {
+        FloatProperty translationProperty = view instanceof TaskView
+                ? ((TaskView) view).getPrimaryDismissTranslationProperty()
+                : getPagedOrientationHandler().getPrimaryViewTranslate();
+
+        float additionalDismissDuration =
+                ADDITIONAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET * indexDiff;
+
+        // We are in non-grid layout.
+        // If dismissing for split select, use split timings.
+        // If not, use dismiss timings.
+        float animationStartProgress = isSplitSelectionActive()
+                ? Utilities.boundToRange(splitTimings.getGridSlideStartOffset(), 0f, 1f)
+                : Utilities.boundToRange(
+                        INITIAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET
+                                + additionalDismissDuration, 0f, 1f);
+
+        float animationEndProgress = isSplitSelectionActive()
+                ? Utilities.boundToRange(splitTimings.getGridSlideStartOffset()
+                + splitTimings.getGridSlideDurationOffset(), 0f, 1f)
+                : 1f;
+
+        // Slide tiles in horizontally to fill dismissed area
+        pendingAnimation.setFloat(
+                view,
+                translationProperty,
+                scrollDiffPerPage,
+                clampToProgress(
+                        splitTimings.getGridSlidePrimaryInterpolator(),
+                        animationStartProgress,
+                        animationEndProgress
+                )
+        );
+
+        if (mEnableDrawingLiveTile && view instanceof TaskView
+                && ((TaskView) view).isRunningTask()) {
+            pendingAnimation.addOnFrameCallback(() -> {
+                runActionOnRemoteHandles(
+                        remoteTargetHandle ->
+                                remoteTargetHandle.getTaskViewSimulator()
+                                        .taskPrimaryTranslation.value =
+                                        getPagedOrientationHandler().getPrimaryValue(
+                                                view.getTranslationX(),
+                                                view.getTranslationY()
+                                        ));
+                redrawLiveTile();
+            });
+        }
+    }
+
+    /**
      * Hides all overview actions if user is halfway through split selection, shows otherwise.
      * We only show split option if:
      * * Focused view is a single app
@@ -4271,17 +4334,24 @@
         return lastVisibleIndex;
     }
 
-    private void removeTaskInternal(int dismissedTaskViewId) {
-        int[] taskIds = getTaskIdsForTaskViewId(dismissedTaskViewId);
-        UI_HELPER_EXECUTOR.getHandler().post(
-                () -> {
-                    for (int taskId : taskIds) {
-                        if (taskId != -1) {
-                            ActivityManagerWrapper.getInstance().removeTask(taskId);
-                        }
-                    }
-                });
-    }
+  private void removeTaskInternal(@NonNull TaskView dismissedTaskView) {
+    UI_HELPER_EXECUTOR
+        .getHandler()
+        .post(
+            () -> {
+              if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()
+                  && dismissedTaskView instanceof DesktopTaskView) {
+                // TODO: b/362720497 - Use the api with desktop id instead.
+                SystemUiProxy.INSTANCE
+                    .get(getContext())
+                    .removeDesktop(mContainer.getDisplay().getDisplayId());
+              } else {
+                for (int taskId : dismissedTaskView.getTaskIds()) {
+                    ActivityManagerWrapper.getInstance().removeTask(taskId);
+                }
+              }
+            });
+  }
 
     protected void onDismissAnimationEnds() {
         AccessibilityManagerCompat.sendTestProtocolEventToTest(getContext(),
@@ -4539,6 +4609,20 @@
     }
 
     @Nullable
+    public TaskView getPreviousTaskView() {
+        return getTaskViewAt(getRunningTaskIndex() - 1);
+    }
+
+    @Nullable
+    public TaskView getLastLargeTaskView() {
+        return mUtils.getLastLargeTaskView(getTaskViews());
+    }
+
+    public int getLargeTilesCount() {
+        return mUtils.getLargeTileCount(getTaskViews());
+    }
+
+    @Nullable
     public TaskView getCurrentPageTaskView() {
         return getTaskViewAt(getCurrentPage());
     }
@@ -4673,6 +4757,10 @@
                 ? (runningTask == null ? INVALID_PAGE : indexOfChild(runningTask))
                 : mOffsetMidpointIndexOverride;
         int modalMidpoint = getCurrentPage();
+        TaskView carouselHiddenMidpointTask = runningTask != null ? runningTask
+                : mUtils.getFirstTaskViewInCarousel(/*nonRunningTaskCarouselHidden=*/true,
+                        getTaskViews(), null);
+        int carouselHiddenMidpoint = indexOfChild(carouselHiddenMidpointTask);
         boolean shouldCalculateOffsetForAllTasks = showAsGrid
                 && (enableGridOnlyOverview() || enableLargeDesktopWindowingTile())
                 && mTaskModalness > 0;
@@ -4692,6 +4780,7 @@
         float modalLeftOffsetSize = 0;
         float modalRightOffsetSize = 0;
         float gridOffsetSize = 0;
+        float carouselHiddenOffsetSize = 0;
 
         if (showAsGrid) {
             // In grid, we only focus the task on the side. The reference index used for offset
@@ -4709,7 +4798,10 @@
                     : 0;
         }
 
+        int primarySize = getPagedOrientationHandler().getPrimaryValue(getWidth(), getHeight());
+        float maxOverscroll = primarySize * OverScroll.OVERSCROLL_DAMP_FACTOR;
         for (int i = 0; i < count; i++) {
+            View child = getChildAt(i);
             float translation = i == midpoint
                     ? midpointOffsetSize
                     : i < midpoint
@@ -4719,16 +4811,31 @@
                 gridOffsetSize = getHorizontalOffsetSize(i, modalMidpoint, modalOffset);
                 gridOffsetSize = Math.abs(gridOffsetSize) * (i <= modalMidpoint ? 1 : -1);
             }
+            if (enableLargeDesktopWindowingTile()) {
+                if (child instanceof TaskView
+                        && !mUtils.isVisibleInCarousel((TaskView) child,
+                        runningTask, /*nonRunningTaskCarouselHidden=*/true)) {
+                    // Increment carouselHiddenOffsetSize by maxOverscroll so it won't be on screen
+                    // even when user overscroll.
+                    carouselHiddenOffsetSize = (Math.abs(getMaxHorizontalOffsetSize(i,
+                            carouselHiddenMidpoint)) + maxOverscroll)
+                            * mDesktopCarouselDetachProgress;
+                    carouselHiddenOffsetSize = carouselHiddenOffsetSize * (
+                            i <= carouselHiddenMidpoint ? 1 : -1);
+                } else {
+                    carouselHiddenOffsetSize = 0;
+                }
+            }
             float modalTranslation = i == modalMidpoint
                     ? modalMidpointOffsetSize
                     : showAsGrid
                             ? gridOffsetSize
                             : i < modalMidpoint ? modalLeftOffsetSize : modalRightOffsetSize;
-            View child = getChildAt(i);
             boolean skipTranslationOffset = enableDesktopTaskAlphaAnimation()
                     && i == getRunningTaskIndex()
                     && child instanceof DesktopTaskView;
-            float totalTranslationX = (skipTranslationOffset ? 0f : translation) + modalTranslation;
+            float totalTranslationX = (skipTranslationOffset ? 0f : translation) + modalTranslation
+                    + carouselHiddenOffsetSize;
             FloatProperty translationPropertyX = child instanceof TaskView
                     ? ((TaskView) child).getPrimaryTaskOffsetTranslationProperty()
                     : getPagedOrientationHandler().getPrimaryViewTranslate();
@@ -4785,6 +4892,14 @@
             return 0;
         }
 
+        return getMaxHorizontalOffsetSize(childIndex, midpointIndex) * offsetProgress;
+    }
+
+    /**
+     * Computes the distance to offset the given child such that it is completely offscreen when
+     * translating away from the given midpoint.
+     */
+    private float getMaxHorizontalOffsetSize(int childIndex, int midpointIndex) {
         // First, get the position of the task relative to the midpoint. If there is no midpoint
         // then we just use the normal (centered) task position.
         RectF taskPosition = mTempRectF;
@@ -4844,7 +4959,7 @@
             }
             distanceToOffscreen -= mLastComputedTaskEndPushOutDistance;
         }
-        return distanceToOffscreen * offsetProgress;
+        return distanceToOffscreen;
     }
 
     /**
@@ -4942,7 +5057,6 @@
         mSplitSelectStateController.setAnimateCurrentTaskDismissal(
                 true /*animateCurrentTaskDismissal*/);
         mSplitHiddenTaskViewIndex = indexOfChild(taskView);
-        updateDesktopTaskVisibility(false /* visible */);
     }
 
     /**
@@ -4956,7 +5070,9 @@
         mSplitHiddenTaskView = getTaskViewByTaskId(splitSelectSource.alreadyRunningTaskId);
         mSplitHiddenTaskViewIndex = indexOfChild(mSplitHiddenTaskView);
         mSplitSelectStateController
-                .setAnimateCurrentTaskDismissal(splitSelectSource.animateCurrentTaskDismissal);
+                .setAnimateCurrentTaskDismissal(splitSelectSource.animateCurrentTaskDismissal
+                        && mSplitHiddenTaskView != null
+                        && !(mSplitHiddenTaskView instanceof DesktopTaskView));
 
         // Prevent dismissing whole task if we're only initiating from one of 2 tasks in split pair
         mSplitSelectStateController.setDismissingFromSplitPair(mSplitHiddenTaskView != null
@@ -4964,12 +5080,35 @@
         mSplitSelectStateController.setInitialTaskSelect(splitSelectSource.intent,
                 splitSelectSource.position.stagePosition, splitSelectSource.getItemInfo(),
                 splitSelectSource.splitEvent, splitSelectSource.alreadyRunningTaskId);
-        updateDesktopTaskVisibility(false /* visible */);
     }
 
-    private void updateDesktopTaskVisibility(boolean visible) {
-        if (mDesktopTaskView != null) {
-            mDesktopTaskView.setVisibility(visible ? VISIBLE : GONE);
+    /**
+     * Animate DesktopTaskView(s) to hide in split select
+     */
+    public void handleDesktopTaskInSplitSelectState(PendingAnimation builder,
+            Interpolator deskTopFadeInterPolator) {
+        if (enableLargeDesktopWindowingTile()) {
+            for (TaskView taskView : getTaskViews()) {
+                if (taskView instanceof DesktopTaskView) {
+                    // Correcting the animation for split mode since we hide DW in split.
+                    builder.addFloat(taskView.getSplitAlphaProperty(),
+                            MULTI_PROPERTY_VALUE, 1f, 0f,
+                            clampToProgress(deskTopFadeInterPolator, 0f, 0.1f));
+                }
+            }
+        }
+    }
+
+    /**
+     * While exiting from split mode, show all existing DesktopTaskViews.
+     */
+    public void resetDesktopTaskFromSplitSelectState() {
+        if (enableLargeDesktopWindowingTile()) {
+            for (TaskView taskView : getTaskViews()) {
+                if (taskView instanceof DesktopTaskView) {
+                    taskView.setSplitAlpha(1f);
+                }
+            }
         }
     }
 
@@ -5012,7 +5151,15 @@
                     true /* dismissingForSplitSelection*/);
         } else {
             // Splitting from Home
-            createInitialSplitSelectAnimation(builder);
+            TaskView currentPageTaskView = getTaskViewAt(mCurrentPage);
+            // When current page is a Desktop task it needs special handling to
+            // display correct animation in split mode
+            if (currentPageTaskView instanceof DesktopTaskView) {
+                createTaskDismissAnimation(builder, null, true, false, duration,
+                        true /* dismissingForSplitSelection*/);
+            } else {
+                createInitialSplitSelectAnimation(builder);
+            }
         }
     }
 
@@ -5178,9 +5325,15 @@
         mSplitHiddenTaskViewIndex = -1;
         if (mSplitHiddenTaskView != null) {
             mSplitHiddenTaskView.setThumbnailVisibility(VISIBLE, INVALID_TASK_ID);
+            // mSplitHiddenTaskView is set when split select animation starts. The TaskView is only
+            // removed when when the animation finishes. So in the case of overview being dismissed
+            // during the animation, we should not call clearAndRecycleTaskView() because it has
+            // not been removed yet.
+            if (mSplitHiddenTaskView.getParent() == null) {
+                clearAndRecycleTaskView(mSplitHiddenTaskView);
+            }
             mSplitHiddenTaskView = null;
         }
-        updateDesktopTaskVisibility(true /* visible */);
     }
 
     private void safeRemoveDragLayerView(@Nullable View viewToRemove) {
@@ -5338,6 +5491,18 @@
                             mTempPointF);
                     setPivotX(mTempPointF.x);
                     setPivotY(mTempPointF.y);
+
+                    // If live tile is not launching, apply pivot to live tile as well and bring it
+                    // above RecentsView to avoid wallpaper blur from being applied to it.
+                    if (!taskView.isRunningTask()) {
+                        runActionOnRemoteHandles(
+                                remoteTargetHandle -> {
+                                    remoteTargetHandle.getTaskViewSimulator().setPivotOverride(
+                                            mTempPointF);
+                                    remoteTargetHandle.getTaskViewSimulator().setDrawsBelowRecents(
+                                            false);
+                                });
+                    }
                 }
             });
         } else if (!showAsGrid) {
@@ -5454,15 +5619,13 @@
                 remoteTargetHandle -> remoteTargetHandle.getTaskViewSimulator()
                         .addOverviewToAppAnim(mPendingAnimation, interpolator));
         mPendingAnimation.addOnFrameCallback(this::redrawLiveTile);
-        if (taskView instanceof DesktopTaskView && mRemoteTargetHandles != null) {
-            mPendingAnimation.addListener(new AnimatorListenerAdapter() {
-                @Override
-                public void onAnimationStart(Animator animation) {
-                    runActionOnRemoteHandles(remoteTargetHandle ->
-                            remoteTargetHandle.getTaskViewSimulator().setDrawsBelowRecents(false));
-                }
-            });
-        }
+        mPendingAnimation.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationStart(Animator animation) {
+                runActionOnRemoteHandles(remoteTargetHandle ->
+                        remoteTargetHandle.getTaskViewSimulator().setDrawsBelowRecents(false));
+            }
+        });
         mPendingAnimation.addEndListener(isSuccess -> {
             if (isSuccess) {
                 if (taskView instanceof GroupedTaskView && hasAllValidTaskIds(taskView.getTaskIds())
@@ -5494,6 +5657,13 @@
     protected Unit onTaskLaunchAnimationEnd(boolean success) {
         if (success) {
             resetTaskVisuals();
+        } else {
+            // If launch animation didn't complete i.e. user dragged live tile down and then
+            // back up and returned to Overview, then we need to ensure we reset the
+            // view to draw below recents so that it can't be interacted with.
+            runActionOnRemoteHandles(remoteTargetHandle ->
+                    remoteTargetHandle.getTaskViewSimulator().setDrawsBelowRecents(true));
+            redrawLiveTile();
         }
         return Unit.INSTANCE;
     }
@@ -5806,16 +5976,18 @@
     private int getFirstViewIndex() {
         final TaskView firstView;
         if (mShowAsGridLastOnLayout) {
-            // For grid Overivew, it always start if a large tile (focused task or desktop task) if
+            // For grid Overview, it always start if a large tile (focused task or desktop task) if
             // they exist, otherwise it start with the first task.
-            TaskView firstLargeTaskView = mUtils.getFirstLargeTaskView(getTaskViews());
+            TaskView firstLargeTaskView = mUtils.getFirstLargeTaskView(getTaskViews(),
+                    isSplitSelectionActive());
             if (firstLargeTaskView != null) {
                 firstView = firstLargeTaskView;
             } else {
-                firstView = getTaskViewAt(0);
+                firstView = mUtils.getFirstSmallTaskView(getTaskViews());
             }
         } else {
-            firstView = mUtils.getFirstTaskViewInCarousel(mNonRunningTaskCategoryHidden,
+            firstView = mUtils.getFirstTaskViewInCarousel(
+                    /*nonRunningTaskCarouselHidden=*/mDesktopCarouselDetachProgress > 0,
                     getTaskViews(), getRunningTaskView());
         }
         return indexOfChild(firstView);
@@ -5836,7 +6008,8 @@
                 lastView = mUtils.getLastLargeTaskView(getTaskViews());
             }
         } else {
-            lastView = mUtils.getLastTaskViewInCarousel(mNonRunningTaskCategoryHidden,
+            lastView = mUtils.getLastTaskViewInCarousel(
+                    /*nonRunningTaskCarouselHidden=*/mDesktopCarouselDetachProgress > 0,
                     getTaskViews(), getRunningTaskView());
         }
         return indexOfChild(lastView);
@@ -6552,6 +6725,26 @@
         successCallback.run();
     }
 
+    /**
+     * Move the provided task into external display and invoke {@code successCallback} if succeeded.
+     */
+    public void moveTaskToExternalDisplay(TaskContainer taskContainer, Runnable successCallback) {
+        if (!DesktopModeStatus.canEnterDesktopMode(mContext)) {
+            return;
+        }
+        switchToScreenshot(() -> finishRecentsAnimation(/* toRecents= */true, /* shouldPip= */false,
+                () -> moveTaskToDesktopInternal(taskContainer, successCallback)));
+    }
+
+    private void moveTaskToDesktopInternal(TaskContainer taskContainer, Runnable successCallback) {
+        if (mDesktopRecentsTransitionController == null) {
+            return;
+        }
+        mDesktopRecentsTransitionController.moveToExternalDisplay(taskContainer.getTask().key.id);
+        successCallback.run();
+    }
+
+
     // Logs when the orientation of Overview changes. We log both real and fake orientation changes.
     private void logOrientationChanged() {
         // Only log when Overview is showing.
diff --git a/quickstep/src/com/android/quickstep/views/RecentsViewContainer.java b/quickstep/src/com/android/quickstep/views/RecentsViewContainer.java
index 8f19444..b04753b 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsViewContainer.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsViewContainer.java
@@ -16,7 +16,6 @@
 
 package com.android.quickstep.views;
 
-import android.app.Activity;
 import android.content.Context;
 import android.content.ContextWrapper;
 import android.content.LocusId;
@@ -26,14 +25,16 @@
 import android.view.View;
 import android.view.Window;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.BaseActivity;
 import com.android.launcher3.logger.LauncherAtom;
 import com.android.launcher3.statehandlers.DesktopVisibilityController;
-import com.android.launcher3.util.SystemUiController;
+import com.android.launcher3.taskbar.TaskbarUIController;
 import com.android.launcher3.views.ActivityContext;
 import com.android.launcher3.views.ScrimView;
+import com.android.quickstep.util.TISBindHelper;
 
 /**
  * Interface to be implemented by the parent view of RecentsView
@@ -44,7 +45,7 @@
      * Returns an instance of an implementation of RecentsViewContainer
      * @param context will find instance of recentsViewContainer from given context.
      */
-    static <T extends RecentsViewContainer> T containerFromContext(Context context) {
+    static <T extends Context & RecentsViewContainer> T containerFromContext(Context context) {
         if (context instanceof RecentsViewContainer) {
             return (T) context;
         } else if (context instanceof ContextWrapper) {
@@ -55,11 +56,6 @@
     }
 
     /**
-     * Returns {@link SystemUiController} to manage various window flags to control system UI.
-     */
-    SystemUiController getSystemUiController();
-
-    /**
      * Returns {@link ScrimView}
      */
     ScrimView getScrimView();
@@ -95,7 +91,7 @@
     /**
      * Returns overview actions view as a view
      */
-    View getActionsView();
+    OverviewActionsView getActionsView();
 
     /**
      * @see BaseActivity#addForceInvisibleFlag(int)
@@ -143,12 +139,6 @@
     void runOnBindToTouchInteractionService(Runnable r);
 
     /**
-     * @see Activity#getWindow()
-     * @return Window
-     */
-    Window getWindow();
-
-    /**
      * @see
      * BaseActivity#addMultiWindowModeChangedListener(BaseActivity.MultiWindowModeChangedListener)
      * @param listener {@link BaseActivity.MultiWindowModeChangedListener}
@@ -177,6 +167,25 @@
     boolean isRecentsViewVisible();
 
     /**
+     * Begins transition to start home through container
+     */
+    default void startHome(){
+        // no op
+    }
+
+    /**
+     * Checks container to see if we can start home transition safely
+     */
+    boolean canStartHomeSafely();
+
+
+    /**
+     * Enter staged split directly from the current running app.
+     * @param leftOrTop if the staged split will be positioned left or top.
+     */
+    default void enterStageSplitFromRunningApp(boolean leftOrTop){}
+
+    /**
      * Overwrites any logged item in Launcher that doesn't have a container with the
      * {@link com.android.launcher3.touch.PagedOrientationHandler} in use for Overview.
      *
@@ -204,4 +213,10 @@
 
     @Nullable
     DesktopVisibilityController getDesktopVisibilityController();
+
+    void setTaskbarUIController(@Nullable TaskbarUIController taskbarUIController);
+
+    @Nullable TaskbarUIController getTaskbarUIController();
+
+    @NonNull TISBindHelper getTISBindHelper();
 }
diff --git a/quickstep/src/com/android/quickstep/views/RecentsViewModelHelper.kt b/quickstep/src/com/android/quickstep/views/RecentsViewModelHelper.kt
index f22c672..3616fbb 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsViewModelHelper.kt
+++ b/quickstep/src/com/android/quickstep/views/RecentsViewModelHelper.kt
@@ -25,6 +25,7 @@
 import kotlinx.coroutines.SupervisorJob
 import kotlinx.coroutines.cancel
 import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
 
 /** Helper for [RecentsView] to interact with the [RecentsViewModel]. */
 class RecentsViewModelHelper(private val recentsViewModel: RecentsViewModel) {
@@ -32,7 +33,7 @@
 
     fun onAttachedToWindow() {
         viewAttachedScope =
-            CoroutineScope(SupervisorJob() + Dispatchers.Main + CoroutineName("RecentsView"))
+            CoroutineScope(SupervisorJob() + Dispatchers.Default + CoroutineName("RecentsView"))
     }
 
     fun onDetachedFromWindow() {
@@ -50,7 +51,7 @@
         viewAttachedScope.launch {
             recentsViewModel.waitForRunningTaskShowScreenshotToUpdate()
             recentsViewModel.waitForThumbnailsToUpdate(updatedThumbnails)
-            ViewUtils.postFrameDrawn(taskView, onFinishRunnable)
+            withContext(Dispatchers.Main) { ViewUtils.postFrameDrawn(taskView, onFinishRunnable) }
         }
     }
 }
diff --git a/quickstep/src/com/android/quickstep/views/TaskContainer.kt b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
index 57d68a0..c940fb4 100644
--- a/quickstep/src/com/android/quickstep/views/TaskContainer.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
@@ -29,6 +29,7 @@
 import com.android.launcher3.util.TransformingTouchDelegate
 import com.android.quickstep.TaskOverlayFactory
 import com.android.quickstep.TaskUtils
+import com.android.quickstep.ViewUtils.addAccessibleChildToList
 import com.android.quickstep.recents.di.RecentsDependencies
 import com.android.quickstep.recents.di.get
 import com.android.quickstep.recents.di.getScope
@@ -56,7 +57,7 @@
     @SplitConfigurationOptions.StagePosition val stagePosition: Int,
     val digitalWellBeingToast: DigitalWellBeingToast?,
     val showWindowsView: View?,
-    taskOverlayFactory: TaskOverlayFactory
+    taskOverlayFactory: TaskOverlayFactory,
 ) {
     val overlay: TaskOverlayFactory.TaskOverlay<*> = taskOverlayFactory.createOverlay(this)
     lateinit var taskContainerData: TaskContainerData
@@ -109,7 +110,6 @@
             return snapshotView as TaskThumbnailViewDeprecated
         }
 
-    // TODO(b/334826842): Support shouldShowSplashView for new TTV.
     val shouldShowSplashView: Boolean
         get() =
             if (enableRefactorTaskThumbnail())
@@ -150,19 +150,20 @@
         if (enableRefactorTaskThumbnail()) {
             bindThumbnailView()
         } else {
-            thumbnailViewDeprecated.bind(task, overlay)
+            thumbnailViewDeprecated.bind(task, overlay, taskView)
         }
         overlay.init()
     }
 
     fun destroy() {
         digitalWellBeingToast?.destroy()
-        if (enableRefactorTaskThumbnail()) {
-            taskView.removeView(thumbnailView)
-        }
         snapshotView.scaleX = 1f
         snapshotView.scaleY = 1f
         overlay.destroy()
+        if (enableRefactorTaskThumbnail()) {
+            RecentsDependencies.getInstance().removeScope(snapshotView)
+            RecentsDependencies.getInstance().removeScope(this)
+        }
     }
 
     fun bindThumbnailView() {
@@ -181,12 +182,4 @@
         showWindowsView?.let { addAccessibleChildToList(it, outChildren) }
         digitalWellBeingToast?.let { addAccessibleChildToList(it, outChildren) }
     }
-
-    private fun addAccessibleChildToList(view: View, outChildren: ArrayList<View>) {
-        if (view.includeForAccessibility()) {
-            outChildren.add(view)
-        } else {
-            view.addChildrenForAccessibility(outChildren)
-        }
-    }
 }
diff --git a/quickstep/src/com/android/quickstep/views/TaskThumbnailViewDeprecated.java b/quickstep/src/com/android/quickstep/views/TaskThumbnailViewDeprecated.java
index 56ca043..5dbc2ef 100644
--- a/quickstep/src/com/android/quickstep/views/TaskThumbnailViewDeprecated.java
+++ b/quickstep/src/com/android/quickstep/views/TaskThumbnailViewDeprecated.java
@@ -110,6 +110,7 @@
     private TaskView.FullscreenDrawParams mFullscreenParams;
     private ImageView mSplashView;
     private Drawable mSplashViewDrawable;
+    private TaskView mTaskView;
 
     @Nullable
     private Task mTask;
@@ -153,10 +154,11 @@
     /**
      * Updates the thumbnail to draw the provided task
      */
-    public void bind(Task task, TaskOverlay<?> overlay) {
+    public void bind(Task task, TaskOverlay<?> overlay, TaskView taskView) {
         mOverlay = overlay;
         mOverlay.reset();
         mTask = task;
+        mTaskView = taskView;
         int color = task == null ? Color.BLACK : task.colorBackground | 0xFF000000;
         mPaint.setColor(color);
         mBackgroundPaint.setColor(color);
@@ -292,8 +294,8 @@
 
     public void drawOnCanvas(Canvas canvas, float x, float y, float width, float height,
             float cornerRadius) {
-        if (mTask != null && getTaskView().isRunningTask()
-                && !getTaskView().getShouldShowScreenshot()) {
+        if (mTask != null && mTaskView.isRunningTask()
+                && !mTaskView.getShouldShowScreenshot()) {
             canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mClearPaint);
             canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius,
                     mDimmingPaintAfterClearing);
@@ -334,10 +336,6 @@
         }
     }
 
-    public TaskView getTaskView() {
-        return (TaskView) getParent();
-    }
-
     public void setOverlayEnabled(boolean overlayEnabled) {
         if (mOverlayEnabled != overlayEnabled) {
             mOverlayEnabled = overlayEnabled;
@@ -390,9 +388,9 @@
         float viewCenterY = viewHeight / 2f;
         float centeredDrawableLeft = (viewWidth - drawableWidth) / 2f;
         float centeredDrawableTop = (viewHeight - drawableHeight) / 2f;
-        float nonGridScale = getTaskView() == null ? 1 : 1 / getTaskView().getNonGridScale();
-        float recentsMaxScale = getTaskView() == null || getTaskView().getRecentsView() == null
-                ? 1 : 1 / getTaskView().getRecentsView().getMaxScaleForFullScreen();
+        float nonGridScale = mTaskView == null ? 1 : 1 / mTaskView.getNonGridScale();
+        float recentsMaxScale = mTaskView == null || mTaskView.getRecentsView() == null
+                ? 1 : 1 / mTaskView.getRecentsView().getMaxScaleForFullScreen();
         float scaleX = nonGridScale * recentsMaxScale * (1 / getScaleX());
         float scaleY = nonGridScale * recentsMaxScale * (1 / getScaleY());
 
@@ -419,7 +417,7 @@
     }
 
     private boolean isThumbnailRotationDifferentFromTask() {
-        RecentsView recents = getTaskView().getRecentsView();
+        RecentsView recents = mTaskView.getRecentsView();
         if (recents == null || mThumbnailData == null) {
             return false;
         }
@@ -467,7 +465,7 @@
         if (mBitmapShader != null && mThumbnailData != null) {
             mPreviewRect.set(0, 0, mThumbnailData.getThumbnail().getWidth(),
                     mThumbnailData.getThumbnail().getHeight());
-            int currentRotation = getTaskView().getOrientedState().getRecentsActivityRotation();
+            int currentRotation = mTaskView.getOrientedState().getRecentsActivityRotation();
             boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
             mPreviewPositionHelper.updateThumbnailMatrix(mPreviewRect, mThumbnailData,
                     getMeasuredWidth(), getMeasuredHeight(), dp.isTablet, currentRotation, isRtl);
@@ -475,7 +473,7 @@
             mBitmapShader.setLocalMatrix(mPreviewPositionHelper.getMatrix());
             mPaint.setShader(mBitmapShader);
         }
-        getTaskView().updateCurrentFullscreenParams();
+        mTaskView.updateCurrentFullscreenParams();
         invalidate();
     }
 
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index 2ed6ae6..b1cb407 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -82,6 +82,7 @@
 import com.android.quickstep.orientation.RecentsPagedOrientationHandler
 import com.android.quickstep.recents.di.RecentsDependencies
 import com.android.quickstep.recents.di.get
+import com.android.quickstep.task.thumbnail.TaskThumbnailView
 import com.android.quickstep.task.viewmodel.TaskViewModel
 import com.android.quickstep.util.ActiveGestureErrorDetector
 import com.android.quickstep.util.ActiveGestureLog
@@ -402,6 +403,15 @@
         }
         get() = taskViewAlpha.get(ALPHA_INDEX_ATTACH).value
 
+    var splitAlpha
+        set(value) {
+            splitAlphaProperty.value = value
+        }
+        get() = splitAlphaProperty.value
+
+    val splitAlphaProperty: MultiPropertyFactory<View>.MultiProperty
+        get() = taskViewAlpha.get(ALPHA_INDEX_SPLIT)
+
     protected var shouldShowScreenshot = false
         get() = !isRunningTask || field
         private set
@@ -606,6 +616,7 @@
     override fun onRecycle() {
         resetPersistentViewTransforms()
         attachAlpha = 1f
+        splitAlpha = 1f
         // Clear any references to the thumbnail (it will be re-read either from the cache or the
         // system on next bind)
         if (!enableRefactorTaskThumbnail()) {
@@ -625,24 +636,29 @@
     override fun onInitializeAccessibilityNodeInfo(info: AccessibilityNodeInfo) {
         super.onInitializeAccessibilityNodeInfo(info)
         with(info) {
-            addAction(
-                AccessibilityAction(
-                    R.id.action_close,
-                    context.getText(R.string.accessibility_close),
+            // Only make actions available if the app icon menu is visible to the user.
+            // When modalness is >0, the user is in select mode and the icon menu is hidden.
+            if (modalness == 0f) {
+                addAction(
+                    AccessibilityAction(
+                        R.id.action_close,
+                        context.getText(R.string.accessibility_close),
+                    )
                 )
-            )
 
-            taskContainers.forEach {
-                TraceHelper.allowIpcs("TV.a11yInfo") {
-                    TaskOverlayFactory.getEnabledShortcuts(this@TaskView, it).forEach { shortcut ->
-                        addAction(shortcut.createAccessibilityAction(context))
+                taskContainers.forEach {
+                    TraceHelper.allowIpcs("TV.a11yInfo") {
+                        TaskOverlayFactory.getEnabledShortcuts(this@TaskView, it).forEach { shortcut
+                            ->
+                            addAction(shortcut.createAccessibilityAction(context))
+                        }
                     }
                 }
-            }
 
-            // Add DWB accessibility action at the end of the list
-            taskContainers.forEach {
-                it.digitalWellBeingToast?.getDWBAccessibilityAction()?.let(::addAction)
+                // Add DWB accessibility action at the end of the list
+                taskContainers.forEach {
+                    it.digitalWellBeingToast?.getDWBAccessibilityAction()?.let(::addAction)
+                }
             }
 
             recentsView?.let {
@@ -687,7 +703,6 @@
         orientedState: RecentsOrientedState,
         taskOverlayFactory: TaskOverlayFactory,
     ) {
-
         cancelPendingLoadTasks()
         taskContainers =
             listOf(
@@ -714,19 +729,23 @@
         @StagePosition stagePosition: Int,
         taskOverlayFactory: TaskOverlayFactory,
     ): TaskContainer {
-        val thumbnailViewDeprecated: TaskThumbnailViewDeprecated = findViewById(thumbnailViewId)!!
+        val existingThumbnailView: View = findViewById(thumbnailViewId)!!
         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)
+            when {
+                !enableRefactorTaskThumbnail() -> existingThumbnailView
+                existingThumbnailView is TaskThumbnailView -> existingThumbnailView
+                else -> {
+                    val indexOfSnapshotView = indexOfChild(existingThumbnailView)
+                    LayoutInflater.from(context)
+                        .inflate(R.layout.task_thumbnail, this, false)
+                        .also {
+                            it.id = thumbnailViewId
+                            addView(it, indexOfSnapshotView, existingThumbnailView.layoutParams)
+                            removeView(existingThumbnailView)
+                        }
                 }
-            } else {
-                thumbnailViewDeprecated
             }
         val iconView = getOrInflateIconView(iconViewId)
-        val digitalWellBeingToast = findViewById<DigitalWellBeingToast>(digitalWellbeingBannerId)!!
         return TaskContainer(
             this,
             task,
@@ -734,7 +753,7 @@
             iconView,
             TransformingTouchDelegate(iconView.asView()),
             stagePosition,
-            digitalWellBeingToast,
+            findViewById(digitalWellbeingBannerId)!!,
             findViewById(showWindowViewId)!!,
             taskOverlayFactory,
         )
@@ -1202,8 +1221,6 @@
                     if (isQuickSwitch) {
                         setFreezeRecentTasksReordering()
                     }
-                    // TODO(b/334826842) no work required - add splash functionality to new TTV -
-                    // cold start e.g. restart device. Small splash moving to bigger splash
                     disableStartingWindow = firstContainer.shouldShowSplashView
                 }
         Executors.UI_HELPER_EXECUTOR.execute {
@@ -1687,8 +1704,9 @@
 
         private const val ALPHA_INDEX_STABLE = 0
         private const val ALPHA_INDEX_ATTACH = 1
+        private const val ALPHA_INDEX_SPLIT = 2
 
-        private const val NUM_ALPHA_CHANNELS = 2
+        private const val NUM_ALPHA_CHANNELS = 3
 
         /** The maximum amount that a task view can be scrimmed, dimmed or tinted. */
         const val MAX_PAGE_SCRIM_ALPHA = 0.4f
diff --git a/quickstep/src_protolog/com/android/launcher3/util/StateManagerProtoLogProxy.java b/quickstep/src_protolog/com/android/launcher3/util/StateManagerProtoLogProxy.java
new file mode 100644
index 0000000..bc989dc
--- /dev/null
+++ b/quickstep/src_protolog/com/android/launcher3/util/StateManagerProtoLogProxy.java
@@ -0,0 +1,68 @@
+/*
+ * 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 static com.android.launcher3.Flags.enableStateManagerProtoLog;
+import static com.android.quickstep.util.QuickstepProtoLogGroup.LAUNCHER_STATE_MANAGER;
+
+import androidx.annotation.NonNull;
+
+import com.android.internal.protolog.ProtoLog;
+
+/**
+ * Proxy class used for StateManager ProtoLog support.
+ */
+public class StateManagerProtoLogProxy {
+
+    public static void logGoToState(
+            @NonNull Object fromState, @NonNull Object toState, @NonNull String trace) {
+        if (!enableStateManagerProtoLog()) return;
+        ProtoLog.d(LAUNCHER_STATE_MANAGER,
+                "StateManager.goToState: fromState: %s, toState: %s, partial trace:\n%s",
+                fromState,
+                toState,
+                trace);
+    }
+
+    public static void logCreateAtomicAnimation(
+            @NonNull Object fromState, @NonNull Object toState, @NonNull String trace) {
+        if (!enableStateManagerProtoLog()) return;
+        ProtoLog.d(LAUNCHER_STATE_MANAGER, "StateManager.createAtomicAnimation: "
+                        + "fromState: %s, toState: %s, partial trace:\n%s",
+                fromState,
+                toState,
+                trace);
+    }
+
+    public static void logOnStateTransitionStart(@NonNull Object state) {
+        if (!enableStateManagerProtoLog()) return;
+        ProtoLog.d(LAUNCHER_STATE_MANAGER, "StateManager.onStateTransitionStart: state: %s", state);
+    }
+
+    public static void logOnStateTransitionEnd(@NonNull Object state) {
+        if (!enableStateManagerProtoLog()) return;
+        ProtoLog.d(LAUNCHER_STATE_MANAGER, "StateManager.onStateTransitionEnd: state: %s", state);
+    }
+
+    public static void logCancelAnimation(boolean animationOngoing, @NonNull String trace) {
+        if (!enableStateManagerProtoLog()) return;
+        ProtoLog.d(LAUNCHER_STATE_MANAGER,
+                "StateManager.cancelAnimation: animation ongoing: %b, partial trace:\n%s",
+                animationOngoing,
+                trace);
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/util/ActiveGestureErrorDetector.java b/quickstep/src_protolog/com/android/quickstep/util/ActiveGestureErrorDetector.java
similarity index 99%
rename from quickstep/src/com/android/quickstep/util/ActiveGestureErrorDetector.java
rename to quickstep/src_protolog/com/android/quickstep/util/ActiveGestureErrorDetector.java
index 2398e66..ab10979 100644
--- a/quickstep/src/com/android/quickstep/util/ActiveGestureErrorDetector.java
+++ b/quickstep/src_protolog/com/android/quickstep/util/ActiveGestureErrorDetector.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2022 The Android Open Source Project
+ * 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.
diff --git a/quickstep/src/com/android/quickstep/util/ActiveGestureLog.java b/quickstep/src_protolog/com/android/quickstep/util/ActiveGestureLog.java
similarity index 78%
rename from quickstep/src/com/android/quickstep/util/ActiveGestureLog.java
rename to quickstep/src_protolog/com/android/quickstep/util/ActiveGestureLog.java
index d46b8fc..23e245c 100644
--- a/quickstep/src/com/android/quickstep/util/ActiveGestureLog.java
+++ b/quickstep/src_protolog/com/android/quickstep/util/ActiveGestureLog.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2019 The Android Open Source Project
+ * 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.
@@ -18,11 +18,10 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
-import com.android.launcher3.util.Preconditions;
-
 import java.io.PrintWriter;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.Date;
 import java.util.List;
@@ -72,14 +71,6 @@
         addLog(event, null);
     }
 
-    public void addLog(@NonNull String event, int extras) {
-        addLog(event, extras, null);
-    }
-
-    public void addLog(@NonNull String event, boolean extras) {
-        addLog(event, extras, null);
-    }
-
     /**
      * Adds a log to be printed at log-dump-time and track the associated event for error detection.
      *
@@ -90,20 +81,6 @@
         addLog(new CompoundString(event), gestureEvent);
     }
 
-    public void addLog(
-            @NonNull String event,
-            int extras,
-            @Nullable ActiveGestureErrorDetector.GestureEvent gestureEvent) {
-        addLog(new CompoundString(event).append(": ").append(extras), gestureEvent);
-    }
-
-    public void addLog(
-            @NonNull String event,
-            boolean extras,
-            @Nullable ActiveGestureErrorDetector.GestureEvent gestureEvent) {
-        addLog(new CompoundString(event).append(": ").append(extras), gestureEvent);
-    }
-
     public void addLog(@NonNull CompoundString compoundString) {
         addLog(compoundString, null);
     }
@@ -252,25 +229,27 @@
     /** A buildable string stored as an array for memory efficiency. */
     public static class CompoundString {
 
-        public static final CompoundString NO_OP = new CompoundString();
+        public static final CompoundString NO_OP = new CompoundString(true);
 
         private final List<String> mSubstrings;
         private final List<Object> mArgs;
 
         private final boolean mIsNoOp;
 
-        private CompoundString() {
-            this(null);
+        public static CompoundString newEmptyString() {
+            return new CompoundString(false);
         }
 
-        public CompoundString(String substring) {
-            mIsNoOp = substring == null;
+        private CompoundString(boolean isNoOp) {
+            mIsNoOp = isNoOp;
             mSubstrings = mIsNoOp ? null : new ArrayList<>();
             mArgs = mIsNoOp ? null : new ArrayList<>();
+        }
 
-            if (!mIsNoOp) {
-                mSubstrings.add(substring);
-            }
+        public CompoundString(String substring, Object... args) {
+            this(substring == null);
+
+            append(substring, args);
         }
 
         public CompoundString append(CompoundString substring) {
@@ -283,80 +262,24 @@
             return this;
         }
 
-        public CompoundString append(String substring) {
+        public CompoundString append(String substring, Object... args) {
             if (mIsNoOp) {
                 return this;
             }
             mSubstrings.add(substring);
+            mArgs.addAll(Arrays.stream(args).toList());
 
             return this;
         }
 
-        public CompoundString append(int num) {
-            if (mIsNoOp) {
-                return this;
-            }
-            mArgs.add(num);
-
-            return append("%d");
-        }
-
-        public CompoundString append(long num) {
-            if (mIsNoOp) {
-                return this;
-            }
-            mArgs.add(num);
-
-            return append("%d");
-        }
-
-        public CompoundString append(float num) {
-            if (mIsNoOp) {
-                return this;
-            }
-            mArgs.add(num);
-
-            return append("%.2f");
-        }
-
-        public CompoundString append(double num) {
-            if (mIsNoOp) {
-                return this;
-            }
-            mArgs.add(num);
-
-            return append("%.2f");
-        }
-
-        public CompoundString append(boolean bool) {
-            if (mIsNoOp) {
-                return this;
-            }
-            mArgs.add(bool);
-
-            return append("%b");
-        }
-
-        private Object[] getArgs() {
-            Preconditions.assertTrue(!mIsNoOp);
-
-            return mArgs.toArray();
-        }
-
         @Override
         public String toString() {
-            return String.format(toUnformattedString(), getArgs());
-        }
-
-        private String toUnformattedString() {
-            Preconditions.assertTrue(!mIsNoOp);
-
+            if (mIsNoOp) return null;
             StringBuilder sb = new StringBuilder();
             for (String substring : mSubstrings) {
                 sb.append(substring);
             }
-
-            return sb.toString();
+            return String.format(sb.toString(), mArgs.toArray());
         }
 
         @Override
@@ -366,10 +289,9 @@
 
         @Override
         public boolean equals(Object obj) {
-            if (!(obj instanceof CompoundString)) {
+            if (!(obj instanceof CompoundString other)) {
                 return false;
             }
-            CompoundString other = (CompoundString) obj;
             return (mIsNoOp == other.mIsNoOp)
                     && Objects.equals(mSubstrings, other.mSubstrings)
                     && Objects.equals(mArgs, other.mArgs);
diff --git a/quickstep/src_protolog/com/android/quickstep/util/ActiveGestureProtoLogProxy.java b/quickstep/src_protolog/com/android/quickstep/util/ActiveGestureProtoLogProxy.java
new file mode 100644
index 0000000..f43a125
--- /dev/null
+++ b/quickstep/src_protolog/com/android/quickstep/util/ActiveGestureProtoLogProxy.java
@@ -0,0 +1,502 @@
+/*
+ * 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 static android.view.MotionEvent.ACTION_DOWN;
+
+import static com.android.launcher3.Flags.enableActiveGestureProtoLog;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.CANCEL_RECENTS_ANIMATION;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.FINISH_RECENTS_ANIMATION;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.INVALID_VELOCITY_ON_SWIPE_UP;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.LAUNCHER_DESTROYED;
+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.ON_CANCEL_RECENTS_ANIMATION;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.ON_FINISH_RECENTS_ANIMATION;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.ON_SETTLED_ON_END_TARGET;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.ON_START_RECENTS_ANIMATION;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.QUICK_SWITCH_FROM_HOME_FAILED;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.QUICK_SWITCH_FROM_HOME_FALLBACK;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.RECENTS_ANIMATION_START_PENDING;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.RECENT_TASKS_MISSING;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.SET_END_TARGET;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.START_RECENTS_ANIMATION;
+import static com.android.quickstep.util.QuickstepProtoLogGroup.ACTIVE_GESTURE_LOG;
+
+import android.view.MotionEvent;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.internal.protolog.ProtoLog;
+import com.android.internal.protolog.common.IProtoLogGroup;
+
+/**
+ * Proxy class used for ActiveGestureLog ProtoLog support.
+ * <p>
+ * This file will have all of its static strings in the
+ * {@link ProtoLog#d(IProtoLogGroup, String, Object...)} calls replaced by dynamic code/strings.
+ * <p>
+ * When a new ActiveGestureLog entry needs to be added to the codebase (or and existing entry needs
+ * to be modified), add it here under a new unique method and make sure the ProtoLog entry matches
+ * to avoid confusion.
+ */
+public class ActiveGestureProtoLogProxy {
+
+    public static void logLauncherDestroyed() {
+        ActiveGestureLog.INSTANCE.addLog("Launcher destroyed", LAUNCHER_DESTROYED);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "Launcher destroyed");
+    }
+
+    public static void logAbsSwipeUpHandlerOnRecentsAnimationCanceled() {
+        ActiveGestureLog.INSTANCE.addLog(
+                /* event= */ "AbsSwipeUpHandler.onRecentsAnimationCanceled",
+                /* gestureEvent= */ CANCEL_RECENTS_ANIMATION);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "AbsSwipeUpHandler.onRecentsAnimationCanceled");
+    }
+
+    public static void logAbsSwipeUpHandlerOnRecentsAnimationFinished() {
+        ActiveGestureLog.INSTANCE.addLog(
+                /* event= */ "RecentsAnimationCallbacks.onAnimationFinished",
+                ON_FINISH_RECENTS_ANIMATION);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "AbsSwipeUpHandler.onAnimationFinished");
+    }
+
+    public static void logAbsSwipeUpHandlerCancelCurrentAnimation() {
+        ActiveGestureLog.INSTANCE.addLog(
+                "AbsSwipeUpHandler.cancelCurrentAnimation",
+                ActiveGestureErrorDetector.GestureEvent.CANCEL_CURRENT_ANIMATION);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "AbsSwipeUpHandler.cancelCurrentAnimation");
+    }
+
+    public static void logAbsSwipeUpHandlerOnTasksAppeared() {
+        ActiveGestureLog.INSTANCE.addLog("AbsSwipeUpHandler.onTasksAppeared: "
+                + "force finish recents animation complete; clearing state callback.");
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "AbsSwipeUpHandler.onTasksAppeared: "
+                + "force finish recents animation complete; clearing state callback.");
+    }
+
+    public static void logFinishRecentsAnimationOnTasksAppeared() {
+        ActiveGestureLog.INSTANCE.addLog("finishRecentsAnimationOnTasksAppeared");
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "finishRecentsAnimationOnTasksAppeared");
+    }
+
+    public static void logRecentsAnimationCallbacksOnAnimationCancelled() {
+        ActiveGestureLog.INSTANCE.addLog(
+                /* event= */ "RecentsAnimationCallbacks.onAnimationCanceled",
+                /* gestureEvent= */ ON_CANCEL_RECENTS_ANIMATION);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "RecentsAnimationCallbacks.onAnimationCanceled");
+    }
+
+    public static void logRecentsAnimationCallbacksOnTasksAppeared() {
+        ActiveGestureLog.INSTANCE.addLog("RecentsAnimationCallbacks.onTasksAppeared",
+                ActiveGestureErrorDetector.GestureEvent.TASK_APPEARED);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "RecentsAnimationCallbacks.onTasksAppeared");
+    }
+
+    public static void logStartRecentsAnimation() {
+        ActiveGestureLog.INSTANCE.addLog(
+                /* event= */ "TaskAnimationManager.startRecentsAnimation",
+                /* gestureEvent= */ START_RECENTS_ANIMATION);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "TaskAnimationManager.startRecentsAnimation");
+    }
+
+    public static void logLaunchingSideTaskFailed() {
+        ActiveGestureLog.INSTANCE.addLog("Unable to launch side task (no recents)");
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "Unable to launch side task (no recents)");
+    }
+
+    public static void logContinueRecentsAnimation() {
+        ActiveGestureLog.INSTANCE.addLog(/* event= */ "continueRecentsAnimation");
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "continueRecentsAnimation");
+    }
+
+    public static void logCleanUpRecentsAnimationSkipped() {
+        ActiveGestureLog.INSTANCE.addLog(
+                /* event= */ "cleanUpRecentsAnimation skipped due to wrong callbacks");
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "cleanUpRecentsAnimation skipped due to wrong callbacks");
+    }
+
+    public static void logCleanUpRecentsAnimation() {
+        ActiveGestureLog.INSTANCE.addLog(/* event= */ "cleanUpRecentsAnimation");
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "cleanUpRecentsAnimation");
+    }
+
+    public static void logOnInputEventUserLocked() {
+        ActiveGestureLog.INSTANCE.addLog(
+                "TIS.onInputEvent: Cannot process input event: user is locked");
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG,
+                "TIS.onInputEvent: Cannot process input event: user is locked");
+    }
+
+    public static void logOnInputIgnoringFollowingEvents() {
+        ActiveGestureLog.INSTANCE.addLog("TIS.onMotionEvent: A new gesture has been started, "
+                        + "but a previously-requested recents animation hasn't started. "
+                        + "Ignoring all following motion events.",
+                RECENTS_ANIMATION_START_PENDING);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "TIS.onMotionEvent: A new gesture has been started, "
+                + "but a previously-requested recents animation hasn't started. "
+                + "Ignoring all following motion events.");
+    }
+
+    public static void logOnInputEventThreeButtonNav() {
+        ActiveGestureLog.INSTANCE.addLog("TIS.onInputEvent: Cannot process input event: "
+                + "using 3-button nav and event is not a trackpad event");
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "TIS.onInputEvent: Cannot process input event: "
+                + "using 3-button nav and event is not a trackpad event");
+    }
+
+    public static void logPreloadRecentsAnimation() {
+        ActiveGestureLog.INSTANCE.addLog("preloadRecentsAnimation");
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "preloadRecentsAnimation");
+    }
+
+    public static void logRecentTasksMissing() {
+        ActiveGestureLog.INSTANCE.addLog("Null mRecentTasks", RECENT_TASKS_MISSING);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "Null mRecentTasks");
+    }
+
+    public static void logExecuteHomeCommand() {
+        ActiveGestureLog.INSTANCE.addLog("OverviewCommandHelper.executeCommand(HOME)");
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "OverviewCommandHelper.executeCommand(HOME)");
+    }
+
+    public static void logFinishRecentsAnimationCallback() {
+        ActiveGestureLog.INSTANCE.addLog("finishRecentsAnimation-callback");
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "finishRecentsAnimation-callback");
+    }
+
+    public static void logOnScrollerAnimationAborted() {
+        ActiveGestureLog.INSTANCE.addLog("scroller animation aborted",
+                ActiveGestureErrorDetector.GestureEvent.SCROLLER_ANIMATION_ABORTED);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "scroller animation aborted");
+    }
+
+    public static void logInputConsumerBecameActive(@NonNull String consumerName) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "%s became active", consumerName));
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "%s became active", consumerName);
+    }
+
+    public static void logTaskLaunchFailed(int launchedTaskId) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "Launch failed, task (id=%d) finished mid transition", launchedTaskId));
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG,
+                "Launch failed, task (id=%d) finished mid transition", launchedTaskId);
+    }
+
+    public static void logOnPageEndTransition(int nextPageIndex) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "onPageEndTransition: current page index updated: %d", nextPageIndex));
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG,
+                "onPageEndTransition: current page index updated: %d", nextPageIndex);
+    }
+
+    public static void logQuickSwitchFromHomeFallback(int taskIndex) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "Quick switch from home fallback case: The TaskView at index %d is missing.",
+                        taskIndex),
+                QUICK_SWITCH_FROM_HOME_FALLBACK);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG,
+                "Quick switch from home fallback case: The TaskView at index %d is missing.",
+                taskIndex);
+    }
+
+    public static void logQuickSwitchFromHomeFailed(int taskIndex) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "Quick switch from home failed: TaskViews at indices %d and 0 are missing.",
+                        taskIndex),
+                QUICK_SWITCH_FROM_HOME_FAILED);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG,
+                "Quick switch from home failed: TaskViews at indices %d and 0 are missing.",
+                taskIndex);
+    }
+
+    public static void logFinishRecentsAnimation(boolean toRecents) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "finishRecentsAnimation: %b", toRecents),
+                /* gestureEvent= */ FINISH_RECENTS_ANIMATION);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "finishRecentsAnimation: %b", toRecents);
+    }
+
+    public static void logSetEndTarget(@NonNull String target) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "setEndTarget %s", target), /* gestureEvent= */ SET_END_TARGET);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "setEndTarget %s", target);
+    }
+
+    public static void logStartHomeIntent(@NonNull String reason) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "OverviewComponentObserver.startHomeIntent: %s", reason));
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "OverviewComponentObserver.startHomeIntent: %s", reason);
+    }
+
+    public static void logRunningTaskPackage(@NonNull String packageName) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "Current running task package name=%s", packageName));
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "Current running task package name=%s", packageName);
+    }
+
+    public static void logSysuiStateFlags(@NonNull String stateFlags) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "Current SystemUi state flags=%s", stateFlags));
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "Current SystemUi state flags=%s", stateFlags);
+    }
+
+    public static void logSetInputConsumer(@NonNull String consumerName, @NonNull String reason) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "setInputConsumer: %s. reason(s):%s", consumerName, reason));
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG,
+                "setInputConsumer: %s. reason(s):%s", consumerName, reason);
+    }
+
+    public static void logUpdateGestureStateRunningTask(
+            @NonNull String otherTaskPackage, @NonNull String runningTaskPackage) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "Changing active task to %s because the previous task running on top of this "
+                        + "one (%s) was excluded from recents",
+                otherTaskPackage,
+                runningTaskPackage));
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG,
+                "Changing active task to %s because the previous task running on top of this "
+                        + "one (%s) was excluded from recents",
+                otherTaskPackage,
+                runningTaskPackage);
+    }
+
+    public static void logOnInputEventActionUp(
+            int x, int y, int action, @NonNull String classification) {
+        String actionString = MotionEvent.actionToString(action);
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "onMotionEvent(%d, %d): %s, %s", x, y, actionString, classification),
+                /* gestureEvent= */ action == ACTION_DOWN
+                        ? MOTION_DOWN
+                        : MOTION_UP);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG,
+                "onMotionEvent(%d, %d): %s, %s", x, y, actionString, classification);
+    }
+
+    public static void logOnInputEventActionMove(
+            @NonNull String action, @NonNull String classification, int pointerCount) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                        "onMotionEvent: %s, %s, pointerCount: %d",
+                        action,
+                        classification,
+                        pointerCount),
+                MOTION_MOVE);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG,
+                "onMotionEvent: %s, %s, pointerCount: %d", action, classification, pointerCount);
+    }
+
+    public static void logOnInputEventGenericAction(
+            @NonNull String action, @NonNull String classification) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "onMotionEvent: %s, %s", action, classification));
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "onMotionEvent: %s, %s", action, classification);
+    }
+
+    public static void logOnInputEventNavModeSwitched(
+            @NonNull String startNavMode, @NonNull String currentNavMode) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "TIS.onInputEvent: Navigation mode switched mid-gesture (%s -> %s); "
+                        + "cancelling gesture.",
+                        startNavMode,
+                        currentNavMode),
+                NAVIGATION_MODE_SWITCHED);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG,
+                "TIS.onInputEvent: Navigation mode switched mid-gesture (%s -> %s); "
+                        + "cancelling gesture.",
+                startNavMode,
+                currentNavMode);
+    }
+
+    public static void logUnknownInputEvent(@NonNull String event) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "TIS.onInputEvent: Cannot process input event: received unknown event %s", event));
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG,
+                "TIS.onInputEvent: Cannot process input event: received unknown event %s", event);
+    }
+
+    public static void logFinishRunningRecentsAnimation(boolean toHome) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "finishRunningRecentsAnimation: %b", toHome));
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "finishRunningRecentsAnimation: %b", toHome);
+    }
+
+    public static void logOnRecentsAnimationStartCancelled() {
+        ActiveGestureLog.INSTANCE.addLog("RecentsAnimationCallbacks.onAnimationStart (canceled): 0",
+                /* gestureEvent= */ ON_START_RECENTS_ANIMATION);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "RecentsAnimationCallbacks.onAnimationStart (canceled): 0");
+    }
+
+    public static void logOnRecentsAnimationStart(int appCount) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "RecentsAnimationCallbacks.onAnimationStart (canceled): %d", appCount),
+                /* gestureEvent= */ ON_START_RECENTS_ANIMATION);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG,
+                "RecentsAnimationCallbacks.onAnimationStart (canceled): %d", appCount);
+    }
+
+    public static void logStartRecentsAnimationCallback(@NonNull String callback) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "TaskAnimationManager.startRecentsAnimation(%s): "
+                        + "Setting mRecentsAnimationStartPending = false",
+                callback));
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG,
+                "TaskAnimationManager.startRecentsAnimation(%s): "
+                        + "Setting mRecentsAnimationStartPending = false",
+                callback);
+    }
+
+    public static void logSettingRecentsAnimationStartPending(boolean value) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "TaskAnimationManager.startRecentsAnimation: "
+                        + "Setting mRecentsAnimationStartPending = %b",
+                value));
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG,
+                "TaskAnimationManager.startRecentsAnimation: "
+                        + "Setting mRecentsAnimationStartPending = %b",
+                value);
+    }
+
+    public static void logLaunchingSideTask(int taskId) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "Launching side task id=%d", taskId));
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "Launching side task id=%d", taskId);
+    }
+
+    public static void logOnInputEventActionDown(@NonNull ActiveGestureLog.CompoundString reason) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "TIS.onMotionEvent: ").append(reason));
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "TIS.onMotionEvent: %s", reason.toString());
+    }
+
+    public static void logStartNewTask(@NonNull ActiveGestureLog.CompoundString tasks) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "Launching task: ").append(tasks));
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "TIS.onMotionEvent: %s", tasks.toString());
+    }
+
+    public static void logMotionPauseDetectorEvent(@NonNull ActiveGestureLog.CompoundString event) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "MotionPauseDetector: ").append(event));
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "MotionPauseDetector: %s", event.toString());
+    }
+
+    public static void logHandleTaskAppearedFailed(
+            @NonNull ActiveGestureLog.CompoundString reason) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "handleTaskAppeared check failed: ").append(reason));
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "handleTaskAppeared check failed: %s", reason.toString());
+    }
+
+    /**
+     * This is for special cases where the string is purely dynamic and therefore has no format that
+     * can be extracted. Do not use in any other case.
+     */
+    public static void logDynamicString(
+            @NonNull String string,
+            @Nullable ActiveGestureErrorDetector.GestureEvent gestureEvent) {
+        ActiveGestureLog.INSTANCE.addLog(string, gestureEvent);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "%s", string);
+    }
+
+    public static void logOnSettledOnEndTarget(@NonNull String endTarget) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "onSettledOnEndTarget %s", endTarget),
+                /* gestureEvent= */ ON_SETTLED_ON_END_TARGET);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "onSettledOnEndTarget %s", endTarget);
+    }
+
+    public static void logOnCalculateEndTarget(float velocityX, float velocityY, double angle) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "calculateEndTarget: velocities=(x=%fdp/ms, y=%fdp/ms), angle=%f",
+                        velocityX,
+                        velocityY,
+                        angle),
+                velocityX == 0 && velocityY == 0 ? INVALID_VELOCITY_ON_SWIPE_UP : null);
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG,
+                "calculateEndTarget: velocities=(x=%fdp/ms, y=%fdp/ms), angle=%f",
+                velocityX,
+                velocityY,
+                angle);
+    }
+
+    public static void logUnexpectedTaskAppeared(int taskId, @NonNull String packageName) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+                "Forcefully finishing recents animation: Unexpected task appeared id=%d, pkg=%s",
+                taskId,
+                packageName));
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG,
+                "Forcefully finishing recents animation: Unexpected task appeared id=%d, pkg=%s",
+                taskId,
+                packageName);
+    }
+}
diff --git a/quickstep/src_protolog/com/android/quickstep/util/QuickstepProtoLogGroup.java b/quickstep/src_protolog/com/android/quickstep/util/QuickstepProtoLogGroup.java
new file mode 100644
index 0000000..bb02a11
--- /dev/null
+++ b/quickstep/src_protolog/com/android/quickstep/util/QuickstepProtoLogGroup.java
@@ -0,0 +1,107 @@
+/*
+ * 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 androidx.annotation.NonNull;
+
+import com.android.internal.protolog.ProtoLog;
+import com.android.internal.protolog.common.IProtoLogGroup;
+
+import java.util.UUID;
+
+/** Enums used to interface with the ProtoLog API. */
+public enum QuickstepProtoLogGroup implements IProtoLogGroup {
+
+    ACTIVE_GESTURE_LOG(true, true, false, "ActiveGestureLog"),
+    RECENTS_WINDOW(true, true, Constants.DEBUG_RECENTS_WINDOW, "RecentsWindow"),
+    LAUNCHER_STATE_MANAGER(true, true, Constants.DEBUG_STATE_MANAGER, "LauncherStateManager");
+
+    private final boolean mEnabled;
+    private volatile boolean mLogToProto;
+    private volatile boolean mLogToLogcat;
+    private final @NonNull String mTag;
+
+    public static void initProtoLog() {
+        ProtoLog.init(QuickstepProtoLogGroup.values());
+    }
+
+    /**
+     * @param enabled     set to false to exclude all log statements for this group from
+     *                    compilation,
+     *                    they will not be available in runtime.
+     * @param logToProto  enable binary logging for the group
+     * @param logToLogcat enable text logging for the group
+     * @param tag         name of the source of the logged message
+     */
+    QuickstepProtoLogGroup(
+            boolean enabled, boolean logToProto, boolean logToLogcat, @NonNull String tag) {
+        this.mEnabled = enabled;
+        this.mLogToProto = logToProto;
+        this.mLogToLogcat = logToLogcat;
+        this.mTag = tag;
+    }
+
+    @Override
+    public boolean isEnabled() {
+        return mEnabled;
+    }
+
+    @Override
+    public boolean isLogToProto() {
+        return mLogToProto;
+    }
+
+    @Override
+    public boolean isLogToLogcat() {
+        return mLogToLogcat;
+    }
+
+    @Override
+    public boolean isLogToAny() {
+        return mLogToLogcat || mLogToProto;
+    }
+
+    @Override
+    public int getId() {
+        return Constants.LOG_START_ID + this.ordinal();
+    }
+
+    @Override
+    public @NonNull String getTag() {
+        return mTag;
+    }
+
+    @Override
+    public void setLogToProto(boolean logToProto) {
+        this.mLogToProto = logToProto;
+    }
+
+    @Override
+    public void setLogToLogcat(boolean logToLogcat) {
+        this.mLogToLogcat = logToLogcat;
+    }
+
+    private static final class Constants {
+
+        private static final boolean DEBUG_RECENTS_WINDOW = false;
+        private static final boolean DEBUG_STATE_MANAGER = true; // b/279059025, b/325463989
+
+        private static final int LOG_START_ID =
+                (int) (UUID.nameUUIDFromBytes(QuickstepProtoLogGroup.class.getName().getBytes())
+                        .getMostSignificantBits() % Integer.MAX_VALUE);
+    }
+}
diff --git a/quickstep/src_protolog/com/android/quickstep/util/RecentsWindowProtoLogProxy.java b/quickstep/src_protolog/com/android/quickstep/util/RecentsWindowProtoLogProxy.java
new file mode 100644
index 0000000..f54ad67
--- /dev/null
+++ b/quickstep/src_protolog/com/android/quickstep/util/RecentsWindowProtoLogProxy.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.quickstep.util;
+
+import static com.android.launcher3.Flags.enableRecentsWindowProtoLog;
+import static com.android.quickstep.util.QuickstepProtoLogGroup.RECENTS_WINDOW;
+
+import androidx.annotation.NonNull;
+
+import com.android.internal.protolog.ProtoLog;
+import com.android.internal.protolog.common.IProtoLogGroup;
+
+/**
+ * Proxy class used for Recents Window ProtoLog support.
+ * <p>
+ * This file will have all of its static strings in the
+ * {@link ProtoLog#d(IProtoLogGroup, String, Object...)} calls replaced by dynamic code/strings.
+ * <p>
+ * When a new Recents Window log needs to be added to the codebase, add it here under a new unique
+ * method. Or, if an existing entry needs to be modified, simply update it here.
+ */
+public class RecentsWindowProtoLogProxy {
+
+    public static void logOnStateSetStart(@NonNull String stateName) {
+        if (!enableRecentsWindowProtoLog()) return;
+        ProtoLog.d(RECENTS_WINDOW, "onStateSetStart: %s", stateName);
+    }
+
+    public static void logOnStateSetEnd(@NonNull String stateName) {
+        if (!enableRecentsWindowProtoLog()) return;
+        ProtoLog.d(RECENTS_WINDOW, "onStateSetEnd: %s", stateName);
+    }
+
+    public static void logStartRecentsWindow(boolean isShown, boolean windowViewIsNull) {
+        if (!enableRecentsWindowProtoLog()) return;
+        ProtoLog.d(RECENTS_WINDOW,
+                "Starting recents window: isShow= %b, windowViewIsNull=%b",
+                isShown,
+                windowViewIsNull);
+    }
+
+    public static void logCleanup(boolean isShown) {
+        if (!enableRecentsWindowProtoLog()) return;
+        ProtoLog.d(RECENTS_WINDOW, "Cleaning up recents window: isShow= %b", isShown);
+    }
+}
diff --git a/quickstep/testing/com/android/launcher3/taskbar/bubbles/testing/FakeBubbleViewFactory.kt b/quickstep/testing/com/android/launcher3/taskbar/bubbles/testing/FakeBubbleViewFactory.kt
index 37a07c3..2f1f0b5 100644
--- a/quickstep/testing/com/android/launcher3/taskbar/bubbles/testing/FakeBubbleViewFactory.kt
+++ b/quickstep/testing/com/android/launcher3/taskbar/bubbles/testing/FakeBubbleViewFactory.kt
@@ -54,14 +54,35 @@
         val flags =
             if (suppressNotification) Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION else 0
         val bubbleInfo =
-            BubbleInfo(key, flags, null, null, 0, context.packageName, null, null, false, true)
+            BubbleInfo(
+                key,
+                flags,
+                null,
+                null,
+                0,
+                context.packageName,
+                null,
+                null,
+                false,
+                true,
+                null,
+            )
         val bubbleView = inflater.inflate(R.layout.bubblebar_item_view, parent, false) as BubbleView
         val dotPath =
             PathParser.createPathFromPathData(
                 context.resources.getString(com.android.internal.R.string.config_icon_mask)
             )
         val bubble =
-            BubbleBarBubble(bubbleInfo, bubbleView, badge, icon, dotColor, dotPath, "test app")
+            BubbleBarBubble(
+                bubbleInfo,
+                bubbleView,
+                badge,
+                icon,
+                dotColor,
+                dotPath,
+                "test app",
+                null,
+            )
         bubbleView.setBubble(bubble)
         return bubbleView
     }
diff --git a/quickstep/tests/multivalentScreenshotTests/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewScreenshotTest.kt b/quickstep/tests/multivalentScreenshotTests/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewScreenshotTest.kt
index e4b8069..b5a418b 100644
--- a/quickstep/tests/multivalentScreenshotTests/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewScreenshotTest.kt
+++ b/quickstep/tests/multivalentScreenshotTests/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewScreenshotTest.kt
@@ -97,6 +97,8 @@
     fun bubbleBarView_expanded_threeBubbles() {
         // if we're still expanding, wait with taking a screenshot
         val shouldWait: (ComponentActivity, View) -> Boolean = { _, _ -> bubbleBarView.isExpanding }
+        // increase the frame limit to allow the animation to end before taking the screenshot
+        screenshotRule.frameLimit = 500
         screenshotRule.screenshotTest(
             "bubbleBarView_expanded_threeBubbles",
             checkView = shouldWait,
diff --git a/quickstep/tests/multivalentScreenshotTests/src/com/android/launcher3/taskbar/bubbles/OWNERS b/quickstep/tests/multivalentScreenshotTests/src/com/android/launcher3/taskbar/bubbles/OWNERS
new file mode 100644
index 0000000..63c1498
--- /dev/null
+++ b/quickstep/tests/multivalentScreenshotTests/src/com/android/launcher3/taskbar/bubbles/OWNERS
@@ -0,0 +1,4 @@
+atsjenk@google.com
+liranb@google.com
+madym@google.com
+mpodolian@google.com
diff --git a/quickstep/tests/multivalentScreenshotTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutViewScreenshotTest.kt b/quickstep/tests/multivalentScreenshotTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutViewScreenshotTest.kt
index 537a755..11c7fe9 100644
--- a/quickstep/tests/multivalentScreenshotTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutViewScreenshotTest.kt
+++ b/quickstep/tests/multivalentScreenshotTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutViewScreenshotTest.kt
@@ -18,6 +18,7 @@
 
 import android.content.Context
 import android.graphics.Color
+import android.graphics.PointF
 import android.graphics.drawable.ColorDrawable
 import androidx.test.core.app.ApplicationProvider
 import com.google.android.apps.nexuslauncher.imagecomparison.goldenpathmanager.ViewScreenshotGoldenPathManager
@@ -59,15 +60,12 @@
     fun bubbleBarFlyoutView_noAvatar_onRight() {
         screenshotRule.screenshotTest("bubbleBarFlyoutView_noAvatar_onRight") { activity ->
             activity.actionBar?.hide()
-            val flyout = BubbleBarFlyoutView(context, onLeft = false)
-            flyout.setData(
-                BubbleBarFlyoutMessage(
-                    senderAvatar = null,
-                    senderName = "sender",
-                    message = "message",
-                    isGroupChat = false,
-                )
-            )
+            val flyout =
+                BubbleBarFlyoutView(context, FakeBubbleBarFlyoutPositioner(isOnLeft = false))
+            flyout.showFromCollapsed(
+                BubbleBarFlyoutMessage(icon = null, title = "sender", message = "message")
+            ) {}
+            flyout.updateExpansionProgress(1f)
             flyout
         }
     }
@@ -76,15 +74,12 @@
     fun bubbleBarFlyoutView_noAvatar_onLeft() {
         screenshotRule.screenshotTest("bubbleBarFlyoutView_noAvatar_onLeft") { activity ->
             activity.actionBar?.hide()
-            val flyout = BubbleBarFlyoutView(context, onLeft = true)
-            flyout.setData(
-                BubbleBarFlyoutMessage(
-                    senderAvatar = null,
-                    senderName = "sender",
-                    message = "message",
-                    isGroupChat = false,
-                )
-            )
+            val flyout =
+                BubbleBarFlyoutView(context, FakeBubbleBarFlyoutPositioner(isOnLeft = true))
+            flyout.showFromCollapsed(
+                BubbleBarFlyoutMessage(icon = null, title = "sender", message = "message")
+            ) {}
+            flyout.updateExpansionProgress(1f)
             flyout
         }
     }
@@ -93,15 +88,16 @@
     fun bubbleBarFlyoutView_noAvatar_longMessage() {
         screenshotRule.screenshotTest("bubbleBarFlyoutView_noAvatar_longMessage") { activity ->
             activity.actionBar?.hide()
-            val flyout = BubbleBarFlyoutView(context, onLeft = true)
-            flyout.setData(
+            val flyout =
+                BubbleBarFlyoutView(context, FakeBubbleBarFlyoutPositioner(isOnLeft = true))
+            flyout.showFromCollapsed(
                 BubbleBarFlyoutMessage(
-                    senderAvatar = null,
-                    senderName = "sender",
+                    icon = null,
+                    title = "sender",
                     message = "really, really, really, really, really long message. like really.",
-                    isGroupChat = false,
                 )
-            )
+            ) {}
+            flyout.updateExpansionProgress(1f)
             flyout
         }
     }
@@ -110,15 +106,16 @@
     fun bubbleBarFlyoutView_avatar_onRight() {
         screenshotRule.screenshotTest("bubbleBarFlyoutView_avatar_onRight") { activity ->
             activity.actionBar?.hide()
-            val flyout = BubbleBarFlyoutView(context, onLeft = false)
-            flyout.setData(
+            val flyout =
+                BubbleBarFlyoutView(context, FakeBubbleBarFlyoutPositioner(isOnLeft = false))
+            flyout.showFromCollapsed(
                 BubbleBarFlyoutMessage(
-                    senderAvatar = ColorDrawable(Color.RED),
-                    senderName = "sender",
+                    icon = ColorDrawable(Color.RED),
+                    title = "sender",
                     message = "message",
-                    isGroupChat = true,
                 )
-            )
+            ) {}
+            flyout.updateExpansionProgress(1f)
             flyout
         }
     }
@@ -127,15 +124,16 @@
     fun bubbleBarFlyoutView_avatar_onLeft() {
         screenshotRule.screenshotTest("bubbleBarFlyoutView_avatar_onLeft") { activity ->
             activity.actionBar?.hide()
-            val flyout = BubbleBarFlyoutView(context, onLeft = true)
-            flyout.setData(
+            val flyout =
+                BubbleBarFlyoutView(context, FakeBubbleBarFlyoutPositioner(isOnLeft = true))
+            flyout.showFromCollapsed(
                 BubbleBarFlyoutMessage(
-                    senderAvatar = ColorDrawable(Color.RED),
-                    senderName = "sender",
+                    icon = ColorDrawable(Color.RED),
+                    title = "sender",
                     message = "message",
-                    isGroupChat = true,
                 )
-            )
+            ) {}
+            flyout.updateExpansionProgress(1f)
             flyout
         }
     }
@@ -144,16 +142,112 @@
     fun bubbleBarFlyoutView_avatar_longMessage() {
         screenshotRule.screenshotTest("bubbleBarFlyoutView_avatar_longMessage") { activity ->
             activity.actionBar?.hide()
-            val flyout = BubbleBarFlyoutView(context, onLeft = true)
-            flyout.setData(
+            val flyout =
+                BubbleBarFlyoutView(context, FakeBubbleBarFlyoutPositioner(isOnLeft = true))
+            flyout.showFromCollapsed(
                 BubbleBarFlyoutMessage(
-                    senderAvatar = ColorDrawable(Color.RED),
-                    senderName = "sender",
+                    icon = ColorDrawable(Color.RED),
+                    title = "sender",
                     message = "really, really, really, really, really long message. like really.",
-                    isGroupChat = true,
                 )
-            )
+            ) {}
+            flyout.updateExpansionProgress(1f)
             flyout
         }
     }
+
+    @Test
+    fun bubbleBarFlyoutView_collapsed_onLeft() {
+        screenshotRule.screenshotTest("bubbleBarFlyoutView_collapsed_onLeft") { activity ->
+            activity.actionBar?.hide()
+            val flyout =
+                BubbleBarFlyoutView(context, FakeBubbleBarFlyoutPositioner(isOnLeft = true))
+            flyout.showFromCollapsed(
+                BubbleBarFlyoutMessage(
+                    icon = ColorDrawable(Color.RED),
+                    title = "sender",
+                    message = "collapsed on left",
+                )
+            ) {}
+            flyout.updateExpansionProgress(0f)
+            flyout
+        }
+    }
+
+    @Test
+    fun bubbleBarFlyoutView_collapsed_onRight() {
+        screenshotRule.screenshotTest("bubbleBarFlyoutView_collapsed_onRight") { activity ->
+            activity.actionBar?.hide()
+            val flyout =
+                BubbleBarFlyoutView(context, FakeBubbleBarFlyoutPositioner(isOnLeft = false))
+            flyout.showFromCollapsed(
+                BubbleBarFlyoutMessage(
+                    icon = ColorDrawable(Color.RED),
+                    title = "sender",
+                    message = "collapsed on right",
+                )
+            ) {}
+            flyout.updateExpansionProgress(0f)
+            flyout
+        }
+    }
+
+    @Test
+    fun bubbleBarFlyoutView_90p_onLeft() {
+        screenshotRule.screenshotTest("bubbleBarFlyoutView_90p_onLeft") { activity ->
+            activity.actionBar?.hide()
+            val flyout =
+                BubbleBarFlyoutView(
+                    context,
+                    FakeBubbleBarFlyoutPositioner(
+                        isOnLeft = true,
+                        distanceToCollapsedPosition = PointF(100f, 100f),
+                    ),
+                )
+            flyout.showFromCollapsed(
+                BubbleBarFlyoutMessage(
+                    icon = ColorDrawable(Color.RED),
+                    title = "sender",
+                    message = "expanded 90% on left",
+                )
+            ) {}
+            flyout.updateExpansionProgress(0.9f)
+            flyout
+        }
+    }
+
+    @Test
+    fun bubbleBarFlyoutView_80p_onRight() {
+        screenshotRule.screenshotTest("bubbleBarFlyoutView_80p_onRight") { activity ->
+            activity.actionBar?.hide()
+            val flyout =
+                BubbleBarFlyoutView(
+                    context,
+                    FakeBubbleBarFlyoutPositioner(
+                        isOnLeft = false,
+                        distanceToCollapsedPosition = PointF(200f, 100f),
+                    ),
+                )
+            flyout.showFromCollapsed(
+                BubbleBarFlyoutMessage(
+                    icon = ColorDrawable(Color.RED),
+                    title = "sender",
+                    message = "expanded 80% on right",
+                )
+            ) {}
+            flyout.updateExpansionProgress(0.8f)
+            flyout
+        }
+    }
+
+    private class FakeBubbleBarFlyoutPositioner(
+        override val isOnLeft: Boolean,
+        override val distanceToCollapsedPosition: PointF = PointF(0f, 0f),
+    ) : BubbleBarFlyoutPositioner {
+        override val targetTy = 0f
+        override val collapsedSize = 30f
+        override val collapsedColor = Color.BLUE
+        override val collapsedElevation = 1f
+        override val distanceToRevealTriangle = 10f
+    }
 }
diff --git a/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/FakeTaskThumbnailViewModel.kt b/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/FakeTaskThumbnailViewModel.kt
new file mode 100644
index 0000000..ff5d8bd
--- /dev/null
+++ b/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/FakeTaskThumbnailViewModel.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.task.thumbnail
+
+import android.graphics.Matrix
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
+import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+
+class FakeTaskThumbnailViewModel : TaskThumbnailViewModel {
+    override val cornerRadiusProgress = MutableStateFlow(0f)
+    override val inheritedScale = MutableStateFlow(1f)
+    override val dimProgress = MutableStateFlow(0f)
+    override val splashAlpha = MutableStateFlow(0f)
+    override val uiState = MutableStateFlow<TaskThumbnailUiState>(Uninitialized)
+
+    override fun bind(taskId: Int) {
+        // no-op
+    }
+
+    override fun getThumbnailPositionState(width: Int, height: Int, isRtl: Boolean) =
+        Matrix.IDENTITY_MATRIX
+}
diff --git a/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewScreenshotTest.kt b/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewScreenshotTest.kt
new file mode 100644
index 0000000..75769e9
--- /dev/null
+++ b/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewScreenshotTest.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.quickstep.task.thumbnail
+
+import android.content.Context
+import android.graphics.Color
+import android.view.LayoutInflater
+import com.android.launcher3.R
+import com.android.quickstep.recents.di.RecentsDependencies
+import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel
+import com.google.android.apps.nexuslauncher.imagecomparison.goldenpathmanager.ViewScreenshotGoldenPathManager
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
+import platform.test.screenshot.DeviceEmulationSpec
+import platform.test.screenshot.Displays
+import platform.test.screenshot.ViewScreenshotTestRule
+import platform.test.screenshot.getEmulatedDevicePathConfig
+
+/** Screenshot tests for [TaskThumbnailView]. */
+@RunWith(ParameterizedAndroidJunit4::class)
+class TaskThumbnailViewScreenshotTest(emulationSpec: DeviceEmulationSpec) {
+
+    @get:Rule
+    val screenshotRule =
+        ViewScreenshotTestRule(
+            emulationSpec,
+            ViewScreenshotGoldenPathManager(getEmulatedDevicePathConfig(emulationSpec)),
+        )
+
+    private val taskThumbnailViewModel = FakeTaskThumbnailViewModel()
+
+    @Test
+    fun taskThumbnailView_uninitialized() {
+        screenshotRule.screenshotTest("taskThumbnailView_uninitialized") { activity ->
+            activity.actionBar?.hide()
+            createTaskThumbnailView(activity)
+        }
+    }
+
+    @Test
+    fun taskThumbnailView_backgroundOnly() {
+        screenshotRule.screenshotTest("taskThumbnailView_backgroundOnly") { activity ->
+            activity.actionBar?.hide()
+            taskThumbnailViewModel.uiState.value = TaskThumbnailUiState.BackgroundOnly(Color.YELLOW)
+            createTaskThumbnailView(activity)
+        }
+    }
+
+    private fun createTaskThumbnailView(context: Context): TaskThumbnailView {
+        val di = RecentsDependencies.initialize(context)
+        val taskThumbnailView =
+            LayoutInflater.from(context).inflate(R.layout.task_thumbnail, null, false)
+        val ttvDiScopeId = di.getScope(taskThumbnailView).scopeId
+        di.provide(TaskThumbnailViewData::class.java, ttvDiScopeId) { TaskThumbnailViewData() }
+        di.provide(TaskThumbnailViewModel::class.java, ttvDiScopeId) { taskThumbnailViewModel }
+
+        return taskThumbnailView as TaskThumbnailView
+    }
+
+    companion object {
+        @Parameters(name = "{0}")
+        @JvmStatic
+        fun getTestSpecs() =
+            DeviceEmulationSpec.forDisplays(
+                Displays.Phone,
+                isDarkTheme = false,
+                isLandscape = false,
+            )
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendControllerTest.kt
new file mode 100644
index 0000000..cfa12e2
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendControllerTest.kt
@@ -0,0 +1,104 @@
+/*
+ * 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.animation.AnimatorTestRule
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_DRAGGING
+import com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_IN_LAUNCHER
+import com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_TOUCHING
+import com.android.launcher3.taskbar.rules.TaskbarModeRule
+import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.TRANSIENT
+import com.android.launcher3.taskbar.rules.TaskbarModeRule.TaskbarMode
+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.quickstep.SystemUiProxy
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(LauncherMultivalentJUnit::class)
+@EmulatedDevices(["pixelTablet2023"])
+class TaskbarAutohideSuspendControllerTest {
+
+    @get:Rule(order = 0)
+    val context =
+        TaskbarWindowSandboxContext.create { builder ->
+            builder.bindSystemUiProxy(
+                object : SystemUiProxy(this) {
+                    override fun notifyTaskbarAutohideSuspend(suspend: Boolean) {
+                        super.notifyTaskbarAutohideSuspend(suspend)
+                        latestSuspendNotification = suspend
+                    }
+                }
+            )
+        }
+    @get:Rule(order = 1) val animatorTestRule = AnimatorTestRule(this)
+    @get:Rule(order = 2) val taskbarModeRule = TaskbarModeRule(context)
+    @get:Rule(order = 3) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context)
+
+    @InjectController lateinit var autohideSuspendController: TaskbarAutohideSuspendController
+    @InjectController lateinit var stashController: TaskbarStashController
+
+    private var latestSuspendNotification: Boolean? = null
+
+    @Test
+    fun testUpdateFlag_suspendInLauncher_notifiesSuspend() {
+        getInstrumentation().runOnMainSync {
+            autohideSuspendController.updateFlag(FLAG_AUTOHIDE_SUSPEND_IN_LAUNCHER, true)
+        }
+        assertThat(latestSuspendNotification).isTrue()
+    }
+
+    @Test
+    fun testUpdateFlag_toggleSuspendDraggingTwice_notifiesUnsuspend() {
+        getInstrumentation().runOnMainSync {
+            autohideSuspendController.updateFlag(FLAG_AUTOHIDE_SUSPEND_DRAGGING, true)
+            autohideSuspendController.updateFlag(FLAG_AUTOHIDE_SUSPEND_DRAGGING, false)
+        }
+        assertThat(latestSuspendNotification).isFalse()
+    }
+
+    @Test
+    fun testUpdateFlag_resetsAlreadyUnsetFlag_noNotifyUnsuspend() {
+        getInstrumentation().runOnMainSync {
+            autohideSuspendController.updateFlag(FLAG_AUTOHIDE_SUSPEND_DRAGGING, false)
+        }
+        assertThat(latestSuspendNotification).isNull()
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testUpdateFlag_suspendTransientTaskbarForTouch_cancelsAutoStashTimeout() {
+        // Unstash and verify alarm.
+        getInstrumentation().runOnMainSync {
+            stashController.updateAndAnimateTransientTaskbar(false)
+            animatorTestRule.advanceTimeBy(stashController.stashDuration)
+        }
+        assertThat(stashController.timeoutAlarm.alarmPending()).isTrue()
+
+        // EDU opens while unstashed.
+        getInstrumentation().runOnMainSync {
+            autohideSuspendController.updateFlag(FLAG_AUTOHIDE_SUSPEND_TOUCHING, true)
+        }
+        assertThat(stashController.timeoutAlarm.alarmPending()).isFalse()
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarControllerTestUtil.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarControllerTestUtil.kt
index a57fb70..6e2f74a 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarControllerTestUtil.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarControllerTestUtil.kt
@@ -16,10 +16,34 @@
 
 package com.android.launcher3.taskbar
 
+import android.content.Context
 import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.android.launcher3.ConstantItem
+import com.android.launcher3.LauncherPrefs
+import kotlin.properties.ReadWriteProperty
+import kotlin.reflect.KProperty
 
 object TaskbarControllerTestUtil {
     inline fun runOnMainSync(crossinline runTest: () -> Unit) {
         getInstrumentation().runOnMainSync { runTest() }
     }
+
+    /** Returns a property to read/write the value of a [ConstantItem]. */
+    fun <T : Any> ConstantItem<T>.asProperty(context: Context): ReadWriteProperty<Any?, T> {
+        return TaskbarItemProperty(context, this)
+    }
+
+    private class TaskbarItemProperty<T : Any>(
+        private val context: Context,
+        private val item: ConstantItem<T>,
+    ) : ReadWriteProperty<Any?, T> {
+
+        override fun getValue(thisRef: Any?, property: KProperty<*>): T {
+            return LauncherPrefs.get(context).get(item)
+        }
+
+        override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
+            runOnMainSync { LauncherPrefs.get(context).put(item, value) }
+        }
+    }
 }
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarDesktopModeControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarDesktopModeControllerTest.kt
index 72bbfc9..455b6c5 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarDesktopModeControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarDesktopModeControllerTest.kt
@@ -16,7 +16,6 @@
 
 package com.android.launcher3.taskbar
 
-import androidx.test.platform.app.InstrumentationRegistry
 import com.android.launcher3.taskbar.TaskbarBackgroundRenderer.Companion.MAX_ROUNDNESS
 import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule
 import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext
@@ -30,12 +29,8 @@
 @LauncherMultivalentJUnit.EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"])
 class TaskbarDesktopModeControllerTest {
 
-    private val context =
-        TaskbarWindowSandboxContext.create(
-            InstrumentationRegistry.getInstrumentation().targetContext
-        )
-
-    @get:Rule(order = 0) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context)
+    @get:Rule(order = 0) val context = TaskbarWindowSandboxContext.create()
+    @get:Rule(order = 1) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context)
 
     @TaskbarUnitTestRule.InjectController
     lateinit var taskbarDesktopModeController: TaskbarDesktopModeController
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarEduTooltipControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarEduTooltipControllerTest.kt
index 961d4dc..3c80352 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarEduTooltipControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarEduTooltipControllerTest.kt
@@ -14,25 +14,16 @@
  * limitations under the License.
  */
 
-package com.android.launcher3.taskbar.test
+package com.android.launcher3.taskbar
 
-import android.util.Log
-import androidx.test.platform.app.InstrumentationRegistry
 import com.android.launcher3.Utilities
-import com.android.launcher3.taskbar.TOOLTIP_STEP_FEATURES
-import com.android.launcher3.taskbar.TOOLTIP_STEP_NONE
-import com.android.launcher3.taskbar.TOOLTIP_STEP_PINNING
-import com.android.launcher3.taskbar.TOOLTIP_STEP_SWIPE
-import com.android.launcher3.taskbar.TaskbarActivityContext
+import com.android.launcher3.taskbar.TaskbarControllerTestUtil.asProperty
 import com.android.launcher3.taskbar.TaskbarControllerTestUtil.runOnMainSync
-import com.android.launcher3.taskbar.TaskbarEduTooltipController
 import com.android.launcher3.taskbar.rules.TaskbarModeRule
 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.taskbar.rules.TaskbarPinningPreferenceRule
-import com.android.launcher3.taskbar.rules.TaskbarPreferenceRule
 import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule
 import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController
 import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext
@@ -42,40 +33,19 @@
 import com.google.common.truth.Truth.assertThat
 import org.junit.After
 import org.junit.Before
-import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
 @RunWith(LauncherMultivalentJUnit::class)
 @EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"])
-@Ignore
 class TaskbarEduTooltipControllerTest {
 
-    private val context =
-        TaskbarWindowSandboxContext.create(
-            InstrumentationRegistry.getInstrumentation().targetContext
-        )
+    @get:Rule(order = 0) val context = TaskbarWindowSandboxContext.create()
 
-    @get:Rule(order = 0)
-    val tooltipStepPreferenceRule =
-        TaskbarPreferenceRule(
-            context,
-            OnboardingPrefs.TASKBAR_EDU_TOOLTIP_STEP.prefItem,
-        )
+    @get:Rule(order = 1) val taskbarModeRule = TaskbarModeRule(context)
 
-    @get:Rule(order = 1)
-    val searchEduPreferenceRule =
-        TaskbarPreferenceRule(
-            context,
-            OnboardingPrefs.TASKBAR_SEARCH_EDU_SEEN,
-        )
-
-    @get:Rule(order = 2) val taskbarPinningPreferenceRule = TaskbarPinningPreferenceRule(context)
-
-    @get:Rule(order = 3) val taskbarModeRule = TaskbarModeRule(context)
-
-    @get:Rule(order = 4) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context)
+    @get:Rule(order = 2) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context)
 
     @InjectController lateinit var taskbarEduTooltipController: TaskbarEduTooltipController
 
@@ -84,9 +54,11 @@
 
     private val wasInTestHarness = Utilities.isRunningInTestHarness()
 
+    private var tooltipStep by OnboardingPrefs.TASKBAR_EDU_TOOLTIP_STEP.prefItem.asProperty(context)
+    private var searchEduSeen by OnboardingPrefs.TASKBAR_SEARCH_EDU_SEEN.asProperty(context)
+
     @Before
     fun setUp() {
-        Log.e("Taskbar", "TaskbarEduTooltipControllerTest test started")
         Utilities.disableRunningInTestHarnessForTests()
     }
 
@@ -95,13 +67,12 @@
         if (wasInTestHarness) {
             Utilities.enableRunningInTestHarnessForTests()
         }
-        Log.e("Taskbar", "TaskbarEduTooltipControllerTest test completed")
     }
 
     @Test
     @TaskbarMode(THREE_BUTTONS)
     fun testMaybeShowSwipeEdu_whenTaskbarIsInThreeButtonMode_doesNotShowSwipeEdu() {
-        tooltipStepPreferenceRule.value = TOOLTIP_STEP_SWIPE
+        tooltipStep = TOOLTIP_STEP_SWIPE
         assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_SWIPE)
         runOnMainSync { taskbarEduTooltipController.maybeShowSwipeEdu() }
         assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_SWIPE)
@@ -111,7 +82,7 @@
     @Test
     @TaskbarMode(TRANSIENT)
     fun testMaybeShowSwipeEdu_whenSwipeEduAlreadyShown_doesNotShowSwipeEdu() {
-        tooltipStepPreferenceRule.value = TOOLTIP_STEP_FEATURES
+        tooltipStep = TOOLTIP_STEP_FEATURES
         assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_FEATURES)
         runOnMainSync { taskbarEduTooltipController.maybeShowSwipeEdu() }
         assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_FEATURES)
@@ -121,7 +92,7 @@
     @Test
     @TaskbarMode(TRANSIENT)
     fun testMaybeShowSwipeEdu_whenUserHasNotSeen_doesShowSwipeEdu() {
-        tooltipStepPreferenceRule.value = TOOLTIP_STEP_SWIPE
+        tooltipStep = TOOLTIP_STEP_SWIPE
         assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_SWIPE)
         runOnMainSync { taskbarEduTooltipController.maybeShowSwipeEdu() }
         assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_FEATURES)
@@ -131,7 +102,7 @@
     @Test
     @TaskbarMode(TRANSIENT)
     fun testMaybeShowFeaturesEdu_whenFeatureEduAlreadyShown_doesNotShowFeatureEdu() {
-        tooltipStepPreferenceRule.value = TOOLTIP_STEP_NONE
+        tooltipStep = TOOLTIP_STEP_NONE
         assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_NONE)
         runOnMainSync { taskbarEduTooltipController.maybeShowFeaturesEdu() }
         assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_NONE)
@@ -141,7 +112,7 @@
     @Test
     @TaskbarMode(TRANSIENT)
     fun testMaybeShowFeaturesEdu_whenUserHasNotSeen_doesShowFeatureEdu() {
-        tooltipStepPreferenceRule.value = TOOLTIP_STEP_FEATURES
+        tooltipStep = TOOLTIP_STEP_FEATURES
         assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_FEATURES)
         runOnMainSync { taskbarEduTooltipController.maybeShowFeaturesEdu() }
         assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_NONE)
@@ -151,7 +122,7 @@
     @Test
     @TaskbarMode(THREE_BUTTONS)
     fun testMaybeShowPinningEdu_whenTaskbarIsInThreeButtonMode_doesNotShowPinningEdu() {
-        tooltipStepPreferenceRule.value = TOOLTIP_STEP_PINNING
+        tooltipStep = TOOLTIP_STEP_PINNING
         assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_PINNING)
         runOnMainSync { taskbarEduTooltipController.maybeShowFeaturesEdu() }
         assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_PINNING)
@@ -162,7 +133,7 @@
     @TaskbarMode(TRANSIENT)
     fun testMaybeShowPinningEdu_whenUserHasNotSeen_doesShowPinningEdu() {
         // Test standalone pinning edu, where user has seen taskbar edu before, but not pinning edu.
-        tooltipStepPreferenceRule.value = TOOLTIP_STEP_PINNING
+        tooltipStep = TOOLTIP_STEP_PINNING
         assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_PINNING)
         runOnMainSync { taskbarEduTooltipController.maybeShowFeaturesEdu() }
         assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_NONE)
@@ -172,21 +143,21 @@
     @Test
     @TaskbarMode(TRANSIENT)
     fun testIsBeforeTooltipFeaturesStep_whenUserHasNotSeenFeatureEdu_shouldReturnTrue() {
-        tooltipStepPreferenceRule.value = TOOLTIP_STEP_SWIPE
+        tooltipStep = TOOLTIP_STEP_SWIPE
         assertThat(taskbarEduTooltipController.isBeforeTooltipFeaturesStep).isTrue()
     }
 
     @Test
     @TaskbarMode(TRANSIENT)
     fun testIsBeforeTooltipFeaturesStep_whenUserHasSeenFeatureEdu_shouldReturnFalse() {
-        tooltipStepPreferenceRule.value = TOOLTIP_STEP_NONE
+        tooltipStep = TOOLTIP_STEP_NONE
         assertThat(taskbarEduTooltipController.isBeforeTooltipFeaturesStep).isFalse()
     }
 
     @Test
     @TaskbarMode(TRANSIENT)
     fun testHide_whenTooltipIsOpen_shouldCloseTooltip() {
-        tooltipStepPreferenceRule.value = TOOLTIP_STEP_SWIPE
+        tooltipStep = TOOLTIP_STEP_SWIPE
         assertThat(taskbarEduTooltipController.tooltipStep).isEqualTo(TOOLTIP_STEP_SWIPE)
         assertThat(taskbarEduTooltipController.isTooltipOpen).isFalse()
         runOnMainSync { taskbarEduTooltipController.maybeShowSwipeEdu() }
@@ -206,7 +177,7 @@
     @Test
     @TaskbarMode(PINNED)
     fun testMaybeShowSearchEdu_whenTaskbarIsPinnedAndUserHasSeenSearchEdu_shouldNotShowSearchEdu() {
-        searchEduPreferenceRule.value = true
+        searchEduSeen = true
         assertThat(taskbarEduTooltipController.userHasSeenSearchEdu).isTrue()
         runOnMainSync { taskbarEduTooltipController.hide() }
         assertThat(taskbarEduTooltipController.isTooltipOpen).isFalse()
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 02d6218..4b04dba 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java
@@ -39,7 +39,7 @@
 import com.android.launcher3.taskbar.TaskbarNavButtonController.TaskbarNavButtonCallbacks;
 import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.TouchInteractionService;
-import com.android.quickstep.util.AssistUtils;
+import com.android.quickstep.util.ContextualSearchInvoker;
 import com.android.systemui.contextualeducation.GestureType;
 
 import org.junit.Before;
@@ -64,7 +64,7 @@
     @Mock
     Handler mockHandler;
     @Mock
-    AssistUtils mockAssistUtils;
+    ContextualSearchInvoker mockContextualSearchInvoker;
     @Mock
     StatsLogManager mockStatsLogManager;
     @Mock
@@ -109,13 +109,13 @@
                 mockSystemUiProxy,
                 mockContextualEduStatsManager,
                 mockHandler,
-                mockAssistUtils);
+                mockContextualSearchInvoker);
     }
 
     @Test
     public void testPressBack() {
         mNavButtonController.onButtonClick(BUTTON_BACK, mockView);
-        verify(mockSystemUiProxy, times(1)).onBackPressed();
+        verify(mockSystemUiProxy, times(1)).onBackEvent(null);
     }
 
     @Test
@@ -166,40 +166,40 @@
     @Test
     public void testLongPressHome_enabled_withoutOverride() {
         mNavButtonController.setAssistantLongPressEnabled(true /*assistantLongPressEnabled*/);
-        when(mockAssistUtils.tryStartAssistOverride(anyInt())).thenReturn(false);
+        when(mockContextualSearchInvoker.tryStartAssistOverride(anyInt())).thenReturn(false);
 
         mNavButtonController.onButtonLongClick(BUTTON_HOME, mockView);
-        verify(mockAssistUtils, times(1)).tryStartAssistOverride(anyInt());
+        verify(mockContextualSearchInvoker, times(1)).tryStartAssistOverride(anyInt());
         verify(mockSystemUiProxy, times(1)).startAssistant(any());
     }
 
     @Test
     public void testLongPressHome_enabled_withOverride() {
         mNavButtonController.setAssistantLongPressEnabled(true /*assistantLongPressEnabled*/);
-        when(mockAssistUtils.tryStartAssistOverride(anyInt())).thenReturn(true);
+        when(mockContextualSearchInvoker.tryStartAssistOverride(anyInt())).thenReturn(true);
 
         mNavButtonController.onButtonLongClick(BUTTON_HOME, mockView);
-        verify(mockAssistUtils, times(1)).tryStartAssistOverride(anyInt());
+        verify(mockContextualSearchInvoker, times(1)).tryStartAssistOverride(anyInt());
         verify(mockSystemUiProxy, never()).startAssistant(any());
     }
 
     @Test
     public void testLongPressHome_disabled_withoutOverride() {
         mNavButtonController.setAssistantLongPressEnabled(false /*assistantLongPressEnabled*/);
-        when(mockAssistUtils.tryStartAssistOverride(anyInt())).thenReturn(false);
+        when(mockContextualSearchInvoker.tryStartAssistOverride(anyInt())).thenReturn(false);
 
         mNavButtonController.onButtonLongClick(BUTTON_HOME, mockView);
-        verify(mockAssistUtils, never()).tryStartAssistOverride(anyInt());
+        verify(mockContextualSearchInvoker, never()).tryStartAssistOverride(anyInt());
         verify(mockSystemUiProxy, never()).startAssistant(any());
     }
 
     @Test
     public void testLongPressHome_disabled_withOverride() {
         mNavButtonController.setAssistantLongPressEnabled(false /*assistantLongPressEnabled*/);
-        when(mockAssistUtils.tryStartAssistOverride(anyInt())).thenReturn(true);
+        when(mockContextualSearchInvoker.tryStartAssistOverride(anyInt())).thenReturn(true);
 
         mNavButtonController.onButtonLongClick(BUTTON_HOME, mockView);
-        verify(mockAssistUtils, never()).tryStartAssistOverride(anyInt());
+        verify(mockContextualSearchInvoker, never()).tryStartAssistOverride(anyInt());
         verify(mockSystemUiProxy, never()).startAssistant(any());
     }
 
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarScrimViewControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarScrimViewControllerTest.kt
new file mode 100644
index 0000000..4c94067
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarScrimViewControllerTest.kt
@@ -0,0 +1,193 @@
+/*
+ * 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.animation.AnimatorTestRule
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
+import android.view.KeyEvent
+import android.view.View.GONE
+import android.view.View.VISIBLE
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController
+import com.android.launcher3.taskbar.rules.TaskbarModeRule
+import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.PINNED
+import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.TRANSIENT
+import com.android.launcher3.taskbar.rules.TaskbarModeRule.TaskbarMode
+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.quickstep.SystemUiProxy
+import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED
+import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED
+import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE
+import com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_BAR
+import com.android.wm.shell.shared.bubbles.BubbleConstants.BUBBLE_EXPANDED_SCRIM_ALPHA
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(LauncherMultivalentJUnit::class)
+@EmulatedDevices(["pixelTablet2023"])
+class TaskbarScrimViewControllerTest {
+    @get:Rule(order = 0) val setFlagsRule = SetFlagsRule()
+    @get:Rule(order = 1)
+    val context =
+        TaskbarWindowSandboxContext.create { builder ->
+            builder.bindSystemUiProxy(
+                object : SystemUiProxy(this) {
+                    override fun onBackEvent(backEvent: KeyEvent?) {
+                        super.onBackEvent(backEvent)
+                        backPressed = true
+                    }
+                }
+            )
+        }
+    @get:Rule(order = 2) val taskbarModeRule = TaskbarModeRule(context)
+    @get:Rule(order = 3) val animatorTestRule = AnimatorTestRule(this)
+    @get:Rule(order = 4) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context)
+
+    @InjectController lateinit var scrimViewController: TaskbarScrimViewController
+
+    // Default animation duration.
+    private val animationDuration =
+        context.resources.getInteger(android.R.integer.config_mediumAnimTime).toLong()
+
+    private var backPressed = false
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testOnTaskbarVisibleChanged_onlyTaskbarVisible_noScrim() {
+        getInstrumentation().runOnMainSync {
+            scrimViewController.onTaskbarVisibilityChanged(VISIBLE)
+            scrimViewController.updateStateForSysuiFlags(0, true)
+        }
+        assertThat(scrimViewController.scrimAlpha).isEqualTo(0)
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testOnTaskbarVisibilityChanged_pinnedTaskbarVisibleWithBubblesExpanded_showsScrim() {
+        getInstrumentation().runOnMainSync {
+            scrimViewController.updateStateForSysuiFlags(SYSUI_STATE_BUBBLES_EXPANDED, true)
+            scrimViewController.onTaskbarVisibilityChanged(VISIBLE)
+            animatorTestRule.advanceTimeBy(animationDuration)
+        }
+
+        assertThat(scrimViewController.scrimAlpha).isEqualTo(BUBBLE_EXPANDED_SCRIM_ALPHA)
+    }
+
+    @Test
+    @DisableFlags(FLAG_ENABLE_BUBBLE_BAR)
+    @TaskbarMode(PINNED)
+    fun testOnTaskbarVisibilityChanged_pinnedTaskbarHiddenDuringScrim_hidesScrim() {
+        getInstrumentation().runOnMainSync {
+            scrimViewController.onTaskbarVisibilityChanged(VISIBLE)
+            scrimViewController.updateStateForSysuiFlags(SYSUI_STATE_BUBBLES_EXPANDED, true)
+        }
+        assertThat(scrimViewController.scrimAlpha).isEqualTo(BUBBLE_EXPANDED_SCRIM_ALPHA)
+
+        getInstrumentation().runOnMainSync {
+            scrimViewController.onTaskbarVisibilityChanged(GONE)
+            animatorTestRule.advanceTimeBy(animationDuration)
+        }
+        assertThat(scrimViewController.scrimAlpha).isEqualTo(0)
+    }
+
+    @Test
+    @EnableFlags(FLAG_ENABLE_BUBBLE_BAR)
+    @TaskbarMode(PINNED)
+    fun testOnTaskbarVisibilityChanged_pinnedTaskbarOnHomeHiddenDuringScrim_hidesScrim() {
+        getInstrumentation().runOnMainSync {
+            scrimViewController.onTaskbarVisibilityChanged(VISIBLE)
+            taskbarUnitTestRule.activityContext.bubbleControllers!!
+                .bubbleStashController
+                .launcherState = BubbleStashController.BubbleLauncherState.HOME
+            scrimViewController.updateStateForSysuiFlags(SYSUI_STATE_BUBBLES_EXPANDED, true)
+        }
+        assertThat(scrimViewController.scrimAlpha).isEqualTo(BUBBLE_EXPANDED_SCRIM_ALPHA)
+
+        getInstrumentation().runOnMainSync {
+            scrimViewController.onTaskbarVisibilityChanged(GONE)
+            animatorTestRule.advanceTimeBy(animationDuration)
+        }
+        assertThat(scrimViewController.scrimAlpha).isEqualTo(0)
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testOnTaskbarVisibilityChanged_notificationsOverPinnedTaskbarAndBubbles_noScrim() {
+        getInstrumentation().runOnMainSync {
+            scrimViewController.updateStateForSysuiFlags(
+                SYSUI_STATE_BUBBLES_EXPANDED or SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE,
+                true,
+            )
+            scrimViewController.onTaskbarVisibilityChanged(VISIBLE)
+        }
+        assertThat(scrimViewController.scrimAlpha).isEqualTo(0)
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testOnTaskbarVisibilityChanged_pinnedTaskbarWithBubbleMenu_darkerScrim() {
+        getInstrumentation().runOnMainSync {
+            scrimViewController.onTaskbarVisibilityChanged(VISIBLE)
+            scrimViewController.updateStateForSysuiFlags(
+                SYSUI_STATE_BUBBLES_EXPANDED or SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED,
+                true,
+            )
+        }
+        assertThat(scrimViewController.scrimAlpha).isGreaterThan(BUBBLE_EXPANDED_SCRIM_ALPHA)
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testOnTaskbarVisibilityChanged_stashedTaskbarWithBubbles_noScrim() {
+        getInstrumentation().runOnMainSync {
+            scrimViewController.updateStateForSysuiFlags(SYSUI_STATE_BUBBLES_EXPANDED, true)
+            scrimViewController.onTaskbarVisibilityChanged(VISIBLE)
+        }
+        assertThat(scrimViewController.scrimAlpha).isEqualTo(0)
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testOnClick_scrimShown_performsSystemBack() {
+        getInstrumentation().runOnMainSync {
+            scrimViewController.updateStateForSysuiFlags(SYSUI_STATE_BUBBLES_EXPANDED, true)
+            scrimViewController.onTaskbarVisibilityChanged(VISIBLE)
+        }
+        assertThat(scrimViewController.scrimView.isClickable).isTrue()
+
+        getInstrumentation().runOnMainSync { scrimViewController.scrimView.performClick() }
+        assertThat(backPressed).isTrue()
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testOnClick_scrimHidden_notClickable() {
+        getInstrumentation().runOnMainSync {
+            scrimViewController.updateStateForSysuiFlags(SYSUI_STATE_BUBBLES_EXPANDED, true)
+            scrimViewController.onTaskbarVisibilityChanged(VISIBLE)
+        }
+        assertThat(scrimViewController.scrimView.isClickable).isFalse()
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarStashControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarStashControllerTest.kt
new file mode 100644
index 0000000..71f4ef4
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarStashControllerTest.kt
@@ -0,0 +1,680 @@
+/*
+ * 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.animation.AnimatorTestRule
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.android.launcher3.LauncherPrefs.Companion.TASKBAR_PINNING
+import com.android.launcher3.R
+import com.android.launcher3.taskbar.StashedHandleViewController.ALPHA_INDEX_STASHED
+import com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_EDU_OPEN
+import com.android.launcher3.taskbar.TaskbarControllerTestUtil.asProperty
+import com.android.launcher3.taskbar.TaskbarStashController.FLAG_IN_APP
+import com.android.launcher3.taskbar.TaskbarStashController.FLAG_IN_OVERVIEW
+import com.android.launcher3.taskbar.TaskbarStashController.FLAG_IN_STASHED_LAUNCHER_STATE
+import com.android.launcher3.taskbar.TaskbarStashController.FLAG_STASHED_DEVICE_LOCKED
+import com.android.launcher3.taskbar.TaskbarStashController.FLAG_STASHED_IME
+import com.android.launcher3.taskbar.TaskbarStashController.FLAG_STASHED_IN_APP_AUTO
+import com.android.launcher3.taskbar.TaskbarStashController.FLAG_STASHED_SMALL_SCREEN
+import com.android.launcher3.taskbar.TaskbarStashController.FLAG_STASHED_SYSUI
+import com.android.launcher3.taskbar.TaskbarStashController.TASKBAR_STASH_DURATION
+import com.android.launcher3.taskbar.TaskbarStashController.TASKBAR_STASH_DURATION_FOR_IME
+import com.android.launcher3.taskbar.TaskbarStashController.TRANSIENT_TASKBAR_STASH_ALPHA_DURATION
+import com.android.launcher3.taskbar.TaskbarStashController.TRANSIENT_TASKBAR_STASH_DURATION
+import com.android.launcher3.taskbar.TaskbarViewController.ALPHA_INDEX_STASH
+import com.android.launcher3.taskbar.bubbles.BubbleBarViewController
+import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController
+import com.android.launcher3.taskbar.rules.TaskbarModeRule
+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.taskbar.rules.TaskbarUnitTestRule
+import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController
+import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.UserSetupMode
+import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext
+import com.android.launcher3.util.LauncherMultivalentJUnit
+import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices
+import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED
+import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SHOWING
+import com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_BAR
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(LauncherMultivalentJUnit::class)
+@EnableFlags(FLAG_ENABLE_BUBBLE_BAR)
+@EmulatedDevices(["pixelTablet2023"])
+class TaskbarStashControllerTest {
+    @get:Rule(order = 0) val setFlagsRule = SetFlagsRule()
+    @get:Rule(order = 1) val context = TaskbarWindowSandboxContext.create()
+    @get:Rule(order = 2) val taskbarModeRule = TaskbarModeRule(context)
+    @get:Rule(order = 4) val animatorTestRule = AnimatorTestRule(this)
+    @get:Rule(order = 5) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context)
+
+    @InjectController lateinit var stashController: TaskbarStashController
+    @InjectController lateinit var viewController: TaskbarViewController
+    @InjectController lateinit var stashedHandleViewController: StashedHandleViewController
+    @InjectController lateinit var dragLayerController: TaskbarDragLayerController
+    @InjectController lateinit var autohideSuspendController: TaskbarAutohideSuspendController
+    @InjectController lateinit var bubbleBarViewController: BubbleBarViewController
+    @InjectController lateinit var bubbleStashController: BubbleStashController
+
+    private val activityContext by taskbarUnitTestRule::activityContext
+
+    // Disable hardware keyboard mode during tests.
+    @Before fun enableSoftwareIme() = TaskbarStashController.enableSoftwareImeForTests(true)
+
+    @After fun resetIme() = TaskbarStashController.enableSoftwareImeForTests(false)
+
+    @After fun cancelTimeoutIfExists() = stashController.cancelTimeoutIfExists()
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testInit_transientMode_stashedInApp() {
+        assertThat(stashController.isStashedInApp).isTrue()
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testInit_pinnedMode_unstashedInApp() {
+        assertThat(stashController.isStashedInApp).isFalse()
+    }
+
+    @Test
+    @UserSetupMode
+    @TaskbarMode(PINNED)
+    fun testInit_userSetupWithPinnedMode_stashedInApp() {
+        assertThat(stashController.isStashedInApp).isTrue()
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testSetSetupUiVisible_true_stashedInApp() {
+        getInstrumentation().runOnMainSync { stashController.setSetupUIVisible(true) }
+        assertThat(stashController.isStashedInApp).isTrue()
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testSetSetupUiVisible_false_unstashedInApp() {
+        getInstrumentation().runOnMainSync { stashController.setSetupUIVisible(false) }
+        assertThat(stashController.isStashedInApp).isFalse()
+    }
+
+    @Test
+    fun testRecreateAsTransient_timeoutStarted() {
+        var isPinned by TASKBAR_PINNING.asProperty(context)
+        isPinned = true
+        activityContext.controllers.sharedState?.taskbarWasPinned = true
+
+        isPinned = false
+        assertThat(stashController.timeoutAlarm.alarmPending()).isTrue()
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testSupportsVisualStashing_transientMode_supported() {
+        assertThat(stashController.supportsVisualStashing()).isTrue()
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testSupportsVisualStashing_pinnedMode_supported() {
+        assertThat(stashController.supportsVisualStashing()).isTrue()
+    }
+
+    @Test
+    @TaskbarMode(THREE_BUTTONS)
+    fun testSupportsVisualStashing_threeButtonsMode_unsupported() {
+        assertThat(stashController.supportsVisualStashing()).isFalse()
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testGetStashDuration_transientMode() {
+        assertThat(stashController.stashDuration).isEqualTo(TRANSIENT_TASKBAR_STASH_DURATION)
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testGetStashDuration_pinnedMode() {
+        assertThat(stashController.stashDuration).isEqualTo(TASKBAR_STASH_DURATION)
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testIsStashed_pinnedInApp_isUnstashed() {
+        getInstrumentation().runOnMainSync {
+            stashController.updateStateForFlag(FLAG_IN_APP, true)
+            stashController.applyState(0)
+        }
+        assertThat(stashController.isStashed).isFalse()
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testIsStashed_transientInApp_isStashed() {
+        getInstrumentation().runOnMainSync {
+            stashController.updateStateForFlag(FLAG_IN_APP, true)
+            stashController.applyState(0)
+        }
+        assertThat(stashController.isStashed).isTrue()
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testIsStashed_transientNotInApp_isUnstashed() {
+        getInstrumentation().runOnMainSync {
+            stashController.updateStateForFlag(FLAG_IN_APP, false)
+            stashController.applyState(0)
+        }
+        assertThat(stashController.isStashed).isFalse()
+    }
+
+    @Test
+    fun testIsStashed_stashedInLauncherState_isStashed() {
+        getInstrumentation().runOnMainSync {
+            stashController.updateStateForFlag(FLAG_IN_APP, false)
+            stashController.updateStateForFlag(FLAG_IN_STASHED_LAUNCHER_STATE, true)
+            stashController.applyState(0)
+        }
+        assertThat(stashController.isStashed).isTrue()
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testIsStashed_transientInOverview_isUnstashed() {
+        getInstrumentation().runOnMainSync {
+            stashController.updateStateForFlag(FLAG_IN_APP, false)
+            stashController.updateStateForFlag(FLAG_IN_OVERVIEW, true)
+            stashController.applyState(0)
+        }
+        assertThat(stashController.isStashed).isFalse()
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testIsStashed_pinnedInOverviewWithIme_isStashed() {
+        getInstrumentation().runOnMainSync {
+            stashController.updateStateForFlag(FLAG_IN_APP, false)
+            stashController.updateStateForFlag(FLAG_IN_OVERVIEW, true)
+            stashController.updateStateForFlag(FLAG_STASHED_IME, true)
+            stashController.applyState(0)
+        }
+        assertThat(stashController.isStashed).isTrue()
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testIsStashed_pinnedTaskbarWithPinnedApp_isStashed() {
+        getInstrumentation().runOnMainSync {
+            stashController.updateStateForFlag(FLAG_IN_APP, true)
+            stashController.updateStateForFlag(FLAG_STASHED_SYSUI, true) // App pinned.
+            stashController.applyState(0)
+        }
+        assertThat(stashController.isStashed).isTrue()
+    }
+
+    @Test
+    fun testIsInStashedLauncherState_flagUnset_false() {
+        stashController.updateStateForFlag(FLAG_IN_STASHED_LAUNCHER_STATE, false)
+        assertThat(stashController.isInStashedLauncherState).isFalse()
+    }
+
+    @Test
+    @TaskbarMode(THREE_BUTTONS)
+    fun testIsInStashedLauncherState_flagSetInThreeButtonsMode_false() {
+        stashController.updateStateForFlag(FLAG_IN_STASHED_LAUNCHER_STATE, true)
+        assertThat(stashController.isInStashedLauncherState).isFalse()
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testIsInStashedLauncherState_flagSetInPinnedMode_true() {
+        stashController.updateStateForFlag(FLAG_IN_STASHED_LAUNCHER_STATE, true)
+        assertThat(stashController.isInStashedLauncherState).isTrue()
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testIsTaskbarVisibleAndNotStashing_pinnedButNotVisible_false() {
+        getInstrumentation().runOnMainSync {
+            viewController.taskbarIconAlpha.get(ALPHA_INDEX_STASH).value = 0f
+        }
+        assertThat(stashController.isTaskbarVisibleAndNotStashing).isFalse()
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testIsTaskbarVisibleAndNotStashing_visibleButStashed_false() {
+        getInstrumentation().runOnMainSync {
+            viewController.taskbarIconAlpha.get(ALPHA_INDEX_STASH).value = 1f
+        }
+        assertThat(stashController.isTaskbarVisibleAndNotStashing).isFalse()
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testIsTaskbarVisibleAndNotStashing_pinnedAndVisible_true() {
+        getInstrumentation().runOnMainSync {
+            viewController.taskbarIconAlpha.get(ALPHA_INDEX_STASH).value = 1f
+        }
+        assertThat(stashController.isTaskbarVisibleAndNotStashing).isTrue()
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testGetTouchableHeight_isStashed_stashedHeight() {
+        assertThat(stashController.touchableHeight).isEqualTo(stashController.stashedHeight)
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testGetTouchableHeight_unstashedTransientMode_heightAndBottomMargin() {
+        getInstrumentation().runOnMainSync {
+            stashController.updateStateForFlag(FLAG_STASHED_IN_APP_AUTO, false)
+            stashController.applyState(0)
+        }
+
+        val expectedHeight =
+            activityContext.deviceProfile.run { taskbarHeight + taskbarBottomMargin }
+        assertThat(stashController.touchableHeight).isEqualTo(expectedHeight)
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testGetTouchableHeight_pinnedMode_taskbarHeight() {
+        assertThat(stashController.touchableHeight)
+            .isEqualTo(activityContext.deviceProfile.taskbarHeight)
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testGetContentHeightToReportToApps_transientMode_stashedHeight() {
+        assertThat(stashController.contentHeightToReportToApps)
+            .isEqualTo(stashController.stashedHeight)
+    }
+
+    @Test
+    @TaskbarMode(THREE_BUTTONS)
+    fun testGetContentHeightToReportToApps_threeButtonsMode_taskbarHeight() {
+        assertThat(stashController.contentHeightToReportToApps)
+            .isEqualTo(activityContext.deviceProfile.taskbarHeight)
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testGetContentHeightToReportToApps_pinnedMode_taskbarHeight() {
+        assertThat(stashController.contentHeightToReportToApps)
+            .isEqualTo(activityContext.deviceProfile.taskbarHeight)
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    @UserSetupMode
+    fun testGetContentHeightToReportToApps_pinnedInSetupMode_setupWizardInsets() {
+        assertThat(stashController.contentHeightToReportToApps)
+            .isEqualTo(context.resources.getDimensionPixelSize(R.dimen.taskbar_suw_insets))
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testGetContentHeightToReportToApps_pinnedModeButFolded_stashedHeight() {
+        getInstrumentation().runOnMainSync {
+            stashedHandleViewController.stashedHandleAlpha.get(ALPHA_INDEX_STASHED).value = 1f
+            stashController.updateStateForFlag(FLAG_STASHED_SMALL_SCREEN, true)
+        }
+        assertThat(stashController.contentHeightToReportToApps)
+            .isEqualTo(stashController.stashedHeight)
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testGetContentHeightToReportToApps_homeDisabledWhenFolded_zeroHeight() {
+        getInstrumentation().runOnMainSync {
+            stashedHandleViewController.stashedHandleAlpha.get(ALPHA_INDEX_STASHED).value = 1f
+            stashedHandleViewController.setIsHomeButtonDisabled(true)
+            stashController.updateStateForFlag(FLAG_STASHED_SMALL_SCREEN, true)
+        }
+        assertThat(stashController.contentHeightToReportToApps).isEqualTo(0)
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testGetTappableHeightToReportToApps_transientMode_zeroHeight() {
+        assertThat(stashController.tappableHeightToReportToApps).isEqualTo(0)
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testGetTappableHeightToReportToApps_pinnedMode_taskbarHeight() {
+        assertThat(stashController.tappableHeightToReportToApps)
+            .isEqualTo(activityContext.deviceProfile.taskbarHeight)
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testUpdateAndAnimateTransientTaskbar_unstashTaskbar_updatesState() {
+        getInstrumentation().runOnMainSync {
+            stashController.updateAndAnimateTransientTaskbar(false)
+        }
+        assertThat(stashController.isStashed).isFalse()
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testUpdateAndAnimateTransientTaskbar_runUnstashAnimation_startsTaskbarTimeout() {
+        getInstrumentation().runOnMainSync {
+            stashController.updateAndAnimateTransientTaskbar(false)
+            animatorTestRule.advanceTimeBy(stashController.stashDuration)
+        }
+        assertThat(stashController.timeoutAlarm.alarmPending()).isTrue()
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testUpdateAndAnimateTransientTaskbar_finishTaskbarTimeout_taskbarStashes() {
+        getInstrumentation().runOnMainSync {
+            stashController.updateAndAnimateTransientTaskbar(false)
+            animatorTestRule.advanceTimeBy(stashController.stashDuration)
+        }
+        assertThat(stashController.timeoutAlarm.alarmPending()).isTrue()
+
+        getInstrumentation().runOnMainSync {
+            stashController.timeoutAlarm.finishAlarm()
+            animatorTestRule.advanceTimeBy(stashController.stashDuration)
+        }
+        assertThat(stashController.isStashed).isTrue()
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testUpdateAndAnimateTransientTaskbar_autoHideSuspendedForEdu_remainsUnstashed() {
+        getInstrumentation().runOnMainSync {
+            stashController.updateAndAnimateTransientTaskbar(false)
+            animatorTestRule.advanceTimeBy(stashController.stashDuration)
+        }
+
+        getInstrumentation().runOnMainSync {
+            autohideSuspendController.updateFlag(FLAG_AUTOHIDE_SUSPEND_EDU_OPEN, true)
+            stashController.updateAndAnimateTransientTaskbar(true)
+            animatorTestRule.advanceTimeBy(stashController.stashDuration)
+        }
+        assertThat(stashController.isStashed).isFalse()
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testUpdateAndAnimateTransientTaskbar_unstashTaskbarWithBubbles_bubbleBarUnstashes() {
+        getInstrumentation().runOnMainSync {
+            bubbleBarViewController.setHiddenForBubbles(false)
+            bubbleStashController.stashBubbleBarImmediate()
+            stashController.updateAndAnimateTransientTaskbar(false, true)
+        }
+        assertThat(bubbleStashController.isStashed).isFalse()
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testUpdateAndAnimateTransientTaskbar_unstashTaskbarWithoutBubbles_bubbleBarStashed() {
+        getInstrumentation().runOnMainSync {
+            bubbleBarViewController.setHiddenForBubbles(false)
+            bubbleStashController.stashBubbleBarImmediate()
+            stashController.updateAndAnimateTransientTaskbar(false, false)
+        }
+        assertThat(bubbleStashController.isStashed).isTrue()
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testUpdateAndAnimateTransientTaskbar_stashTaskbarWithBubbles_bubbleBarStashes() {
+        getInstrumentation().runOnMainSync {
+            bubbleBarViewController.setHiddenForBubbles(false)
+            bubbleStashController.showBubbleBarImmediate()
+            stashController.updateAndAnimateTransientTaskbar(true, true)
+        }
+        assertThat(bubbleStashController.isStashed).isTrue()
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testUpdateAndAnimateTransientTaskbar_stashTaskbarWithoutBubbles_bubbleBarUnstashed() {
+        getInstrumentation().runOnMainSync {
+            bubbleBarViewController.setHiddenForBubbles(false)
+            bubbleStashController.showBubbleBarImmediate()
+            stashController.updateAndAnimateTransientTaskbar(true, false)
+        }
+        assertThat(bubbleStashController.isStashed).isFalse()
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testUpdateAndAnimateTransientTaskbar_bubbleBarExpandedBeforeTimeout_expandedAfterwards() {
+        getInstrumentation().runOnMainSync {
+            bubbleBarViewController.setHiddenForBubbles(false)
+            bubbleBarViewController.isExpanded = true
+            stashController.updateAndAnimateTransientTaskbar(false)
+            animatorTestRule.advanceTimeBy(stashController.stashDuration)
+        }
+        assertThat(stashController.timeoutAlarm.alarmPending()).isTrue()
+
+        getInstrumentation().runOnMainSync {
+            stashController.timeoutAlarm.finishAlarm()
+            animatorTestRule.advanceTimeBy(stashController.stashDuration)
+        }
+        assertThat(bubbleBarViewController.isExpanded).isTrue()
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testToggleTaskbarStash_pinnedMode_doesNothing() {
+        getInstrumentation().runOnMainSync { stashController.toggleTaskbarStash() }
+        assertThat(stashController.isStashed).isFalse()
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testToggleTaskbarStash_transientMode_unstashesTaskbar() {
+        getInstrumentation().runOnMainSync { stashController.toggleTaskbarStash() }
+        assertThat(stashController.isStashed).isFalse()
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testToggleTaskbarStash_twiceInTransientMode_stashesTaskbar() {
+        getInstrumentation().runOnMainSync {
+            stashController.toggleTaskbarStash()
+            stashController.toggleTaskbarStash()
+        }
+        assertThat(stashController.isStashed).isTrue()
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testToggleTaskbarStash_notInAppWithTransientMode_doesNothing() {
+        getInstrumentation().runOnMainSync {
+            stashController.updateStateForFlag(FLAG_IN_APP, false)
+            stashController.applyState(0)
+            stashController.toggleTaskbarStash()
+        }
+        assertThat(stashController.isStashed).isFalse()
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testAnimateTransientTaskbar_bubblesShownInOverview_stashesTaskbar() {
+        // Start in Overview. Should unstash Taskbar.
+        getInstrumentation().runOnMainSync {
+            stashController.updateStateForFlag(FLAG_STASHED_IN_APP_AUTO, false)
+            stashController.updateStateForFlag(FLAG_IN_APP, false)
+            stashController.updateStateForFlag(FLAG_IN_OVERVIEW, true)
+            stashController.applyState(0)
+        }
+        assertThat(stashController.isStashed).isFalse()
+
+        // Expand bubbles. Should stash Taskbar.
+        getInstrumentation().runOnMainSync {
+            stashController.updateStateForSysuiFlags(SYSUI_STATE_BUBBLES_EXPANDED, false)
+            animatorTestRule.advanceTimeBy(TASKBAR_STASH_DURATION)
+        }
+        assertThat(stashController.isStashed).isTrue()
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testAnimatePinnedTaskbar_imeShown_replacesIconsWithHandle() {
+        getInstrumentation().runOnMainSync {
+            stashController.updateStateForSysuiFlags(SYSUI_STATE_IME_SHOWING, false)
+            animatorTestRule.advanceTimeBy(TASKBAR_STASH_DURATION_FOR_IME)
+        }
+        assertThat(viewController.areIconsVisible()).isFalse()
+        assertThat(stashedHandleViewController.isStashedHandleVisible).isTrue()
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testAnimatePinnedTaskbar_imeHidden_replacesHandleWithIcons() {
+        getInstrumentation().runOnMainSync {
+            stashController.updateStateForSysuiFlags(SYSUI_STATE_IME_SHOWING, true)
+            animatorTestRule.advanceTimeBy(0)
+        }
+
+        getInstrumentation().runOnMainSync {
+            stashController.updateStateForSysuiFlags(0, true)
+            animatorTestRule.advanceTimeBy(0)
+        }
+        assertThat(stashedHandleViewController.isStashedHandleVisible).isFalse()
+        assertThat(viewController.areIconsVisible()).isTrue()
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testAnimatePinnedTaskbar_imeHidden_verifyAnimationDuration() {
+        // Start with IME shown.
+        getInstrumentation().runOnMainSync {
+            stashController.updateStateForSysuiFlags(SYSUI_STATE_IME_SHOWING, true)
+            animatorTestRule.advanceTimeBy(0)
+        }
+
+        // Hide IME with animation.
+        getInstrumentation().runOnMainSync {
+            stashController.updateStateForSysuiFlags(0, false)
+            // Fast forward without start delay.
+            animatorTestRule.advanceTimeBy(TASKBAR_STASH_DURATION_FOR_IME)
+        }
+        // Icons should not be visible yet due to start delay.
+        assertThat(viewController.areIconsVisible()).isFalse()
+
+        // Advance by start delay retroactively. Animation should complete.
+        getInstrumentation().runOnMainSync {
+            animatorTestRule.advanceTimeBy(stashController.taskbarStashStartDelayForIme)
+        }
+        assertThat(viewController.areIconsVisible()).isTrue()
+    }
+
+    @Test
+    @TaskbarMode(THREE_BUTTONS)
+    fun testAnimateThreeButtonsTaskbar_imeShown_hidesIconsAndBg() {
+        getInstrumentation().runOnMainSync {
+            stashController.updateStateForSysuiFlags(SYSUI_STATE_IME_SHOWING, false)
+            animatorTestRule.advanceTimeBy(TASKBAR_STASH_DURATION_FOR_IME)
+        }
+        assertThat(viewController.areIconsVisible()).isFalse()
+        assertThat(dragLayerController.imeBgTaskbar.value).isEqualTo(0)
+    }
+
+    @Test
+    @TaskbarMode(THREE_BUTTONS)
+    fun testAnimateThreeButtonsTaskbar_imeHidden_showsIconsAndBg() {
+        getInstrumentation().runOnMainSync {
+            stashController.updateStateForSysuiFlags(SYSUI_STATE_IME_SHOWING, false)
+            animatorTestRule.advanceTimeBy(TASKBAR_STASH_DURATION_FOR_IME)
+        }
+
+        getInstrumentation().runOnMainSync {
+            stashController.updateStateForSysuiFlags(0, false)
+            animatorTestRule.advanceTimeBy(
+                TASKBAR_STASH_DURATION_FOR_IME + stashController.taskbarStashStartDelayForIme
+            )
+        }
+        assertThat(viewController.areIconsVisible()).isTrue()
+        assertThat(dragLayerController.imeBgTaskbar.value).isEqualTo(1)
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testSetSystemGestureInProgress_whileImeShown_unstashesTaskbar() {
+        getInstrumentation().runOnMainSync {
+            stashController.updateStateForSysuiFlags(SYSUI_STATE_IME_SHOWING, true)
+            animatorTestRule.advanceTimeBy(0)
+        }
+
+        getInstrumentation().runOnMainSync {
+            stashController.setSystemGestureInProgress(true)
+            animatorTestRule.advanceTimeBy(
+                TASKBAR_STASH_DURATION_FOR_IME + stashController.taskbarStashStartDelayForIme
+            )
+        }
+        assertThat(stashController.isStashed).isFalse()
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testUnlockTransition_pinnedMode_fadesOutHandle() {
+        getInstrumentation().runOnMainSync {
+            stashController.updateStateForFlag(FLAG_STASHED_DEVICE_LOCKED, true)
+            stashController.applyState(0)
+        }
+        assertThat(stashedHandleViewController.isStashedHandleVisible).isTrue()
+
+        getInstrumentation().runOnMainSync {
+            stashController.updateStateForFlag(FLAG_STASHED_DEVICE_LOCKED, false)
+            stashController.applyState()
+            animatorTestRule.advanceTimeBy(stashController.stashDuration)
+        }
+        assertThat(stashedHandleViewController.isStashedHandleVisible).isFalse()
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testUnlockTransition_transientMode_fadesOutHandleEarly() {
+        getInstrumentation().runOnMainSync {
+            stashController.updateStateForFlag(FLAG_IN_APP, false)
+            stashController.updateStateForFlag(FLAG_STASHED_DEVICE_LOCKED, true)
+            stashController.applyState(0)
+        }
+        assertThat(stashedHandleViewController.isStashedHandleVisible).isTrue()
+
+        getInstrumentation().runOnMainSync {
+            stashController.updateStateForFlag(FLAG_STASHED_DEVICE_LOCKED, false)
+            stashController.applyState()
+            // Time it takes for just the handle to hide (full stash animation is longer).
+            animatorTestRule.advanceTimeBy(TRANSIENT_TASKBAR_STASH_ALPHA_DURATION)
+        }
+        assertThat(stashedHandleViewController.isStashedHandleVisible).isFalse()
+    }
+}
+
+private fun TaskbarStashController.updateStateForFlag(flag: Int, value: Boolean) {
+    updateStateForFlag(flag.toLong(), value)
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewControllerTest.kt
index 4aac1dc..b13eafe 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewControllerTest.kt
@@ -17,7 +17,6 @@
 package com.android.launcher3.taskbar
 
 import android.view.View
-import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
 import com.android.launcher3.taskbar.TaskbarViewController.DIVIDER_VIEW_POSITION_OFFSET
 import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule
 import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController
@@ -33,19 +32,23 @@
 @EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"])
 /**
  * Legend for the comments below:
+ * ```
  * A: All Apps Button
  * H: Hotseat item
  * |: Divider
  * R: Recent item
+ * ```
  *
  * The comments are formatted in two lines:
+ * ```
  * // Items in taskbar, e.g.               A  |  HHHHHH
  * // Index of items relative to Hotseat: -1 -.5 012345
+ * ```
  */
 class TaskbarViewControllerTest {
 
-    private val context = TaskbarWindowSandboxContext.create(getInstrumentation().targetContext)
-    @get:Rule val taskbarUnitTestRule = TaskbarUnitTestRule(this, context)
+    @get:Rule(order = 0) val context = TaskbarWindowSandboxContext.create()
+    @get:Rule(order = 1) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context)
 
     @InjectController lateinit var taskbarViewController: TaskbarViewController
 
@@ -59,7 +62,7 @@
                 /* isAllAppsButton = */ true,
                 /* isTaskbarDividerView = */ false,
                 /* isDividerForRecents = */ false,
-                /* recentTaskIndex = */ -1
+                /* recentTaskIndex = */ -1,
             )
         // [>A<] | [HHHHHH]
         //  -1 -.5  012345
@@ -77,7 +80,7 @@
                 /* isAllAppsButton = */ true,
                 /* isTaskbarDividerView = */ false,
                 /* isDividerForRecents = */ false,
-                /* recentTaskIndex = */ -1
+                /* recentTaskIndex = */ -1,
             )
         // [HHHHHH] | [>A<]
         //  012345 5.5  6
@@ -94,7 +97,7 @@
                 /* isAllAppsButton = */ false,
                 /* isTaskbarDividerView = */ true,
                 /* isDividerForRecents = */ false,
-                /* recentTaskIndex = */ -1
+                /* recentTaskIndex = */ -1,
             )
         // [A] >|< [HHHHHH]
         // -1  -.5  012345
@@ -112,7 +115,7 @@
                 /* isAllAppsButton = */ false,
                 /* isTaskbarDividerView = */ true,
                 /* isDividerForRecents = */ true,
-                /* recentTaskIndex = */ -1
+                /* recentTaskIndex = */ -1,
             )
         // [A] [HHHHHH] >|< [RR]
         // -1   012345  5.5  67
@@ -130,7 +133,7 @@
                 /* isAllAppsButton = */ false,
                 /* isTaskbarDividerView = */ true,
                 /* isDividerForRecents = */ false,
-                /* recentTaskIndex = */ -1
+                /* recentTaskIndex = */ -1,
             )
         // [HHHHHH] >|< [A]
         //  012345  5.5  6
@@ -148,7 +151,7 @@
                 /* isAllAppsButton = */ false,
                 /* isTaskbarDividerView = */ true,
                 /* isDividerForRecents = */ true,
-                /* recentTaskIndex = */ -1
+                /* recentTaskIndex = */ -1,
             )
         // [HHHHHH][A] >|< [RR]
         //  012345  6  6.5  78
@@ -167,7 +170,7 @@
                 /* isAllAppsButton = */ false,
                 /* isTaskbarDividerView = */ false,
                 /* isDividerForRecents = */ false,
-                /* recentTaskIndex = */ recentTaskIndex
+                /* recentTaskIndex = */ recentTaskIndex,
             )
         // [A][HHHHHH] | [>R<R]
         // -1  012345 5.5  6 7
@@ -186,7 +189,7 @@
                 /* isAllAppsButton = */ false,
                 /* isTaskbarDividerView = */ false,
                 /* isDividerForRecents = */ false,
-                /* recentTaskIndex = */ recentTaskIndex
+                /* recentTaskIndex = */ recentTaskIndex,
             )
         // [A][HHHHHH] | [R>R<]
         // -1  012345 5.5 6 7
@@ -205,7 +208,7 @@
                 /* isAllAppsButton = */ false,
                 /* isTaskbarDividerView = */ false,
                 /* isDividerForRecents = */ false,
-                /* recentTaskIndex = */ recentTaskIndex
+                /* recentTaskIndex = */ recentTaskIndex,
             )
         // [HHHHHH][A] | [>R<R]
         //  012345  6 6.5  7 8
@@ -224,7 +227,7 @@
                 /* isAllAppsButton = */ false,
                 /* isTaskbarDividerView = */ false,
                 /* isDividerForRecents = */ false,
-                /* recentTaskIndex = */ recentTaskIndex
+                /* recentTaskIndex = */ recentTaskIndex,
             )
         // [HHHHHH][A] | [R>R<]
         //  012345  6 6.5 7 8
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 43d924a..60c94a8 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
@@ -44,13 +44,9 @@
 @EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"])
 class TaskbarAllAppsControllerTest {
 
-    @get:Rule
-    val taskbarUnitTestRule =
-        TaskbarUnitTestRule(
-            this,
-            TaskbarWindowSandboxContext.create(getInstrumentation().targetContext),
-        )
-    @get:Rule val animatorTestRule = AnimatorTestRule(this)
+    @get:Rule(order = 0) val context = TaskbarWindowSandboxContext.create()
+    @get:Rule(order = 1) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context)
+    @get:Rule(order = 2) val animatorTestRule = AnimatorTestRule(this)
 
     @InjectController lateinit var allAppsController: TaskbarAllAppsController
     @InjectController lateinit var overlayController: TaskbarOverlayController
@@ -199,8 +195,8 @@
         assertThat(editText?.hasFocus()).isTrue()
     }
 
-    private companion object {
-        private val TEST_APPS =
+    companion object {
+        val TEST_APPS =
             Array(16) {
                 AppInfo(
                     ComponentName(
@@ -213,6 +209,6 @@
                 )
             }
 
-        private val TEST_PREDICTED_APPS = TEST_APPS.take(4).map { WorkspaceItemInfo(it) }
+        val TEST_PREDICTED_APPS = TEST_APPS.take(4).map { WorkspaceItemInfo(it) }
     }
 }
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsViewControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsViewControllerTest.kt
new file mode 100644
index 0000000..3c0d9c6
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsViewControllerTest.kt
@@ -0,0 +1,153 @@
+/*
+ * 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.allapps
+
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.android.launcher3.R
+import com.android.launcher3.appprediction.AppsDividerView
+import com.android.launcher3.appprediction.AppsDividerView.DividerType
+import com.android.launcher3.appprediction.PredictionRowView
+import com.android.launcher3.taskbar.TaskbarControllerTestUtil.asProperty
+import com.android.launcher3.taskbar.TaskbarStashController
+import com.android.launcher3.taskbar.TaskbarStashController.FLAG_STASHED_IN_APP_AUTO
+import com.android.launcher3.taskbar.allapps.TaskbarAllAppsControllerTest.Companion.TEST_PREDICTED_APPS
+import com.android.launcher3.taskbar.overlay.TaskbarOverlayController
+import com.android.launcher3.taskbar.rules.TaskbarModeRule
+import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.PINNED
+import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.TRANSIENT
+import com.android.launcher3.taskbar.rules.TaskbarModeRule.TaskbarMode
+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.OnboardingPrefs.ALL_APPS_VISITED_COUNT
+import com.android.launcher3.util.TestUtil
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(LauncherMultivalentJUnit::class)
+@EmulatedDevices(["pixelFoldable2023"])
+class TaskbarAllAppsViewControllerTest {
+
+    @get:Rule(order = 0) val context = TaskbarWindowSandboxContext.create()
+    @get:Rule(order = 1) val taskbarModeRule = TaskbarModeRule(context)
+    @get:Rule(order = 2) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context)
+
+    @InjectController lateinit var overlayController: TaskbarOverlayController
+    @InjectController lateinit var stashController: TaskbarStashController
+
+    private var allAppsVisitedCount by ALL_APPS_VISITED_COUNT.prefItem.asProperty(context)
+    private val searchSessionController =
+        TestUtil.getOnUiThread { TaskbarSearchSessionController.newInstance(context) }
+
+    @After
+    fun cleanUpSearchSessionController() {
+        getInstrumentation().runOnMainSync { searchSessionController.onDestroy() }
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testShow_transientMode_stashesTaskbar() {
+        getInstrumentation().runOnMainSync {
+            stashController.updateStateForFlag(FLAG_STASHED_IN_APP_AUTO.toLong(), false)
+            stashController.applyState(0)
+        }
+
+        val viewController = createViewController()
+        getInstrumentation().runOnMainSync { viewController.show(false) }
+        assertThat(stashController.isStashed).isTrue()
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testShow_pinnedMode_taskbarDoesNotStash() {
+        val viewController = createViewController()
+        getInstrumentation().runOnMainSync { viewController.show(false) }
+        assertThat(stashController.isStashed).isFalse()
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testHide_transientMode_unstashesTaskbar() {
+        getInstrumentation().runOnMainSync {
+            stashController.updateStateForFlag(FLAG_STASHED_IN_APP_AUTO.toLong(), false)
+            stashController.applyState(0)
+        }
+
+        val viewController = createViewController()
+        getInstrumentation().runOnMainSync { viewController.show(false) }
+        getInstrumentation().runOnMainSync { viewController.close(false) }
+        assertThat(stashController.isStashed).isFalse()
+    }
+
+    @Test
+    fun testShow_firstAllAppsVisit_hasAllAppsTextDivider() {
+        allAppsVisitedCount = 0
+        val viewController = createViewController()
+        getInstrumentation().runOnMainSync { viewController.show(false) }
+
+        val appsView = overlayController.requestWindow().appsView
+        getInstrumentation().runOnMainSync {
+            appsView.floatingHeaderView
+                .findFixedRowByType(PredictionRowView::class.java)
+                .setPredictedApps(TEST_PREDICTED_APPS)
+        }
+
+        val dividerView =
+            appsView.floatingHeaderView.findFixedRowByType(AppsDividerView::class.java)
+        assertThat(dividerView.dividerType).isEqualTo(DividerType.ALL_APPS_LABEL)
+    }
+
+    @Test
+    fun testShow_maxAllAppsVisitedCount_hasLineDivider() {
+        allAppsVisitedCount = ALL_APPS_VISITED_COUNT.maxCount
+        val viewController = createViewController()
+        getInstrumentation().runOnMainSync { viewController.show(false) }
+
+        val appsView = overlayController.requestWindow().appsView
+        getInstrumentation().runOnMainSync {
+            appsView.floatingHeaderView
+                .findFixedRowByType(PredictionRowView::class.java)
+                .setPredictedApps(TEST_PREDICTED_APPS)
+        }
+
+        val dividerView =
+            appsView.floatingHeaderView.findFixedRowByType(AppsDividerView::class.java)
+        assertThat(dividerView.dividerType).isEqualTo(DividerType.LINE)
+    }
+
+    private fun createViewController(): TaskbarAllAppsViewController {
+        return TestUtil.getOnUiThread {
+            val overlayContext = overlayController.requestWindow()
+            TaskbarAllAppsViewController(
+                overlayContext,
+                overlayContext.layoutInflater.inflate(
+                    R.layout.taskbar_all_apps_sheet,
+                    overlayContext.dragLayer,
+                    false,
+                ) as TaskbarAllAppsSlideInView,
+                taskbarUnitTestRule.activityContext.controllers,
+                searchSessionController,
+                /* showKeyboard= */ false, // Covered in TaskbarAllAppsControllerTest.
+            )
+        }
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleBarInputConsumerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleBarInputConsumerTest.kt
index 785ec66..c8f50f7 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleBarInputConsumerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleBarInputConsumerTest.kt
@@ -49,6 +49,7 @@
     @Mock private lateinit var bubbleDismissController: BubbleDismissController
     @Mock private lateinit var bubbleBarPinController: BubbleBarPinController
     @Mock private lateinit var bubblePinController: BubblePinController
+    @Mock private lateinit var bubbleBarSwipeController: BubbleBarSwipeController
     @Mock private lateinit var bubbleCreator: BubbleCreator
 
     @Mock private lateinit var motionEvent: MotionEvent
@@ -67,7 +68,8 @@
                 bubbleDismissController,
                 bubbleBarPinController,
                 bubblePinController,
-                bubbleCreator
+                Optional.of(bubbleBarSwipeController),
+                bubbleCreator,
             )
     }
 
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleBarSwipeControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleBarSwipeControllerTest.kt
new file mode 100644
index 0000000..2e471b8
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleBarSwipeControllerTest.kt
@@ -0,0 +1,383 @@
+/*
+ * 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.bubbles
+
+import android.animation.AnimatorTestRule
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController
+import com.android.launcher3.touch.OverScroll
+import com.google.common.truth.Truth.assertThat
+import java.util.Optional
+import kotlin.math.abs
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.kotlin.any
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.atLeastOnce
+import org.mockito.kotlin.never
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@RunWith(AndroidJUnit4::class)
+class BubbleBarSwipeControllerTest {
+
+    companion object {
+        const val UNSTASH_THRESHOLD = 100
+        const val MAX_OVERSCROLL = 300
+
+        const val UP_BELOW_UNSTASH = -UNSTASH_THRESHOLD + 10f
+        const val UP_ABOVE_UNSTASH = -UNSTASH_THRESHOLD - 10f
+        const val DOWN = UNSTASH_THRESHOLD + 10f
+    }
+
+    private val context = ApplicationProvider.getApplicationContext<Context>()
+
+    @get:Rule(order = 0) val mockitoRule: MockitoRule = MockitoJUnit.rule()
+    @get:Rule(order = 1) val animatorTestRule: AnimatorTestRule = AnimatorTestRule(this)
+
+    private lateinit var bubbleBarSwipeController: BubbleBarSwipeController
+
+    @Mock private lateinit var bubbleBarController: BubbleBarController
+    @Mock private lateinit var bubbleBarViewController: BubbleBarViewController
+    @Mock private lateinit var bubbleStashController: BubbleStashController
+    @Mock private lateinit var bubbleStashedHandleViewController: BubbleStashedHandleViewController
+    @Mock private lateinit var bubbleDragController: BubbleDragController
+    @Mock private lateinit var bubbleDismissController: BubbleDismissController
+    @Mock private lateinit var bubbleBarPinController: BubbleBarPinController
+    @Mock private lateinit var bubblePinController: BubblePinController
+    @Mock private lateinit var bubbleCreator: BubbleCreator
+
+    @Before
+    fun setUp() {
+        val dimensionProvider =
+            object : BubbleBarSwipeController.DimensionProvider {
+                override val unstashThreshold: Int
+                    get() = UNSTASH_THRESHOLD
+
+                override val maxOverscroll: Int
+                    get() = MAX_OVERSCROLL
+            }
+        bubbleBarSwipeController = BubbleBarSwipeController(context, dimensionProvider)
+
+        val bubbleControllers =
+            BubbleControllers(
+                bubbleBarController,
+                bubbleBarViewController,
+                bubbleStashController,
+                Optional.of(bubbleStashedHandleViewController),
+                bubbleDragController,
+                bubbleDismissController,
+                bubbleBarPinController,
+                bubblePinController,
+                Optional.of(bubbleBarSwipeController),
+                bubbleCreator,
+            )
+
+        bubbleBarSwipeController.init(bubbleControllers)
+    }
+
+    // region Test that views have damped translation on swipe
+
+    private fun testViewsHaveDampedTranslationOnSwipe(swipe: Float) {
+        val isUp = swipe < 0
+        val damped = OverScroll.dampedScroll(abs(swipe), MAX_OVERSCROLL).toFloat()
+        val dampedTranslation = if (isUp) -damped else damped
+        getInstrumentation().runOnMainSync {
+            bubbleBarSwipeController.start()
+            bubbleBarSwipeController.swipeTo(swipe)
+        }
+        verify(bubbleStashedHandleViewController).setTranslationYForSwipe(dampedTranslation)
+        verify(bubbleBarViewController).setTranslationYForSwipe(dampedTranslation)
+    }
+
+    @Test
+    fun swipeUp_stashedBar_belowUnstashThreshold_viewsHaveDampedTranslation() {
+        setUpStashedBar()
+        testViewsHaveDampedTranslationOnSwipe(UP_BELOW_UNSTASH)
+    }
+
+    @Test
+    fun swipeUp_stashedBar_aboveUnstashThreshold_viewsHaveDampedTranslation() {
+        setUpStashedBar()
+        testViewsHaveDampedTranslationOnSwipe(UP_ABOVE_UNSTASH)
+    }
+
+    @Test
+    fun swipeUp_collapsedBar_aboveUnstashThreshold_viewsHaveDampedTranslation() {
+        setUpCollapsedBar()
+        testViewsHaveDampedTranslationOnSwipe(UP_ABOVE_UNSTASH)
+    }
+
+    // endregion
+
+    // region Test that translation on views is reset on finish
+
+    private fun testViewsTranslationResetOnFinish(swipe: Float) {
+        getInstrumentation().runOnMainSync {
+            bubbleBarSwipeController.start()
+            bubbleBarSwipeController.swipeTo(swipe)
+            bubbleBarSwipeController.finish()
+            // We use a spring animation. Advance by 5 seconds to give it time to finish
+            animatorTestRule.advanceTimeBy(5000)
+        }
+        val handleSwipeTranslation = argumentCaptor<Float>()
+        val barSwipeTranslation = argumentCaptor<Float>()
+        verify(bubbleStashedHandleViewController, atLeastOnce())
+            .setTranslationYForSwipe(handleSwipeTranslation.capture())
+        verify(bubbleBarViewController, atLeastOnce())
+            .setTranslationYForSwipe(barSwipeTranslation.capture())
+
+        assertThat(handleSwipeTranslation.firstValue).isNonZero()
+        assertThat(handleSwipeTranslation.lastValue).isZero()
+
+        assertThat(barSwipeTranslation.firstValue).isNonZero()
+        assertThat(barSwipeTranslation.lastValue).isZero()
+    }
+
+    @Test
+    fun swipeUp_stashedBar_belowUnstashThreshold_animateTranslationToZeroOnFinish() {
+        setUpStashedBar()
+        testViewsTranslationResetOnFinish(UP_BELOW_UNSTASH)
+    }
+
+    @Test
+    fun swipeUp_stashedBar_aboveUnstashThreshold_animateTranslationToZeroOnFinish() {
+        setUpStashedBar()
+        testViewsTranslationResetOnFinish(UP_ABOVE_UNSTASH)
+    }
+
+    @Test
+    fun swipeUp_collapsedBar_aboveUnstashThreshold_animateTranslationToZeroOnFinish() {
+        setUpCollapsedBar()
+        testViewsTranslationResetOnFinish(UP_ABOVE_UNSTASH)
+    }
+
+    // endregion
+
+    // region Test swipe interactions on stashed bar
+
+    @Test
+    fun swipeUp_stashedBar_belowUnstashThreshold_doesNotShowBar() {
+        setUpStashedBar()
+        getInstrumentation().runOnMainSync {
+            bubbleBarSwipeController.start()
+            bubbleBarSwipeController.swipeTo(UP_BELOW_UNSTASH)
+        }
+        verify(bubbleStashController, never()).showBubbleBar(any())
+    }
+
+    @Test
+    fun swipeUp_stashedBar_belowUnstashThreshold_isSwipeGestureFalse() {
+        setUpStashedBar()
+        getInstrumentation().runOnMainSync {
+            bubbleBarSwipeController.start()
+            bubbleBarSwipeController.swipeTo(UP_BELOW_UNSTASH)
+        }
+        assertThat(bubbleBarSwipeController.isSwipeGesture()).isFalse()
+    }
+
+    @Test
+    fun swipeUp_stashedBar_overUnstashThreshold_unstashBubbleBar() {
+        setUpStashedBar()
+        getInstrumentation().runOnMainSync {
+            bubbleBarSwipeController.start()
+            bubbleBarSwipeController.swipeTo(UP_ABOVE_UNSTASH)
+        }
+        verify(bubbleStashController).showBubbleBar(expandBubbles = false)
+    }
+
+    @Test
+    fun swipeUp_stashedBar_overUnstashThreshold_isSwipeGestureTrue() {
+        setUpStashedBar()
+        getInstrumentation().runOnMainSync {
+            bubbleBarSwipeController.start()
+            bubbleBarSwipeController.swipeTo(UP_ABOVE_UNSTASH)
+        }
+        assertThat(bubbleBarSwipeController.isSwipeGesture()).isTrue()
+    }
+
+    @Test
+    fun swipeUp_stashedBar_overUnstashThresholdMultipleTimes_unstashesMultipleTimes() {
+        setUpStashedBar()
+        getInstrumentation().runOnMainSync {
+            bubbleBarSwipeController.start()
+            bubbleBarSwipeController.swipeTo(UP_ABOVE_UNSTASH)
+            bubbleBarSwipeController.swipeTo(UP_BELOW_UNSTASH)
+        }
+        verify(bubbleStashController).showBubbleBar(expandBubbles = false)
+        verify(bubbleStashController).stashBubbleBar()
+
+        getInstrumentation().runOnMainSync { bubbleBarSwipeController.swipeTo(UP_ABOVE_UNSTASH) }
+        verify(bubbleStashController, times(2)).showBubbleBar(expandBubbles = false)
+    }
+
+    @Test
+    fun swipeUp_stashedBar_releaseOverUnstashThreshold_expandsBar() {
+        setUpStashedBar()
+        getInstrumentation().runOnMainSync {
+            bubbleBarSwipeController.start()
+            bubbleBarSwipeController.swipeTo(UP_ABOVE_UNSTASH)
+        }
+        verify(bubbleStashController, never()).showBubbleBar(expandBubbles = true)
+        getInstrumentation().runOnMainSync { bubbleBarSwipeController.finish() }
+        verify(bubbleStashController).showBubbleBar(expandBubbles = true)
+    }
+
+    @Test
+    fun swipeUp_stashedBar_overUnstashReleaseBelowUnstash_doesNotExpandBar() {
+        setUpStashedBar()
+        getInstrumentation().runOnMainSync {
+            bubbleBarSwipeController.start()
+            bubbleBarSwipeController.swipeTo(UP_ABOVE_UNSTASH)
+        }
+        verify(bubbleStashController).showBubbleBar(expandBubbles = false)
+        getInstrumentation().runOnMainSync {
+            bubbleBarSwipeController.swipeTo(UP_BELOW_UNSTASH)
+            bubbleBarSwipeController.finish()
+        }
+        verify(bubbleStashController, never()).showBubbleBar(expandBubbles = true)
+    }
+
+    @Test
+    fun swipeDown_stashedBar_swipeIgnored() {
+        setUpStashedBar()
+        getInstrumentation().runOnMainSync {
+            bubbleBarSwipeController.start()
+            bubbleBarSwipeController.swipeTo(DOWN)
+        }
+        verify(bubbleStashedHandleViewController, never()).setTranslationYForSwipe(any())
+        verify(bubbleBarViewController, never()).setTranslationYForSwipe(any())
+        verify(bubbleStashController, never()).showBubbleBar(any())
+    }
+
+    // endregion
+
+    // region Test swipe interactions on expanded bar
+
+    @Test
+    fun swipe_expandedBar_swipeIgnored() {
+        setUpExpandedBar()
+        getInstrumentation().runOnMainSync {
+            bubbleBarSwipeController.start()
+            bubbleBarSwipeController.swipeTo(UP_ABOVE_UNSTASH)
+            bubbleBarSwipeController.swipeTo(DOWN)
+            bubbleBarSwipeController.finish()
+        }
+        verify(bubbleStashedHandleViewController, never()).setTranslationYForSwipe(any())
+        verify(bubbleBarViewController, never()).setTranslationYForSwipe(any())
+        verify(bubbleStashController, never()).showBubbleBar(any())
+    }
+
+    // endregion
+
+    // region Test swipe interactions on collapsed bar
+
+    @Test
+    fun swipeUp_collapsedBar_doesNotShowBarDuringDrag() {
+        setUpCollapsedBar()
+        getInstrumentation().runOnMainSync {
+            bubbleBarSwipeController.start()
+            bubbleBarSwipeController.swipeTo(UP_BELOW_UNSTASH)
+            bubbleBarSwipeController.swipeTo(UP_ABOVE_UNSTASH)
+        }
+        verify(bubbleStashController, never()).showBubbleBar(any())
+    }
+
+    @Test
+    fun swipeUp_collapsedBar_belowUnstashThreshold_isSwipeGestureFalse() {
+        setUpCollapsedBar()
+        getInstrumentation().runOnMainSync {
+            bubbleBarSwipeController.start()
+            bubbleBarSwipeController.swipeTo(UP_BELOW_UNSTASH)
+        }
+        assertThat(bubbleBarSwipeController.isSwipeGesture()).isFalse()
+    }
+
+    @Test
+    fun swipeUp_collapsedBar_overUnstashThreshold_isSwipeGestureTrue() {
+        setUpCollapsedBar()
+        getInstrumentation().runOnMainSync {
+            bubbleBarSwipeController.start()
+            bubbleBarSwipeController.swipeTo(UP_ABOVE_UNSTASH)
+        }
+        assertThat(bubbleBarSwipeController.isSwipeGesture()).isTrue()
+    }
+
+    @Test
+    fun swipeUp_collapsedBar_finishOverUnstashThreshold_expandsBar() {
+        setUpCollapsedBar()
+        getInstrumentation().runOnMainSync {
+            bubbleBarSwipeController.start()
+            bubbleBarSwipeController.swipeTo(UP_ABOVE_UNSTASH)
+            bubbleBarSwipeController.finish()
+        }
+        verify(bubbleStashController).showBubbleBar(expandBubbles = true)
+    }
+
+    @Test
+    fun swipeUp_collapsedBar_finishBelowUnstashThreshold_doesNotExpandBar() {
+        setUpCollapsedBar()
+        getInstrumentation().runOnMainSync {
+            bubbleBarSwipeController.start()
+            bubbleBarSwipeController.swipeTo(UP_BELOW_UNSTASH)
+            bubbleBarSwipeController.finish()
+        }
+        verify(bubbleStashController, never()).showBubbleBar(any())
+    }
+
+    @Test
+    fun swipeDown_collapsedBar_swipeIgnored() {
+        setUpCollapsedBar()
+        getInstrumentation().runOnMainSync {
+            bubbleBarSwipeController.start()
+            bubbleBarSwipeController.swipeTo(DOWN)
+        }
+        verify(bubbleStashedHandleViewController, never()).setTranslationYForSwipe(any())
+        verify(bubbleBarViewController, never()).setTranslationYForSwipe(any())
+        verify(bubbleStashController, never()).showBubbleBar(any())
+        verify(bubbleStashController, never()).stashBubbleBar()
+    }
+
+    // endregion
+
+    private fun setUpStashedBar() {
+        whenever(bubbleStashController.isStashed).thenReturn(true)
+        whenever(bubbleStashController.isBubbleBarVisible()).thenReturn(false)
+        whenever(bubbleBarViewController.isExpanded).thenReturn(false)
+    }
+
+    private fun setUpCollapsedBar() {
+        whenever(bubbleStashController.isStashed).thenReturn(false)
+        whenever(bubbleStashController.isBubbleBarVisible()).thenReturn(true)
+        whenever(bubbleBarViewController.isExpanded).thenReturn(false)
+    }
+
+    private fun setUpExpandedBar() {
+        whenever(bubbleStashController.isStashed).thenReturn(false)
+        whenever(bubbleStashController.isBubbleBarVisible()).thenReturn(true)
+        whenever(bubbleBarViewController.isExpanded).thenReturn(true)
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleViewTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleViewTest.kt
index 94f9cf5..4ae8877 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleViewTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleViewTest.kt
@@ -65,10 +65,31 @@
             overflowView.setOverflow(BubbleBarOverflow(overflowView), bitmap)
 
             val bubbleInfo =
-                BubbleInfo("key", 0, null, null, 0, context.packageName, null, null, false, true)
+                BubbleInfo(
+                    "key",
+                    0,
+                    null,
+                    null,
+                    0,
+                    context.packageName,
+                    null,
+                    null,
+                    false,
+                    true,
+                    null,
+                )
             bubbleView = inflater.inflate(R.layout.bubblebar_item_view, null, false) as BubbleView
             bubble =
-                BubbleBarBubble(bubbleInfo, bubbleView, bitmap, bitmap, Color.WHITE, Path(), "")
+                BubbleBarBubble(
+                    bubbleInfo,
+                    bubbleView,
+                    bitmap,
+                    bitmap,
+                    Color.WHITE,
+                    Path(),
+                    "",
+                    null,
+                )
             bubbleView.setBubble(bubble)
         }
         InstrumentationRegistry.getInstrumentation().waitForIdleSync()
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt
index 84e872d..582ea54 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt
@@ -19,11 +19,13 @@
 import android.content.Context
 import android.graphics.Color
 import android.graphics.Path
+import android.graphics.PointF
 import android.graphics.drawable.ColorDrawable
 import android.view.LayoutInflater
 import android.view.View
 import android.view.View.VISIBLE
 import android.widget.FrameLayout
+import android.widget.TextView
 import androidx.core.animation.AnimatorTestRule
 import androidx.core.graphics.drawable.toBitmap
 import androidx.dynamicanimation.animation.DynamicAnimation
@@ -36,6 +38,11 @@
 import com.android.launcher3.taskbar.bubbles.BubbleBarOverflow
 import com.android.launcher3.taskbar.bubbles.BubbleBarView
 import com.android.launcher3.taskbar.bubbles.BubbleView
+import com.android.launcher3.taskbar.bubbles.flyout.BubbleBarFlyoutController
+import com.android.launcher3.taskbar.bubbles.flyout.BubbleBarFlyoutMessage
+import com.android.launcher3.taskbar.bubbles.flyout.BubbleBarFlyoutPositioner
+import com.android.launcher3.taskbar.bubbles.flyout.FlyoutCallbacks
+import com.android.launcher3.taskbar.bubbles.flyout.FlyoutScheduler
 import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController
 import com.android.wm.shell.shared.animation.PhysicsAnimator
 import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils
@@ -63,13 +70,19 @@
     private lateinit var bubbleView: BubbleView
     private lateinit var bubble: BubbleBarBubble
     private lateinit var bubbleBarView: BubbleBarView
+    private lateinit var flyoutContainer: FrameLayout
     private lateinit var bubbleStashController: BubbleStashController
+    private lateinit var flyoutController: BubbleBarFlyoutController
     private val onExpandedNoOp = Runnable {}
 
+    private val flyoutView: View?
+        get() = flyoutContainer.findViewById(R.id.bubble_bar_flyout_view)
+
     @Before
     fun setUp() {
         animatorScheduler = TestBubbleBarViewAnimatorScheduler()
         PhysicsAnimatorTestUtils.prepareForTest()
+        setupFlyoutController()
     }
 
     @Test
@@ -85,8 +98,9 @@
             BubbleBarViewAnimator(
                 bubbleBarView,
                 bubbleStashController,
+                flyoutController,
                 onExpandedNoOp,
-                animatorScheduler
+                animatorScheduler,
             )
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
@@ -106,10 +120,14 @@
         assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
         assertThat(animator.isAnimating).isTrue()
 
+        waitForFlyoutToShow()
+
         // execute the hide bubble animation
         assertThat(animatorScheduler.delayedBlock).isNotNull()
         InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
 
+        waitForFlyoutToHide()
+
         // let the animation start and wait for it to complete
         InstrumentationRegistry.getInstrumentation().runOnMainSync {}
         PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
@@ -134,8 +152,9 @@
             BubbleBarViewAnimator(
                 bubbleBarView,
                 bubbleStashController,
+                flyoutController,
                 onExpandedNoOp,
-                animatorScheduler
+                animatorScheduler,
             )
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
@@ -157,10 +176,14 @@
 
         verify(bubbleStashController, atLeastOnce()).updateTaskbarTouchRegion()
 
+        waitForFlyoutToShow()
+
         // verify the hide bubble animation is pending
         assertThat(animatorScheduler.delayedBlock).isNotNull()
 
-        animator.onBubbleBarTouchedWhileAnimating()
+        InstrumentationRegistry.getInstrumentation().runOnMainSync { animator.interruptForTouch() }
+
+        waitForFlyoutToHide()
 
         assertThat(animatorScheduler.delayedBlock).isNull()
         assertThat(bubbleBarView.alpha).isEqualTo(1)
@@ -182,8 +205,9 @@
             BubbleBarViewAnimator(
                 bubbleBarView,
                 bubbleStashController,
+                flyoutController,
                 onExpandedNoOp,
-                animatorScheduler
+                animatorScheduler,
             )
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
@@ -227,8 +251,9 @@
             BubbleBarViewAnimator(
                 bubbleBarView,
                 bubbleStashController,
+                flyoutController,
                 onExpandedNoOp,
-                animatorScheduler
+                animatorScheduler,
             )
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
@@ -239,10 +264,14 @@
         InstrumentationRegistry.getInstrumentation().runOnMainSync {}
         PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
 
+        waitForFlyoutToShow()
+
         // execute the hide bubble animation
         assertThat(animatorScheduler.delayedBlock).isNotNull()
         InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
 
+        waitForFlyoutToHide()
+
         // wait for the hide animation to start
         InstrumentationRegistry.getInstrumentation().runOnMainSync {}
         handleAnimator.assertIsRunning()
@@ -273,8 +302,9 @@
             BubbleBarViewAnimator(
                 bubbleBarView,
                 bubbleStashController,
+                flyoutController,
                 onExpandedNoOp,
-                animatorScheduler
+                animatorScheduler,
             )
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
@@ -310,8 +340,9 @@
             BubbleBarViewAnimator(
                 bubbleBarView,
                 bubbleStashController,
+                flyoutController,
                 onExpanded,
-                animatorScheduler
+                animatorScheduler,
             )
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
@@ -354,8 +385,9 @@
             BubbleBarViewAnimator(
                 bubbleBarView,
                 bubbleStashController,
+                flyoutController,
                 onExpanded,
-                animatorScheduler
+                animatorScheduler,
             )
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
@@ -404,8 +436,9 @@
             BubbleBarViewAnimator(
                 bubbleBarView,
                 bubbleStashController,
+                flyoutController,
                 onExpanded,
-                animatorScheduler
+                animatorScheduler,
             )
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
@@ -418,6 +451,9 @@
         PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
 
         assertThat(animator.isAnimating).isTrue()
+
+        waitForFlyoutToShow()
+
         // verify the hide bubble animation is pending
         assertThat(animatorScheduler.delayedBlock).isNotNull()
 
@@ -428,6 +464,8 @@
         // verify that the hide animation was canceled
         assertThat(animatorScheduler.delayedBlock).isNull()
 
+        waitForFlyoutToHide()
+
         assertThat(handle.alpha).isEqualTo(0)
         assertThat(handle.translationY)
             .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR)
@@ -453,8 +491,9 @@
             BubbleBarViewAnimator(
                 bubbleBarView,
                 bubbleStashController,
+                flyoutController,
                 onExpandedNoOp,
-                animatorScheduler
+                animatorScheduler,
             )
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
@@ -469,9 +508,13 @@
         assertThat(bubbleBarView.alpha).isEqualTo(1)
         assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
 
+        waitForFlyoutToShow()
+
         assertThat(animatorScheduler.delayedBlock).isNotNull()
         InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
 
+        waitForFlyoutToHide()
+
         InstrumentationRegistry.getInstrumentation().runOnMainSync {}
         PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
 
@@ -503,8 +546,9 @@
             BubbleBarViewAnimator(
                 bubbleBarView,
                 bubbleStashController,
+                flyoutController,
                 onExpanded,
-                animatorScheduler
+                animatorScheduler,
             )
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
@@ -537,8 +581,9 @@
             BubbleBarViewAnimator(
                 bubbleBarView,
                 bubbleStashController,
+                flyoutController,
                 onExpandedNoOp,
-                animatorScheduler
+                animatorScheduler,
             )
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
@@ -553,9 +598,13 @@
         assertThat(bubbleBarView.alpha).isEqualTo(1)
         assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT)
 
+        waitForFlyoutToShow()
+
         assertThat(animatorScheduler.delayedBlock).isNotNull()
         InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
 
+        waitForFlyoutToHide()
+
         assertThat(animator.isAnimating).isFalse()
         assertThat(bubbleBarView.alpha).isEqualTo(1)
         assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT)
@@ -576,8 +625,9 @@
             BubbleBarViewAnimator(
                 bubbleBarView,
                 bubbleStashController,
+                flyoutController,
                 onExpanded,
-                animatorScheduler
+                animatorScheduler,
             )
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
@@ -624,8 +674,9 @@
             BubbleBarViewAnimator(
                 bubbleBarView,
                 bubbleStashController,
+                flyoutController,
                 onExpanded,
-                animatorScheduler
+                animatorScheduler,
             )
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
@@ -636,6 +687,8 @@
         InstrumentationRegistry.getInstrumentation().runOnMainSync {}
         PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
 
+        waitForFlyoutToShow()
+
         assertThat(animator.isAnimating).isTrue()
         // verify the hide bubble animation is pending
         assertThat(animatorScheduler.delayedBlock).isNotNull()
@@ -644,6 +697,8 @@
             animator.expandedWhileAnimating()
         }
 
+        waitForFlyoutToHide()
+
         // verify that the hide animation was canceled
         assertThat(animatorScheduler.delayedBlock).isNull()
 
@@ -665,8 +720,9 @@
             BubbleBarViewAnimator(
                 bubbleBarView,
                 bubbleStashController,
+                flyoutController,
                 onExpandedNoOp,
-                animatorScheduler
+                animatorScheduler,
             )
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
@@ -687,9 +743,13 @@
         barAnimator.assertIsRunning()
         PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
 
+        waitForFlyoutToShow()
+
         assertThat(animatorScheduler.delayedBlock).isNotNull()
         InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
 
+        waitForFlyoutToHide()
+
         assertThat(animator.isAnimating).isFalse()
         // the bubble bar translation y should be back to its initial value
         assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT)
@@ -712,8 +772,9 @@
             BubbleBarViewAnimator(
                 bubbleBarView,
                 bubbleStashController,
+                flyoutController,
                 onExpanded,
-                animatorScheduler
+                animatorScheduler,
             )
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
@@ -759,8 +820,9 @@
             BubbleBarViewAnimator(
                 bubbleBarView,
                 bubbleStashController,
+                flyoutController,
                 onExpanded,
-                animatorScheduler
+                animatorScheduler,
             )
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
@@ -773,23 +835,27 @@
 
         // advance the animation handler by the duration of the initial lift
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
-            animatorTestRule.advanceTimeBy(250)
+            animatorTestRule.advanceTimeBy(100)
         }
 
-        // the lift animation is complete; the spring back animation should start now
-        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
-        barAnimator.assertIsRunning()
-        PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(barAnimator) { true }
+        // send the expand signal in the middle of the lift animation
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animator.expandedWhileAnimating()
+        }
+
+        // let the lift animation complete
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animatorTestRule.advanceTimeBy(150)
+        }
 
         // verify there is a pending hide animation
         assertThat(animatorScheduler.delayedBlock).isNotNull()
         assertThat(animator.isAnimating).isTrue()
 
-        InstrumentationRegistry.getInstrumentation().runOnMainSync {
-            animator.expandedWhileAnimating()
-        }
-
-        // let the animation finish
+        // the lift animation is complete; the spring back animation should start now. wait for it
+        // to complete
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        barAnimator.assertIsRunning()
         PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
 
         // verify that the hide animation was canceled
@@ -817,8 +883,9 @@
             BubbleBarViewAnimator(
                 bubbleBarView,
                 bubbleStashController,
+                flyoutController,
                 onExpanded,
-                animatorScheduler
+                animatorScheduler,
             )
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
@@ -843,6 +910,8 @@
         assertThat(animatorScheduler.delayedBlock).isNotNull()
         assertThat(animator.isAnimating).isTrue()
 
+        waitForFlyoutToShow()
+
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
             animator.expandedWhileAnimating()
         }
@@ -850,6 +919,8 @@
         // verify that the hide animation was canceled
         assertThat(animatorScheduler.delayedBlock).isNull()
 
+        waitForFlyoutToHide()
+
         assertThat(animator.isAnimating).isFalse()
         assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT)
         assertThat(bubbleBarView.isExpanded).isTrue()
@@ -857,6 +928,344 @@
         assertThat(notifiedExpanded).isTrue()
     }
 
+    @Test
+    fun interruptAnimation_whileAnimatingIn() {
+        setUpBubbleBar()
+        setUpBubbleStashController()
+
+        val handle = View(context)
+        val handleAnimator = PhysicsAnimator.getInstance(handle)
+        whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
+
+        val animator =
+            BubbleBarViewAnimator(
+                bubbleBarView,
+                bubbleStashController,
+                flyoutController,
+                onExpandedNoOp,
+                animatorScheduler,
+            )
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animator.animateBubbleInForStashed(bubble, isExpanding = false)
+        }
+
+        // let the animation start and wait until the first frame
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(handleAnimator) { true }
+
+        handleAnimator.assertIsRunning()
+        assertThat(animator.isAnimating).isTrue()
+
+        val updatedBubble =
+            bubble.copy(flyoutMessage = bubble.flyoutMessage!!.copy(message = "updated message"))
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            bubbleView.setBubble(updatedBubble)
+            animator.animateBubbleInForStashed(updatedBubble, isExpanding = false)
+        }
+
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        assertThat(handle.alpha).isEqualTo(0)
+        assertThat(handle.translationY)
+            .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR)
+        assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE)
+        assertThat(bubbleBarView.scaleX).isEqualTo(1)
+        assertThat(bubbleBarView.scaleY).isEqualTo(1)
+        assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
+        assertThat(animator.isAnimating).isTrue()
+
+        waitForFlyoutToShow()
+        assertThat(flyoutView!!.findViewById<TextView>(R.id.bubble_flyout_text).text)
+            .isEqualTo("updated message")
+
+        // run the hide animation
+        assertThat(animatorScheduler.delayedBlock).isNotNull()
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
+
+        waitForFlyoutToHide()
+
+        // let the animation start and wait for it to complete
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        assertThat(handle.alpha).isEqualTo(1)
+        assertThat(handle.translationY).isEqualTo(0)
+        assertThat(bubbleBarView.alpha).isEqualTo(0)
+        assertThat(animator.isAnimating).isFalse()
+        verify(bubbleStashController).stashBubbleBarImmediate()
+    }
+
+    @Test
+    fun interruptAnimation_whileIn() {
+        setUpBubbleBar()
+        setUpBubbleStashController()
+
+        val handle = View(context)
+        val handleAnimator = PhysicsAnimator.getInstance(handle)
+        whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
+
+        val animator =
+            BubbleBarViewAnimator(
+                bubbleBarView,
+                bubbleStashController,
+                flyoutController,
+                onExpandedNoOp,
+                animatorScheduler,
+            )
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animator.animateBubbleInForStashed(bubble, isExpanding = false)
+        }
+
+        // let the animation start and wait for it to complete
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        assertThat(handle.alpha).isEqualTo(0)
+        assertThat(handle.translationY)
+            .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR)
+        assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE)
+        assertThat(bubbleBarView.scaleX).isEqualTo(1)
+        assertThat(bubbleBarView.scaleY).isEqualTo(1)
+        assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
+        assertThat(animator.isAnimating).isTrue()
+
+        waitForFlyoutToShow()
+
+        assertThat(flyoutView!!.findViewById<TextView>(R.id.bubble_flyout_text).text)
+            .isEqualTo("message")
+
+        // verify the hide animation is pending
+        assertThat(animatorScheduler.delayedBlock).isNotNull()
+
+        val updatedBubble =
+            bubble.copy(flyoutMessage = bubble.flyoutMessage!!.copy(message = "updated message"))
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            bubbleView.setBubble(updatedBubble)
+            animator.animateBubbleInForStashed(updatedBubble, isExpanding = false)
+        }
+
+        // verify the hide animation was rescheduled
+        assertThat(animatorScheduler.canceledBlock).isNotNull()
+        assertThat(animatorScheduler.delayedBlock).isNotNull()
+
+        waitForFlyoutToFadeOutAndBackIn()
+
+        assertThat(flyoutView!!.findViewById<TextView>(R.id.bubble_flyout_text).text)
+            .isEqualTo("updated message")
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
+
+        waitForFlyoutToHide()
+
+        // let the animation start and wait for it to complete
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        assertThat(handle.alpha).isEqualTo(1)
+        assertThat(handle.translationY).isEqualTo(0)
+        assertThat(bubbleBarView.alpha).isEqualTo(0)
+        assertThat(animator.isAnimating).isFalse()
+        verify(bubbleStashController).stashBubbleBarImmediate()
+    }
+
+    @Test
+    fun interruptAnimation_whileAnimatingOut_whileCollapsingFlyout() {
+        setUpBubbleBar()
+        setUpBubbleStashController()
+
+        val handle = View(context)
+        val handleAnimator = PhysicsAnimator.getInstance(handle)
+        whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
+
+        val animator =
+            BubbleBarViewAnimator(
+                bubbleBarView,
+                bubbleStashController,
+                flyoutController,
+                onExpandedNoOp,
+                animatorScheduler,
+            )
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animator.animateBubbleInForStashed(bubble, isExpanding = false)
+        }
+
+        // let the animation start and wait for it to complete
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        assertThat(handle.alpha).isEqualTo(0)
+        assertThat(handle.translationY)
+            .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR)
+        assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE)
+        assertThat(bubbleBarView.scaleX).isEqualTo(1)
+        assertThat(bubbleBarView.scaleY).isEqualTo(1)
+        assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
+        assertThat(animator.isAnimating).isTrue()
+
+        waitForFlyoutToShow()
+
+        assertThat(flyoutView!!.findViewById<TextView>(R.id.bubble_flyout_text).text)
+            .isEqualTo("message")
+
+        // run the hide animation
+        assertThat(animatorScheduler.delayedBlock).isNotNull()
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
+
+        // interrupt the animation while the flyout is collapsing
+        val updatedBubble =
+            bubble.copy(flyoutMessage = bubble.flyoutMessage!!.copy(message = "updated message"))
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animatorTestRule.advanceTimeBy(100)
+            bubbleView.setBubble(updatedBubble)
+            animator.animateBubbleInForStashed(updatedBubble, isExpanding = false)
+
+            // the flyout should now reverse and expand
+            animatorTestRule.advanceTimeBy(400)
+        }
+
+        assertThat(flyoutView!!.findViewById<TextView>(R.id.bubble_flyout_text).text)
+            .isEqualTo("updated message")
+
+        assertThat(handle.alpha).isEqualTo(0)
+        assertThat(handle.translationY)
+            .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR)
+        assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE)
+        assertThat(bubbleBarView.scaleX).isEqualTo(1)
+        assertThat(bubbleBarView.scaleY).isEqualTo(1)
+        assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
+
+        // verify the hide animation was rescheduled and run it
+        assertThat(animatorScheduler.delayedBlock).isNotNull()
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
+
+        waitForFlyoutToHide()
+
+        // let the animation start and wait for it to complete
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        assertThat(handle.alpha).isEqualTo(1)
+        assertThat(handle.translationY).isEqualTo(0)
+        assertThat(bubbleBarView.alpha).isEqualTo(0)
+        assertThat(animator.isAnimating).isFalse()
+        verify(bubbleStashController).stashBubbleBarImmediate()
+    }
+
+    @Test
+    fun interruptAnimation_whileAnimatingOut_barToHandle() {
+        setUpBubbleBar()
+        setUpBubbleStashController()
+
+        val handle = View(context)
+        val handleAnimator = PhysicsAnimator.getInstance(handle)
+        whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
+
+        val animator =
+            BubbleBarViewAnimator(
+                bubbleBarView,
+                bubbleStashController,
+                flyoutController,
+                onExpandedNoOp,
+                animatorScheduler,
+            )
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animator.animateBubbleInForStashed(bubble, isExpanding = false)
+        }
+
+        // let the animation start and wait for it to complete
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        assertThat(handle.alpha).isEqualTo(0)
+        assertThat(handle.translationY)
+            .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR)
+        assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE)
+        assertThat(bubbleBarView.scaleX).isEqualTo(1)
+        assertThat(bubbleBarView.scaleY).isEqualTo(1)
+        assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
+        assertThat(animator.isAnimating).isTrue()
+
+        waitForFlyoutToShow()
+
+        assertThat(flyoutView!!.findViewById<TextView>(R.id.bubble_flyout_text).text)
+            .isEqualTo("message")
+
+        // run the hide animation
+        assertThat(animatorScheduler.delayedBlock).isNotNull()
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
+
+        waitForFlyoutToHide()
+
+        // interrupt the animation while the bar is animating to the handle
+        PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(handleAnimator) {
+            bubbleBarView.alpha < 0.5
+        }
+
+        // we're about to interrupt the animation which will cancel the current animation and start
+        // a new one. pause the scheduler to delay starting the new animation. this allows us to run
+        // the test deterministically
+        animatorScheduler.pauseScheduler = true
+
+        val updatedBubble =
+            bubble.copy(flyoutMessage = bubble.flyoutMessage!!.copy(message = "updated message"))
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            bubbleView.setBubble(updatedBubble)
+            animator.animateBubbleInForStashed(updatedBubble, isExpanding = false)
+        }
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        // verify there's a new job scheduled and start it. this is starting the animation from the
+        // handle back to the bar
+        assertThat(animatorScheduler.pausedBlock).isNotNull()
+        animatorScheduler.pauseScheduler = false
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.pausedBlock!!)
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        waitForFlyoutToShow()
+
+        assertThat(flyoutView!!.findViewById<TextView>(R.id.bubble_flyout_text).text)
+            .isEqualTo("updated message")
+        assertThat(handle.alpha).isEqualTo(0)
+        assertThat(handle.translationY)
+            .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR)
+        assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE)
+        assertThat(bubbleBarView.scaleX).isEqualTo(1)
+        assertThat(bubbleBarView.scaleY).isEqualTo(1)
+        assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
+
+        // run the hide animation
+        assertThat(animatorScheduler.delayedBlock).isNotNull()
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
+
+        waitForFlyoutToHide()
+
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        // verify the hide animation was rescheduled and run it
+        assertThat(animatorScheduler.delayedBlock).isNotNull()
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
+
+        waitForFlyoutToHide()
+
+        // let the animation start and wait for it to complete
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        assertThat(handle.alpha).isEqualTo(1)
+        assertThat(handle.translationY).isEqualTo(0)
+        assertThat(bubbleBarView.alpha).isEqualTo(0)
+        assertThat(animator.isAnimating).isFalse()
+        verify(bubbleStashController).stashBubbleBarImmediate()
+    }
+
     private fun setUpBubbleBar() {
         bubbleBarView = BubbleBarView(context)
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
@@ -870,11 +1279,32 @@
             bubbleBarView.addView(overflowView)
 
             val bubbleInfo =
-                BubbleInfo("key", 0, null, null, 0, context.packageName, null, null, false, true)
+                BubbleInfo(
+                    "key",
+                    0,
+                    null,
+                    null,
+                    0,
+                    context.packageName,
+                    null,
+                    null,
+                    false,
+                    true,
+                    null,
+                )
             bubbleView =
                 inflater.inflate(R.layout.bubblebar_item_view, bubbleBarView, false) as BubbleView
             bubble =
-                BubbleBarBubble(bubbleInfo, bubbleView, bitmap, bitmap, Color.WHITE, Path(), "")
+                BubbleBarBubble(
+                    bubbleInfo,
+                    bubbleView,
+                    bitmap,
+                    bitmap,
+                    Color.WHITE,
+                    Path(),
+                    "",
+                    BubbleBarFlyoutMessage(icon = null, title = "title", message = "message"),
+                )
             bubbleView.setBubble(bubble)
             bubbleBarView.addView(bubbleView)
         }
@@ -892,6 +1322,36 @@
             .thenReturn(BAR_TRANSLATION_Y_FOR_TASKBAR)
     }
 
+    private fun setupFlyoutController() {
+        flyoutContainer = FrameLayout(context)
+        val flyoutPositioner =
+            object : BubbleBarFlyoutPositioner {
+                override val isOnLeft = true
+                override val targetTy = 100f
+                override val distanceToCollapsedPosition = PointF(0f, 0f)
+                override val collapsedSize = 30f
+                override val collapsedColor = Color.BLUE
+                override val collapsedElevation = 1f
+                override val distanceToRevealTriangle = 10f
+            }
+        val flyoutCallbacks =
+            object : FlyoutCallbacks {
+                override fun extendTopBoundary(space: Int) {}
+
+                override fun resetTopBoundary() {}
+
+                override fun flyoutClicked() {}
+            }
+        val flyoutScheduler = FlyoutScheduler { block -> block.invoke() }
+        flyoutController =
+            BubbleBarFlyoutController(
+                flyoutContainer,
+                flyoutPositioner,
+                flyoutCallbacks,
+                flyoutScheduler,
+            )
+    }
+
     private fun verifyBubbleBarIsExpandedWithTranslation(ty: Float) {
         assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE)
         assertThat(bubbleBarView.scaleX).isEqualTo(1)
@@ -900,6 +1360,27 @@
         assertThat(bubbleBarView.isExpanded).isTrue()
     }
 
+    private fun waitForFlyoutToShow() {
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animatorTestRule.advanceTimeBy(400)
+        }
+        assertThat(flyoutView).isNotNull()
+    }
+
+    private fun waitForFlyoutToHide() {
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animatorTestRule.advanceTimeBy(350)
+        }
+        assertThat(flyoutView).isNull()
+    }
+
+    private fun waitForFlyoutToFadeOutAndBackIn() {
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animatorTestRule.advanceTimeBy(750)
+        }
+        assertThat(flyoutView).isNotNull()
+    }
+
     private fun <T> PhysicsAnimator<T>.assertIsRunning() {
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
             assertThat(isRunning()).isTrue()
@@ -914,20 +1395,30 @@
 
     private class TestBubbleBarViewAnimatorScheduler : BubbleBarViewAnimator.Scheduler {
 
+        var pauseScheduler = false
+        var pausedBlock: Runnable? = null
+            private set
+
         var delayedBlock: Runnable? = null
             private set
 
+        var canceledBlock: Runnable? = null
+            private set
+
         override fun post(block: Runnable) {
+            if (pauseScheduler) {
+                pausedBlock = block
+                return
+            }
             block.run()
         }
 
         override fun postDelayed(delayMillis: Long, block: Runnable) {
-            check(delayedBlock == null) { "there is already a pending block waiting to run" }
             delayedBlock = block
         }
 
         override fun cancel(block: Runnable) {
-            check(delayedBlock == block) { "the pending block does not match the canceled block" }
+            canceledBlock = delayedBlock
             delayedBlock = null
         }
     }
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutControllerTest.kt
index a58ce08..103c769 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/flyout/BubbleBarFlyoutControllerTest.kt
@@ -17,15 +17,21 @@
 package com.android.launcher3.taskbar.bubbles.flyout
 
 import android.content.Context
+import android.graphics.Color
+import android.graphics.PointF
 import android.view.Gravity
+import android.view.View
 import android.widget.FrameLayout
 import android.widget.TextView
+import androidx.core.animation.AnimatorTestRule
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
 import com.android.launcher3.R
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -34,64 +40,270 @@
 @RunWith(AndroidJUnit4::class)
 class BubbleBarFlyoutControllerTest {
 
+    @get:Rule val animatorTestRule = AnimatorTestRule()
+
     private lateinit var flyoutController: BubbleBarFlyoutController
     private lateinit var flyoutContainer: FrameLayout
+    private lateinit var flyoutCallbacks: FakeFlyoutCallbacks
     private val context = ApplicationProvider.getApplicationContext<Context>()
-    private val flyoutMessage =
-        BubbleBarFlyoutMessage(senderAvatar = null, "sender name", "message", isGroupChat = false)
+    private val flyoutMessage = BubbleBarFlyoutMessage(icon = null, "sender name", "message")
     private var onLeft = true
+    private var flyoutTy = 50f
+
+    private val showAnimationDuration = 400L
+    private val hideAnimationDuration = 350L
 
     @Before
     fun setUp() {
         flyoutContainer = FrameLayout(context)
         val positioner =
             object : BubbleBarFlyoutPositioner {
-                override val isOnLeft: Boolean
+                override val isOnLeft
                     get() = onLeft
 
-                override val targetTy: Float
-                    get() = 50f
+                override val targetTy
+                    get() = flyoutTy
+
+                override val distanceToCollapsedPosition = PointF(100f, 200f)
+                override val collapsedSize = 30f
+                override val collapsedColor = Color.BLUE
+                override val collapsedElevation = 1f
+                override val distanceToRevealTriangle = 50f
             }
-        flyoutController = BubbleBarFlyoutController(flyoutContainer, positioner)
+        flyoutCallbacks = FakeFlyoutCallbacks()
+        val flyoutScheduler = FlyoutScheduler { block -> block.invoke() }
+        flyoutController =
+            BubbleBarFlyoutController(flyoutContainer, positioner, flyoutCallbacks, flyoutScheduler)
     }
 
     @Test
     fun flyoutPosition_left() {
-        flyoutController.setUpFlyout(flyoutMessage)
-        assertThat(flyoutContainer.childCount).isEqualTo(1)
-        val flyout = flyoutContainer.getChildAt(0)
-        val lp = flyout.layoutParams as FrameLayout.LayoutParams
-        assertThat(lp.gravity).isEqualTo(Gravity.BOTTOM or Gravity.LEFT)
-        assertThat(flyout.translationY).isEqualTo(50f)
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            setupAndShowFlyout()
+            assertThat(flyoutContainer.childCount).isEqualTo(1)
+            val flyout = flyoutContainer.getChildAt(0)
+            val lp = flyout.layoutParams as FrameLayout.LayoutParams
+            assertThat(lp.gravity).isEqualTo(Gravity.BOTTOM or Gravity.LEFT)
+            assertThat(flyout.translationY).isEqualTo(50f)
+        }
     }
 
     @Test
     fun flyoutPosition_right() {
         onLeft = false
-        flyoutController.setUpFlyout(flyoutMessage)
-        assertThat(flyoutContainer.childCount).isEqualTo(1)
-        val flyout = flyoutContainer.getChildAt(0)
-        val lp = flyout.layoutParams as FrameLayout.LayoutParams
-        assertThat(lp.gravity).isEqualTo(Gravity.BOTTOM or Gravity.RIGHT)
-        assertThat(flyout.translationY).isEqualTo(50f)
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            setupAndShowFlyout()
+            assertThat(flyoutContainer.childCount).isEqualTo(1)
+            val flyout = flyoutContainer.getChildAt(0)
+            val lp = flyout.layoutParams as FrameLayout.LayoutParams
+            assertThat(lp.gravity).isEqualTo(Gravity.BOTTOM or Gravity.RIGHT)
+            assertThat(flyout.translationY).isEqualTo(50f)
+        }
     }
 
     @Test
     fun flyoutMessage() {
-        flyoutController.setUpFlyout(flyoutMessage)
-        assertThat(flyoutContainer.childCount).isEqualTo(1)
-        val flyout = flyoutContainer.getChildAt(0)
-        val sender = flyout.findViewById<TextView>(R.id.bubble_flyout_name)
-        assertThat(sender.text).isEqualTo("sender name")
-        val message = flyout.findViewById<TextView>(R.id.bubble_flyout_text)
-        assertThat(message.text).isEqualTo("message")
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            setupAndShowFlyout()
+            assertThat(flyoutContainer.childCount).isEqualTo(1)
+            val flyout = flyoutContainer.getChildAt(0)
+            val sender = flyout.findViewById<TextView>(R.id.bubble_flyout_title)
+            assertThat(sender.text).isEqualTo("sender name")
+            val message = flyout.findViewById<TextView>(R.id.bubble_flyout_text)
+            assertThat(message.text).isEqualTo("message")
+        }
     }
 
     @Test
     fun hideFlyout_removedFromContainer() {
-        flyoutController.setUpFlyout(flyoutMessage)
-        assertThat(flyoutContainer.childCount).isEqualTo(1)
-        flyoutController.hideFlyout()
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            setupAndShowFlyout()
+            assertThat(flyoutController.hasFlyout()).isTrue()
+            assertThat(flyoutContainer.childCount).isEqualTo(1)
+            flyoutController.collapseFlyout {}
+            animatorTestRule.advanceTimeBy(hideAnimationDuration)
+        }
         assertThat(flyoutContainer.childCount).isEqualTo(0)
+        assertThat(flyoutController.hasFlyout()).isFalse()
+    }
+
+    @Test
+    fun showFlyout_extendsTopBoundary() {
+        // set negative translation for the flyout so that it will request to extend the top
+        // boundary
+        flyoutTy = -50f
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            setupAndShowFlyout()
+            assertThat(flyoutContainer.childCount).isEqualTo(1)
+        }
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animatorTestRule.advanceTimeBy(showAnimationDuration)
+        }
+        assertThat(flyoutCallbacks.topBoundaryExtendedSpace).isEqualTo(50)
+    }
+
+    @Test
+    fun showFlyout_withinBoundary() {
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            setupAndShowFlyout()
+            assertThat(flyoutContainer.childCount).isEqualTo(1)
+        }
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            animatorTestRule.advanceTimeBy(showAnimationDuration)
+        }
+        assertThat(flyoutCallbacks.topBoundaryExtendedSpace).isEqualTo(0)
+    }
+
+    @Test
+    fun collapseFlyout_resetsTopBoundary() {
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            setupAndShowFlyout()
+            assertThat(flyoutContainer.childCount).isEqualTo(1)
+            flyoutController.collapseFlyout {}
+            animatorTestRule.advanceTimeBy(hideAnimationDuration)
+        }
+        assertThat(flyoutCallbacks.topBoundaryReset).isTrue()
+    }
+
+    @Test
+    fun cancelFlyout_fadesOutFlyout() {
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            setupAndShowFlyout()
+            assertThat(flyoutContainer.childCount).isEqualTo(1)
+            val flyoutView = flyoutContainer.findViewById<View>(R.id.bubble_bar_flyout_view)
+            assertThat(flyoutView.alpha).isEqualTo(1f)
+            flyoutController.cancelFlyout {}
+            animatorTestRule.advanceTimeBy(hideAnimationDuration)
+            assertThat(flyoutView.alpha).isEqualTo(0f)
+        }
+        assertThat(flyoutCallbacks.topBoundaryReset).isTrue()
+    }
+
+    @Test
+    fun clickFlyout_notifiesCallback() {
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            setupAndShowFlyout()
+            assertThat(flyoutContainer.childCount).isEqualTo(1)
+            val flyoutView = flyoutContainer.findViewById<View>(R.id.bubble_bar_flyout_view)
+            assertThat(flyoutView.alpha).isEqualTo(1f)
+            animatorTestRule.advanceTimeBy(showAnimationDuration)
+            flyoutView.performClick()
+        }
+        assertThat(flyoutCallbacks.flyoutClicked).isTrue()
+    }
+
+    @Test
+    fun updateFlyoutWhileExpanding() {
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            setupAndShowFlyout()
+            assertThat(flyoutController.hasFlyout()).isTrue()
+            val flyout = flyoutContainer.findViewById<View>(R.id.bubble_bar_flyout_view)
+            assertThat(flyout.findViewById<TextView>(R.id.bubble_flyout_text).text)
+                .isEqualTo("message")
+            // advance the animation about halfway
+            animatorTestRule.advanceTimeBy(100)
+        }
+        assertThat(flyoutController.hasFlyout()).isTrue()
+
+        val newFlyoutMessage = flyoutMessage.copy(message = "new message")
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            val flyout = flyoutContainer.findViewById<View>(R.id.bubble_bar_flyout_view)
+            // set negative translation to verify that the top boundary extends as a result of
+            // updating while expanding
+            flyout.translationY = -50f
+            flyoutController.updateFlyoutWhileExpanding(newFlyoutMessage)
+            assertThat(flyout.findViewById<TextView>(R.id.bubble_flyout_text).text)
+                .isEqualTo("new message")
+        }
+        assertThat(flyoutCallbacks.topBoundaryExtendedSpace).isEqualTo(50)
+    }
+
+    @Test
+    fun updateFlyoutFullyExpanded() {
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            setupAndShowFlyout()
+            animatorTestRule.advanceTimeBy(showAnimationDuration)
+        }
+        assertThat(flyoutController.hasFlyout()).isTrue()
+
+        val newFlyoutMessage = flyoutMessage.copy(message = "new message")
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            val flyout = flyoutContainer.findViewById<View>(R.id.bubble_bar_flyout_view)
+            // set negative translation to verify that the top boundary extends as a result of
+            // updating while fully expanded
+            flyout.translationY = -50f
+            flyoutController.updateFlyoutFullyExpanded(newFlyoutMessage) {}
+
+            // advance the timer so that the fade out animation plays
+            animatorTestRule.advanceTimeBy(hideAnimationDuration)
+            assertThat(flyout.alpha).isEqualTo(0)
+            assertThat(flyout.findViewById<TextView>(R.id.bubble_flyout_text).text)
+                .isEqualTo("new message")
+
+            // advance the timer so that the fade in animation plays
+            animatorTestRule.advanceTimeBy(showAnimationDuration)
+            assertThat(flyout.alpha).isEqualTo(1)
+        }
+        assertThat(flyoutCallbacks.topBoundaryExtendedSpace).isEqualTo(50)
+    }
+
+    @Test
+    fun updateFlyoutWhileCollapsing() {
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            setupAndShowFlyout()
+            animatorTestRule.advanceTimeBy(showAnimationDuration)
+        }
+        assertThat(flyoutController.hasFlyout()).isTrue()
+
+        val newFlyoutMessage = flyoutMessage.copy(message = "new message")
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            var flyoutCollapsed = false
+            flyoutController.collapseFlyout { flyoutCollapsed = true }
+            // advance the fake timer so that the collapse animation runs for 125ms
+            animatorTestRule.advanceTimeBy(125)
+
+            // update the flyout in the middle of collapsing, which should start expanding it.
+            var flyoutReversed = false
+            flyoutController.updateFlyoutWhileCollapsing(newFlyoutMessage) { flyoutReversed = true }
+
+            // the collapse and expand animations use an emphasized interpolator, so the reverse
+            // path does not take the same time. advance the timer the by full duration of the show
+            // animation to ensure it completes
+            animatorTestRule.advanceTimeBy(showAnimationDuration)
+            val flyout = flyoutContainer.findViewById<View>(R.id.bubble_bar_flyout_view)
+            assertThat(flyout.alpha).isEqualTo(1)
+            assertThat(flyout.findViewById<TextView>(R.id.bubble_flyout_text).text)
+                .isEqualTo("new message")
+            // verify that we never called the end action on the collapse animation
+            assertThat(flyoutCollapsed).isFalse()
+            // verify that we called the end action on the reverse animation
+            assertThat(flyoutReversed).isTrue()
+        }
+        assertThat(flyoutController.hasFlyout()).isTrue()
+    }
+
+    private fun setupAndShowFlyout() {
+        flyoutController.setUpAndShowFlyout(flyoutMessage, {}, {})
+    }
+
+    class FakeFlyoutCallbacks : FlyoutCallbacks {
+
+        var topBoundaryExtendedSpace = 0
+        var topBoundaryReset = false
+        var flyoutClicked = false
+
+        override fun extendTopBoundary(space: Int) {
+            topBoundaryExtendedSpace = space
+        }
+
+        override fun resetTopBoundary() {
+            topBoundaryReset = true
+        }
+
+        override fun flyoutClicked() {
+            flyoutClicked = true
+        }
     }
 }
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashControllerTest.kt
index 4106a2c..f795ab1 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashControllerTest.kt
@@ -27,6 +27,7 @@
 import com.android.launcher3.taskbar.TaskbarInsetsController
 import com.android.launcher3.taskbar.bubbles.BubbleBarView
 import com.android.launcher3.taskbar.bubbles.BubbleBarViewController
+import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.BubbleLauncherState
 import com.android.launcher3.util.MultiValueAlpha
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
@@ -37,6 +38,7 @@
 import org.mockito.junit.MockitoJUnit
 import org.mockito.junit.MockitoRule
 import org.mockito.kotlin.clearInvocations
+import org.mockito.kotlin.never
 import org.mockito.kotlin.verify
 import org.mockito.kotlin.whenever
 
@@ -47,6 +49,7 @@
 
     companion object {
         const val BUBBLE_BAR_HEIGHT = 100f
+        const val HOTSEAT_VERTICAL_CENTER = 95
         const val HOTSEAT_TRANSLATION_Y = -45f
         const val TASK_BAR_TRANSLATION_Y = -5f
     }
@@ -73,24 +76,39 @@
             PersistentBubbleStashController(DefaultDimensionsProvider())
         setUpBubbleBarView()
         setUpBubbleBarController()
+        persistentTaskBarStashController.setHotseatVerticalCenter(HOTSEAT_VERTICAL_CENTER)
         persistentTaskBarStashController.init(
             taskbarInsetsController,
             bubbleBarViewController,
             null,
-            ImmediateAction()
+            ImmediateAction(),
         )
     }
 
     @Test
+    fun updateLauncherState_noBubbles_controllerNotified() {
+        // Given bubble bar has  no bubbles
+        whenever(bubbleBarViewController.hasBubbles()).thenReturn(false)
+
+        // When switch to home screen
+        getInstrumentation().runOnMainSync {
+            persistentTaskBarStashController.launcherState = BubbleLauncherState.HOME
+        }
+
+        // Then bubble bar view controller is notified
+        verify(bubbleBarViewController).onBubbleBarConfigurationChanged(/* animate= */ false)
+    }
+
+    @Test
     fun setBubblesShowingOnHomeUpdatedToFalse_barPositionYUpdated_controllersNotified() {
         // Given bubble bar is on home and has bubbles
         whenever(bubbleBarViewController.hasBubbles()).thenReturn(false)
-        persistentTaskBarStashController.isBubblesShowingOnHome = true
+        persistentTaskBarStashController.launcherState = BubbleLauncherState.HOME
         whenever(bubbleBarViewController.hasBubbles()).thenReturn(true)
 
         // When switch out of the home screen
         getInstrumentation().runOnMainSync {
-            persistentTaskBarStashController.isBubblesShowingOnHome = false
+            persistentTaskBarStashController.launcherState = BubbleLauncherState.IN_APP
         }
 
         // Then translation Y is animating and the bubble bar controller is notified
@@ -110,7 +128,7 @@
 
         // When switch to home screen
         getInstrumentation().runOnMainSync {
-            persistentTaskBarStashController.isBubblesShowingOnHome = true
+            persistentTaskBarStashController.launcherState = BubbleLauncherState.HOME
         }
 
         // Then translation Y is animating and the bubble bar controller is notified
@@ -127,11 +145,11 @@
     @Test
     fun setBubblesShowingOnOverviewUpdatedToFalse_controllersNotified() {
         // Given bubble bar is on overview
-        persistentTaskBarStashController.isBubblesShowingOnOverview = true
+        persistentTaskBarStashController.launcherState = BubbleLauncherState.OVERVIEW
         clearInvocations(bubbleBarViewController)
 
         // When switch out of the overview screen
-        persistentTaskBarStashController.isBubblesShowingOnOverview = false
+        persistentTaskBarStashController.launcherState = BubbleLauncherState.IN_APP
 
         // Then bubble bar controller is notified
         verify(bubbleBarViewController).onBubbleBarConfigurationChanged(/* animate= */ true)
@@ -140,7 +158,7 @@
     @Test
     fun setBubblesShowingOnOverviewUpdatedToTrue_controllersNotified() {
         // When switch to the overview screen
-        persistentTaskBarStashController.isBubblesShowingOnOverview = true
+        persistentTaskBarStashController.launcherState = BubbleLauncherState.OVERVIEW
 
         // Then bubble bar controller is notified
         verify(bubbleBarViewController).onBubbleBarConfigurationChanged(/* animate= */ true)
@@ -150,7 +168,7 @@
     fun isSysuiLockedSwitchedToFalseForOverview_unlockAnimationIsShown() {
         // Given screen is locked and bubble bar has bubbles
         persistentTaskBarStashController.isSysuiLocked = true
-        persistentTaskBarStashController.isBubblesShowingOnOverview = true
+        persistentTaskBarStashController.launcherState = BubbleLauncherState.OVERVIEW
         whenever(bubbleBarViewController.hasBubbles()).thenReturn(true)
 
         // When switch to the overview screen
@@ -211,20 +229,119 @@
     fun bubbleBarTranslationYForTaskbar() {
         // Give bubble bar is on home
         whenever(bubbleBarViewController.hasBubbles()).thenReturn(false)
-        persistentTaskBarStashController.isBubblesShowingOnHome = true
+        persistentTaskBarStashController.launcherState = BubbleLauncherState.HOME
 
         // Then bubbleBarTranslationY would be HOTSEAT_TRANSLATION_Y
         assertThat(persistentTaskBarStashController.bubbleBarTranslationY)
             .isEqualTo(HOTSEAT_TRANSLATION_Y)
 
         // Give bubble bar is not on home
-        persistentTaskBarStashController.isBubblesShowingOnHome = false
+        persistentTaskBarStashController.launcherState = BubbleLauncherState.IN_APP
 
         // Then bubbleBarTranslationY would be TASK_BAR_TRANSLATION_Y
         assertThat(persistentTaskBarStashController.bubbleBarTranslationY)
             .isEqualTo(TASK_BAR_TRANSLATION_Y)
     }
 
+    @Test
+    fun inAppDisplayOverrideProgress_onHome_updatesTranslationFromHomeToInApp() {
+        whenever(bubbleBarViewController.hasBubbles()).thenReturn(false)
+        persistentTaskBarStashController.launcherState = BubbleLauncherState.HOME
+
+        assertThat(persistentTaskBarStashController.bubbleBarTranslationY)
+            .isEqualTo(HOTSEAT_TRANSLATION_Y)
+
+        persistentTaskBarStashController.inAppDisplayOverrideProgress = 0.5f
+
+        val middleBetweenHotseatAndTaskbar = (HOTSEAT_TRANSLATION_Y + TASK_BAR_TRANSLATION_Y) / 2f
+        assertThat(persistentTaskBarStashController.bubbleBarTranslationY)
+            .isWithin(0.1f)
+            .of(middleBetweenHotseatAndTaskbar)
+
+        persistentTaskBarStashController.inAppDisplayOverrideProgress = 1f
+
+        assertThat(persistentTaskBarStashController.bubbleBarTranslationY)
+            .isEqualTo(TASK_BAR_TRANSLATION_Y)
+    }
+
+    @Test
+    fun inAppDisplayOverrideProgress_onHome_updatesInsetsWhenProgressReachesOne() {
+        whenever(bubbleBarViewController.hasBubbles()).thenReturn(false)
+        persistentTaskBarStashController.launcherState = BubbleLauncherState.HOME
+        // Reset invocations to track only changes from in-app display override
+        clearInvocations(taskbarInsetsController)
+
+        // Insets are not updated for values between 0 and 1
+        persistentTaskBarStashController.inAppDisplayOverrideProgress = 0.5f
+        verify(taskbarInsetsController, never()).onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
+
+        // Update insets when progress reaches 1
+        persistentTaskBarStashController.inAppDisplayOverrideProgress = 1f
+        verify(taskbarInsetsController).onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
+    }
+
+    @Test
+    fun inAppDisplayOverrideProgress_onHome_updatesInsetsWhenProgressReachesZero() {
+        whenever(bubbleBarViewController.hasBubbles()).thenReturn(false)
+        persistentTaskBarStashController.launcherState = BubbleLauncherState.HOME
+        persistentTaskBarStashController.inAppDisplayOverrideProgress = 1f
+        // Reset invocations to track only changes from in-app display override
+        clearInvocations(taskbarInsetsController)
+
+        // Insets are not updated for values between 0 and 1
+        persistentTaskBarStashController.inAppDisplayOverrideProgress = 0.5f
+        verify(taskbarInsetsController, never()).onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
+
+        // Update insets when progress reaches 0
+        persistentTaskBarStashController.inAppDisplayOverrideProgress = 0f
+        verify(taskbarInsetsController).onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
+    }
+
+    @Test
+    fun inAppDisplayOverrideProgress_onHome_cancelExistingAnimation() {
+        whenever(bubbleBarViewController.hasBubbles()).thenReturn(false)
+        persistentTaskBarStashController.launcherState = BubbleLauncherState.HOME
+
+        bubbleBarViewController.bubbleBarTranslationY.animateToValue(100f)
+        advanceTimeBy(10)
+        assertThat(bubbleBarViewController.bubbleBarTranslationY.isAnimating).isTrue()
+
+        getInstrumentation().runOnMainSync {
+            persistentTaskBarStashController.inAppDisplayOverrideProgress = 0.5f
+        }
+        assertThat(bubbleBarViewController.bubbleBarTranslationY.isAnimating).isFalse()
+    }
+
+    @Test
+    fun inAppDisplayProgressUpdate_inApp_noTranslationUpdate() {
+        whenever(bubbleBarViewController.hasBubbles()).thenReturn(false)
+        persistentTaskBarStashController.launcherState = BubbleLauncherState.IN_APP
+
+        assertThat(persistentTaskBarStashController.bubbleBarTranslationY)
+            .isEqualTo(TASK_BAR_TRANSLATION_Y)
+
+        persistentTaskBarStashController.inAppDisplayOverrideProgress = 0.5f
+
+        assertThat(persistentTaskBarStashController.bubbleBarTranslationY)
+            .isEqualTo(TASK_BAR_TRANSLATION_Y)
+    }
+
+    @Test
+    fun inAppDisplayOverrideProgress_inApp_noInsetsUpdate() {
+        whenever(bubbleBarViewController.hasBubbles()).thenReturn(false)
+        persistentTaskBarStashController.launcherState = BubbleLauncherState.IN_APP
+
+        // Reset invocations to track only changes from in-app display override
+        clearInvocations(taskbarInsetsController)
+
+        persistentTaskBarStashController.inAppDisplayOverrideProgress = 0.5f
+        persistentTaskBarStashController.inAppDisplayOverrideProgress = 1f
+        persistentTaskBarStashController.inAppDisplayOverrideProgress = 0f
+
+        // Never triggers an update to insets
+        verify(taskbarInsetsController, never()).onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
+    }
+
     private fun advanceTimeBy(advanceMs: Long) {
         // Advance animator for on-device tests
         getInstrumentation().runOnMainSync { animatorTestRule.advanceTimeBy(advanceMs) }
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/StashingTestUtils.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/StashingTestUtils.kt
index 0f8a2c3..96c2f45 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/StashingTestUtils.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/StashingTestUtils.kt
@@ -23,21 +23,13 @@
 class DefaultDimensionsProvider(
     private val taskBarBottomSpace: Int = TASKBAR_BOTTOM_SPACE,
     private val taskBarHeight: Int = TASKBAR_HEIGHT,
-    private val hotseatBottomSpace: Int = HOTSEAT_BOTTOM_SPACE,
-    private val hotseatHeight: Int = HOTSEAT_HEIGHT
 ) : BubbleStashController.TaskbarHotseatDimensionsProvider {
     override fun getTaskbarBottomSpace(): Int = taskBarBottomSpace
 
     override fun getTaskbarHeight(): Int = taskBarHeight
 
-    override fun getHotseatBottomSpace(): Int = hotseatBottomSpace
-
-    override fun getHotseatHeight(): Int = hotseatHeight
-
     companion object {
         const val TASKBAR_BOTTOM_SPACE = 0
         const val TASKBAR_HEIGHT = 110
-        const val HOTSEAT_BOTTOM_SPACE = 20
-        const val HOTSEAT_HEIGHT = 150
     }
 }
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashControllerTest.kt
index d4a3b3a..1bbd12a 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashControllerTest.kt
@@ -29,13 +29,16 @@
 import com.android.launcher3.anim.AnimatedFloat
 import com.android.launcher3.taskbar.StashedHandleView
 import com.android.launcher3.taskbar.TaskbarInsetsController
+import com.android.launcher3.taskbar.TaskbarStashController
 import com.android.launcher3.taskbar.bubbles.BubbleBarView
 import com.android.launcher3.taskbar.bubbles.BubbleBarViewController
 import com.android.launcher3.taskbar.bubbles.BubbleStashedHandleViewController
 import com.android.launcher3.taskbar.bubbles.BubbleView
+import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.BubbleLauncherState
 import com.android.launcher3.util.MultiValueAlpha
 import com.android.wm.shell.shared.animation.PhysicsAnimator
 import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils
+import com.google.common.collect.Range
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
 import org.junit.Rule
@@ -46,6 +49,7 @@
 import org.mockito.junit.MockitoRule
 import org.mockito.kotlin.any
 import org.mockito.kotlin.atLeastOnce
+import org.mockito.kotlin.never
 import org.mockito.kotlin.verify
 import org.mockito.kotlin.whenever
 
@@ -56,10 +60,11 @@
 
     companion object {
         const val TASKBAR_BOTTOM_SPACE = 5
+        const val HOTSEAT_VERTICAL_CENTER = 95
         const val BUBBLE_BAR_WIDTH = 200
         const val BUBBLE_BAR_HEIGHT = 100
         const val HOTSEAT_TRANSLATION_Y = -45f
-        const val TASK_BAR_TRANSLATION_Y = -TASKBAR_BOTTOM_SPACE
+        const val TASK_BAR_TRANSLATION_Y = -TASKBAR_BOTTOM_SPACE.toFloat()
         const val HANDLE_VIEW_WIDTH = 150
         const val HANDLE_VIEW_HEIGHT = 4
         const val BUBBLE_BAR_STASHED_TRANSLATION_Y = -4.5f
@@ -104,6 +109,7 @@
         setUpStashedHandleView()
         setUpBubbleStashedHandleViewController()
         PhysicsAnimatorTestUtils.prepareForTest()
+        mTransientBubbleStashController.setHotseatVerticalCenter(HOTSEAT_VERTICAL_CENTER)
         mTransientBubbleStashController.init(
             taskbarInsetsController,
             bubbleBarViewController,
@@ -113,13 +119,27 @@
     }
 
     @Test
+    fun updateLauncherState_noBubbles_controllerNotified() {
+        // Given bubble bar has  no bubbles
+        whenever(bubbleBarViewController.hasBubbles()).thenReturn(false)
+
+        // When switch to home screen
+        getInstrumentation().runOnMainSync {
+            mTransientBubbleStashController.launcherState = BubbleLauncherState.HOME
+        }
+
+        // Then bubble bar view controller is notified
+        verify(bubbleBarViewController).onBubbleBarConfigurationChanged(/* animate= */ false)
+    }
+
+    @Test
     fun setBubblesShowingOnHomeUpdatedToTrue_barPositionYUpdated_controllersNotified() {
         // Given bubble bar is on home and has bubbles
         whenever(bubbleBarViewController.hasBubbles()).thenReturn(true)
 
         // When switch out of the home screen
         getInstrumentation().runOnMainSync {
-            mTransientBubbleStashController.isBubblesShowingOnHome = true
+            mTransientBubbleStashController.launcherState = BubbleLauncherState.HOME
         }
 
         // Then BubbleBarView is animating, BubbleBarViewController controller is notified
@@ -137,12 +157,12 @@
 
     @Test
     fun setBubblesShowingOnOverviewUpdatedToTrue_barPositionYUpdated_controllersNotified() {
-        // Given bubble bar is on home and has bubbles
+        // Given bubble bar is on overview and has bubbles
         whenever(bubbleBarViewController.hasBubbles()).thenReturn(true)
 
         // When switch out of the home screen
         getInstrumentation().runOnMainSync {
-            mTransientBubbleStashController.isBubblesShowingOnOverview = true
+            mTransientBubbleStashController.launcherState = BubbleLauncherState.OVERVIEW
         }
 
         // Then BubbleBarView is animating, BubbleBarViewController controller is notified
@@ -159,6 +179,27 @@
     }
 
     @Test
+    fun setBubblesShowingOnOverviewUpdatedToTrue_unstashes() {
+        // Given bubble bar is stashed with bubbles
+        whenever(bubbleBarViewController.hasBubbles()).thenReturn(true)
+
+        getInstrumentation().runOnMainSync {
+            mTransientBubbleStashController.updateStashedAndExpandedState(
+                stash = true,
+                expand = false,
+            )
+        }
+        assertThat(mTransientBubbleStashController.isStashed).isTrue()
+
+        // Move to overview
+        getInstrumentation().runOnMainSync {
+            mTransientBubbleStashController.launcherState = BubbleLauncherState.OVERVIEW
+        }
+        // No longer stashed in overview
+        assertThat(mTransientBubbleStashController.isStashed).isFalse()
+    }
+
+    @Test
     fun updateStashedAndExpandedState_stashAndCollapse_bubbleBarHidden_stashedHandleShown() {
         // Given bubble bar has bubbles and not stashed
         mTransientBubbleStashController.isStashed = false
@@ -196,11 +237,136 @@
     }
 
     @Test
+    fun updateStashedAndExpandedState_unstash_bubbleBarShown_stashedHandleHidden() {
+        // Given bubble bar has bubbles and is stashed
+        mTransientBubbleStashController.isStashed = true
+        whenever(bubbleBarViewController.isHiddenForNoBubbles).thenReturn(false)
+
+        val bubbleInitialTranslation = bubbleView.translationY
+
+        // When unstash
+        getInstrumentation().runOnMainSync {
+            mTransientBubbleStashController.updateStashedAndExpandedState(
+                stash = false,
+                expand = false,
+            )
+        }
+
+        // Wait until animations ends
+        advanceTimeBy(BubbleStashController.BAR_STASH_DURATION)
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        // Then check BubbleBarController is notified
+        verify(bubbleBarViewController).onStashStateChanging()
+        // Bubble bar is unstashed
+        assertThat(mTransientBubbleStashController.isStashed).isFalse()
+        assertThat(bubbleBarView.translationY).isEqualTo(TASK_BAR_TRANSLATION_Y)
+        assertThat(bubbleBarView.alpha).isEqualTo(1f)
+        assertThat(bubbleBarView.scaleX).isEqualTo(1f)
+        assertThat(bubbleBarView.scaleY).isEqualTo(1f)
+        assertThat(bubbleBarView.background.alpha).isEqualTo(255)
+        // Handle view is hidden
+        assertThat(stashedHandleView.translationY).isEqualTo(0)
+        assertThat(stashedHandleView.alpha).isEqualTo(0)
+        // Bubble view is reset
+        assertThat(bubbleView.translationY).isEqualTo(bubbleInitialTranslation)
+        assertThat(bubbleView.alpha).isEqualTo(1f)
+    }
+
+    @Test
+    fun updateStashedAndExpandedState_stash_animatesAlphaForBubblesAndBackgroundSeparately() {
+        // Given bubble bar has bubbles and is unstashed
+        mTransientBubbleStashController.isStashed = false
+        whenever(bubbleBarViewController.isHiddenForNoBubbles).thenReturn(false)
+
+        // When stash
+        getInstrumentation().runOnMainSync {
+            mTransientBubbleStashController.updateStashedAndExpandedState(
+                stash = true,
+                expand = false,
+            )
+        }
+
+        // Stop after alpha starts
+        advanceTimeBy(TaskbarStashController.TASKBAR_STASH_ALPHA_START_DELAY + 10)
+
+        // Bubble bar alpha is set to 1
+        assertThat(bubbleBarView.alpha).isEqualTo(1f)
+        // We animate alpha for background and children separately
+        assertThat(bubbleView.alpha).isIn(Range.open(0f, 1f))
+        assertThat(bubbleBarView.background.alpha).isIn(Range.open(0, 255))
+        assertThat(bubbleBarView.background.alpha).isNotEqualTo((bubbleView.alpha * 255f).toInt())
+    }
+
+    @Test
+    fun updateStashedAndExpandedState_unstash_animatesAlphaForBubblesAndBackgroundSeparately() {
+        // Given bubble bar has bubbles and is stashed
+        mTransientBubbleStashController.isStashed = true
+        whenever(bubbleBarViewController.isHiddenForNoBubbles).thenReturn(false)
+
+        // When unstash
+        getInstrumentation().runOnMainSync {
+            mTransientBubbleStashController.updateStashedAndExpandedState(
+                stash = false,
+                expand = false,
+            )
+        }
+
+        // Stop after alpha starts
+        advanceTimeBy(TaskbarStashController.TASKBAR_STASH_ALPHA_START_DELAY + 10)
+
+        // Bubble bar alpha is set to 1
+        assertThat(bubbleBarView.alpha).isEqualTo(1f)
+        // We animate alpha for background and children separately
+        assertThat(bubbleView.alpha).isIn(Range.open(0f, 1f))
+        assertThat(bubbleBarView.background.alpha).isIn(Range.open(0, 255))
+        assertThat(bubbleBarView.background.alpha).isNotEqualTo((bubbleView.alpha * 255f).toInt())
+    }
+
+    @Test
+    fun updateStashedAndExpandedState_stash_updateBarVisibilityAfterAnimation() {
+        // Given bubble bar has bubbles and is unstashed
+        mTransientBubbleStashController.isStashed = false
+        whenever(bubbleBarViewController.isHiddenForNoBubbles).thenReturn(false)
+
+        // When stash
+        getInstrumentation().runOnMainSync {
+            mTransientBubbleStashController.updateStashedAndExpandedState(
+                stash = true,
+                expand = false,
+            )
+        }
+
+        // Hides bubble bar only after animation completes
+        verify(bubbleBarViewController, never()).setHiddenForStashed(true)
+        advanceTimeBy(BubbleStashController.BAR_STASH_DURATION)
+        verify(bubbleBarViewController).setHiddenForStashed(true)
+    }
+
+    @Test
+    fun updateStashedAndExpandedState_unstash_updateBarVisibilityBeforeAnimation() {
+        // Given bubble bar has bubbles and is stashed
+        mTransientBubbleStashController.isStashed = true
+        whenever(bubbleBarViewController.isHiddenForNoBubbles).thenReturn(false)
+
+        // When unstash
+        getInstrumentation().runOnMainSync {
+            mTransientBubbleStashController.updateStashedAndExpandedState(
+                stash = false,
+                expand = false,
+            )
+        }
+
+        // Shows bubble bar immediately
+        verify(bubbleBarViewController).setHiddenForStashed(false)
+    }
+
+    @Test
     fun isSysuiLockedSwitchedToFalseForOverview_unlockAnimationIsShown() {
         // Given screen is locked and bubble bar has bubbles
         getInstrumentation().runOnMainSync {
             mTransientBubbleStashController.isSysuiLocked = true
-            mTransientBubbleStashController.isBubblesShowingOnOverview = true
+            mTransientBubbleStashController.launcherState = BubbleLauncherState.OVERVIEW
             whenever(bubbleBarViewController.hasBubbles()).thenReturn(true)
         }
         advanceTimeBy(BubbleStashController.BAR_TRANSLATION_DURATION)
@@ -247,6 +413,8 @@
         assertThat(stashedHandleView.alpha).isEqualTo(0)
         // Insets controller is notified
         verify(taskbarInsetsController).onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
+        // Bubble bar visibility updated
+        verify(bubbleBarViewController).setHiddenForStashed(false)
     }
 
     @Test
@@ -264,6 +432,8 @@
         assertThat(stashedHandleView.translationY).isEqualTo(0)
         // Insets controller is notified
         verify(taskbarInsetsController).onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
+        // Bubble bar visibility updated
+        verify(bubbleBarViewController).setHiddenForStashed(true)
     }
 
     @Test
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 4fa821d..1113129 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.platform.app.InstrumentationRegistry.getInstrumentation
 import com.android.launcher3.AbstractFloatingView
 import com.android.launcher3.AbstractFloatingView.TYPE_OPTIONS_POPUP
 import com.android.launcher3.AbstractFloatingView.TYPE_TASKBAR_ALL_APPS
@@ -42,12 +41,8 @@
 @EmulatedDevices(["pixelFoldable2023"])
 class TaskbarOverlayControllerTest {
 
-    @get:Rule
-    val taskbarUnitTestRule =
-        TaskbarUnitTestRule(
-            this,
-            TaskbarWindowSandboxContext.create(getInstrumentation().targetContext),
-        )
+    @get:Rule(order = 0) val context = TaskbarWindowSandboxContext.create()
+    @get:Rule(order = 1) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context)
     @InjectController lateinit var overlayController: TaskbarOverlayController
 
     private val taskbarContext: TaskbarActivityContext
@@ -223,9 +218,8 @@
     }
 
     private class TestOverlayView
-    private constructor(
-        private val overlayContext: TaskbarOverlayContext,
-    ) : AbstractFloatingView(overlayContext, null) {
+    private constructor(private val overlayContext: TaskbarOverlayContext) :
+        AbstractFloatingView(overlayContext, null) {
 
         var type = TYPE_OPTIONS_POPUP
 
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRule.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRule.kt
index c48947e..74b154a 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRule.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRule.kt
@@ -61,7 +61,7 @@
                 val mode = taskbarMode.mode
 
                 getInstrumentation().runOnMainSync {
-                    context.applicationContext.putObject(
+                    context.putObject(
                         DisplayController.INSTANCE,
                         object : DisplayController(context) {
                             override fun getInfo(): Info {
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRuleTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRuleTest.kt
index f7e4576..0dd1324 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRuleTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRuleTest.kt
@@ -16,7 +16,6 @@
 
 package com.android.launcher3.taskbar.rules
 
-import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
 import com.android.launcher3.InvariantDeviceProfile
 import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.PINNED
 import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.THREE_BUTTONS
@@ -35,9 +34,8 @@
 @EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"])
 class TaskbarModeRuleTest {
 
-    private val context = TaskbarWindowSandboxContext.create(getInstrumentation().targetContext)
-
-    @get:Rule val taskbarModeRule = TaskbarModeRule(context)
+    @get:Rule(order = 0) val context = TaskbarWindowSandboxContext.create()
+    @get:Rule(order = 1) val taskbarModeRule = TaskbarModeRule(context)
 
     @Test
     @TaskbarMode(TRANSIENT)
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
deleted file mode 100644
index d417790..0000000
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPinningPreferenceRule.kt
+++ /dev/null
@@ -1,65 +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.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
deleted file mode 100644
index a515405..0000000
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPinningPreferenceRuleTest.kt
+++ /dev/null
@@ -1,114 +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.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.LauncherMultivalentJUnit.EmulatedDevices
-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)
-@EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"])
-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
deleted file mode 100644
index a76a77d..0000000
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPreferenceRule.kt
+++ /dev/null
@@ -1,54 +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.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
deleted file mode 100644
index 46817d2..0000000
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPreferenceRuleTest.kt
+++ /dev/null
@@ -1,67 +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.rules
-
-import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
-import com.android.launcher3.LauncherPrefs.Companion.TASKBAR_PINNING
-import com.android.launcher3.util.LauncherMultivalentJUnit
-import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices
-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)
-@EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"])
-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
index cb5e464..096f879 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt
@@ -19,27 +19,26 @@
 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 android.provider.Settings.Secure.getUriFor
 import androidx.test.platform.app.InstrumentationRegistry
-import androidx.test.rule.ServiceTestRule
 import com.android.launcher3.LauncherAppState
 import com.android.launcher3.statehandlers.DesktopVisibilityController
 import com.android.launcher3.taskbar.TaskbarActivityContext
+import com.android.launcher3.taskbar.TaskbarControllers
 import com.android.launcher3.taskbar.TaskbarManager
 import com.android.launcher3.taskbar.TaskbarNavButtonController.TaskbarNavButtonCallbacks
 import com.android.launcher3.taskbar.TaskbarViewController
+import com.android.launcher3.taskbar.bubbles.BubbleControllers
 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.TestUtil
 import com.android.quickstep.AllAppsActionManager
-import com.android.quickstep.TouchInteractionService
-import com.android.quickstep.TouchInteractionService.TISBinder
+import java.lang.reflect.Field
+import java.lang.reflect.ParameterizedType
+import java.util.Optional
 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
@@ -76,11 +75,6 @@
 ) : 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
 
@@ -91,10 +85,6 @@
         }
 
     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() {
 
@@ -106,34 +96,10 @@
                 }
 
                 // 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
-                    }
+                context.settingsCacheSandbox[getUriFor(USER_SETUP_COMPLETE)] =
+                    if (description.getAnnotation(UserSetupMode::class.java) != null) 0 else 1
+                context.settingsCacheSandbox[getUriFor(NAV_BAR_KIDS_MODE)] =
+                    if (description.getAnnotation(NavBarKidsMode::class.java) != null) 1 else 0
 
                 taskbarManager =
                     TestUtil.getOnUiThread {
@@ -156,20 +122,12 @@
                 try {
                     TaskbarViewController.enableModelLoadingForTests(false)
 
-                    // Replace Launcher Taskbar window with test instance.
-                    instrumentation.runOnMainSync {
-                        launcherTaskbarManager?.setSuspended(true)
-                        taskbarManager.onUserUnlocked() // Required to complete initialization.
-                    }
+                    // Required to complete initialization.
+                    instrumentation.runOnMainSync { taskbarManager.onUserUnlocked() }
 
                     base.evaluate()
                 } finally {
-                    // Revert Taskbar window.
-                    instrumentation.runOnMainSync {
-                        taskbarManager.destroy()
-                        launcherTaskbarManager?.setSuspended(false)
-                    }
-
+                    instrumentation.runOnMainSync { taskbarManager.destroy() }
                     TaskbarViewController.enableModelLoadingForTests(true)
                 }
             }
@@ -180,19 +138,38 @@
     fun recreateTaskbar() = instrumentation.runOnMainSync { taskbarManager.recreateTaskbar() }
 
     private fun injectControllers() {
-        val controllers = activityContext.controllers
-        val controllerFieldsByType = controllers.javaClass.fields.associateBy { it.type }
+        val bubbleControllerTypes =
+            BubbleControllers::class.java.fields.map { f ->
+                if (f.type == Optional::class.java) {
+                    (f.genericType as ParameterizedType).actualTypeArguments[0] as Class<*>
+                } else {
+                    f.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}"),
-                )
+                val controllers: Any =
+                    if (it.type in bubbleControllerTypes) {
+                        activityContext.controllers.bubbleControllers.orElseThrow {
+                            NoSuchElementException("Bubble controllers are not initialized")
+                        }
+                    } else {
+                        activityContext.controllers
+                    }
+                injectController(it, testInstance, controllers)
             }
     }
 
+    private fun injectController(field: Field, testInstance: Any, controllers: Any) {
+        val controllerFieldsByType = controllers.javaClass.fields.associateBy { it.type }
+        field.set(
+            testInstance,
+            controllerFieldsByType[field.type]?.get(controllers)
+                ?: throw NoSuchElementException("Failed to find controller for ${field.type}"),
+        )
+    }
+
     /**
      * Annotates test controller fields to inject the corresponding controllers from the current
      * [TaskbarControllers] instance.
@@ -214,25 +191,4 @@
     @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
index 5d4fdc5..7daa142 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRuleTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRuleTest.kt
@@ -16,18 +16,23 @@
 
 package com.android.launcher3.taskbar.rules
 
-import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
 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.bubbles.BubbleBarController
 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.android.wm.shell.Flags
 import com.google.common.truth.Truth.assertThat
 import org.junit.Assert.assertThrows
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.Description
 import org.junit.runner.RunWith
@@ -37,7 +42,8 @@
 @EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"])
 class TaskbarUnitTestRuleTest {
 
-    private val context = TaskbarWindowSandboxContext.create(getInstrumentation().targetContext)
+    @get:Rule(order = 0) val context = TaskbarWindowSandboxContext.create()
+    @get:Rule(order = 1) val setFlagsRule = SetFlagsRule()
 
     @Test
     fun testSetup_taskbarInitialized() {
@@ -127,6 +133,44 @@
         }
     }
 
+    @EnableFlags(Flags.FLAG_ENABLE_BUBBLE_BAR)
+    @Test
+    fun testInjectBubbleController_bubbleFlagOn_isInjected() {
+        val testClass =
+            object {
+                @InjectController lateinit var controller: BubbleBarController
+                val isInjected: Boolean
+                    get() = ::controller.isInitialized
+            }
+
+        TaskbarUnitTestRule(testClass, context).apply(EMPTY_STATEMENT, DESCRIPTION).evaluate()
+
+        onSetup(TaskbarUnitTestRule(testClass, context)) {
+            assertThat(testClass.isInjected).isTrue()
+        }
+    }
+
+    @DisableFlags(Flags.FLAG_ENABLE_BUBBLE_BAR)
+    @Test
+    fun testInjectBubbleController_bubbleFlagOff_exceptionThrown() {
+        val testClass =
+            object {
+                @InjectController lateinit var controller: BubbleBarController
+            }
+
+        // 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 testUserSetupMode_default_isComplete() {
         onSetup { assertThat(activityContext.isUserSetupComplete).isTrue() }
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContext.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContext.kt
index ee21df8..8c51216 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContext.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContext.kt
@@ -18,44 +18,112 @@
 
 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
+import android.hardware.display.DisplayManager
+import android.hardware.display.VirtualDisplay
+import android.view.Display.DEFAULT_DISPLAY
+import androidx.test.core.app.ApplicationProvider
+import com.android.launcher3.FakeLauncherPrefs
+import com.android.launcher3.LauncherPrefs
+import com.android.launcher3.dagger.LauncherAppComponent
+import com.android.launcher3.dagger.LauncherAppSingleton
+import com.android.launcher3.util.MainThreadInitializedObject.ObjectSandbox
+import com.android.launcher3.util.SandboxApplication
+import com.android.launcher3.util.SettingsCache
+import com.android.launcher3.util.SettingsCacheSandbox
+import com.android.quickstep.SystemUiProxy
+import dagger.BindsInstance
+import dagger.Component
+import org.junit.rules.ExternalResource
+import org.junit.rules.RuleChain
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+/** Include additional bindings when building a [TaskbarSandboxComponent]. */
+typealias TaskbarComponentBinder =
+    TaskbarWindowSandboxContext.(TaskbarSandboxComponent.Builder) -> Unit
 
 /**
- * Sandbox wrapper where [createWindowContext] provides contexts that are still sandboxed within
- * [application].
+ * [SandboxApplication] for running Taskbar tests.
  *
- * 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].
+ * Tests need to run on a [VirtualDisplay] to avoid conflicting with Launcher's Taskbar on the
+ * [DEFAULT_DISPLAY] (i.e. test is executing on a device).
  */
 class TaskbarWindowSandboxContext
-private constructor(private val application: SandboxContext, base: Context) : ContextWrapper(base) {
+private constructor(
+    private val base: SandboxApplication,
+    val virtualDisplay: VirtualDisplay,
+    private val componentBinder: TaskbarComponentBinder?,
+) : ContextWrapper(base), ObjectSandbox by base, TestRule {
 
-    override fun createWindowContext(type: Int, options: Bundle?): Context {
-        return TaskbarWindowSandboxContext(application, super.createWindowContext(type, options))
+    val settingsCacheSandbox = SettingsCacheSandbox()
+
+    private val virtualDisplayRule =
+        object : ExternalResource() {
+            override fun after() = virtualDisplay.release()
+        }
+
+    private val singletonSetupRule =
+        object : ExternalResource() {
+            override fun before() {
+                val context = this@TaskbarWindowSandboxContext
+                val builder =
+                    DaggerTaskbarSandboxComponent.builder()
+                        .bindSystemUiProxy(SystemUiProxy(context))
+                        .bindSettingsCache(settingsCacheSandbox.cache)
+                componentBinder?.invoke(context, builder)
+                base.initDaggerComponent(builder)
+
+                putObject(LauncherPrefs.INSTANCE, FakeLauncherPrefs(context))
+            }
+        }
+
+    override fun apply(statement: Statement, description: Description): Statement {
+        return RuleChain.outerRule(virtualDisplayRule)
+            .around(base)
+            .around(singletonSetupRule)
+            .apply(statement, description)
     }
 
-    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) }
+        private const val VIRTUAL_DISPLAY_NAME = "TaskbarSandboxDisplay"
+
+        /** Creates a [SandboxApplication] for Taskbar tests. */
+        fun create(componentBinder: TaskbarComponentBinder? = null): TaskbarWindowSandboxContext {
+            val base = ApplicationProvider.getApplicationContext<Context>()
+            val displayManager = checkNotNull(base.getSystemService(DisplayManager::class.java))
+
+            // Create virtual display to avoid clashing with Taskbar on default display.
+            val virtualDisplay =
+                base.resources.displayMetrics.let {
+                    displayManager.createVirtualDisplay(
+                        VIRTUAL_DISPLAY_NAME,
+                        it.widthPixels,
+                        it.heightPixels,
+                        it.densityDpi,
+                        /* surface= */ null,
+                        /* flags= */ 0,
+                    )
+                }
+
+            return TaskbarWindowSandboxContext(
+                SandboxApplication(base.createDisplayContext(virtualDisplay.display)),
+                virtualDisplay,
+                componentBinder,
+            )
         }
     }
 }
+
+@LauncherAppSingleton
+@Component
+interface TaskbarSandboxComponent : LauncherAppComponent {
+    @Component.Builder
+    interface Builder : LauncherAppComponent.Builder {
+        @BindsInstance fun bindSystemUiProxy(proxy: SystemUiProxy): Builder
+
+        @BindsInstance fun bindSettingsCache(settingsCache: SettingsCache): Builder
+
+        override fun build(): TaskbarSandboxComponent
+    }
+}
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
index 4834d48..69095e7 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContextTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContextTest.kt
@@ -16,30 +16,32 @@
 
 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.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices
 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)
-@LauncherMultivalentJUnit.EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"])
+@EmulatedDevices(["pixelFoldable2023"])
 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)
-    }
+    fun testVirtualDisplay_releasedOnTeardown() {
+        val context = TaskbarWindowSandboxContext.create()
+        assertThat(context.virtualDisplay.token).isNotNull()
 
-    @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)
+        context
+            .apply(
+                object : Statement() {
+                    override fun evaluate() = Unit
+                },
+                Description.createSuiteDescription(TaskbarWindowSandboxContextTest::class.java),
+            )
+            .evaluate()
+
+        assertThat(context.virtualDisplay.token).isNull()
     }
 }
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/util/SettingsCacheSandbox.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/util/SettingsCacheSandbox.kt
new file mode 100644
index 0000000..dcd5352
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/util/SettingsCacheSandbox.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.net.Uri
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.mock
+
+/**
+ * Provides a sandboxed [SettingsCache] for testing.
+ *
+ * Note that listeners registered to [cache] will never be invoked.
+ */
+class SettingsCacheSandbox {
+    private val values = mutableMapOf<Uri, Int>()
+
+    /** Fake cache that delegates [SettingsCache.getValue] to [values]. */
+    val cache =
+        mock<SettingsCache> {
+            on { getValue(any<Uri>()) } doAnswer { mock.getValue(it.getArgument(0), 1) }
+            on { getValue(any<Uri>(), any<Int>()) } doAnswer
+                {
+                    values.getOrDefault(it.getArgument(0), it.getArgument(1)) == 1
+                }
+        }
+
+    operator fun get(key: Uri): Int? = values[key]
+
+    operator fun set(key: Uri, value: Int) {
+        values[key] = value
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/AbsSwipeUpHandlerTestCase.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/AbsSwipeUpHandlerTestCase.java
index 2a0aa4c..6b95f8d 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/AbsSwipeUpHandlerTestCase.java
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/AbsSwipeUpHandlerTestCase.java
@@ -16,15 +16,22 @@
 
 package com.android.quickstep;
 
+import static com.android.quickstep.AbsSwipeUpHandler.STATE_HANDLER_INVALIDATED;
+
+import static junit.framework.TestCase.assertFalse;
+import static junit.framework.TestCase.assertTrue;
+
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeast;
 import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.animation.ValueAnimator;
 import android.app.ActivityManager;
 import android.content.Context;
 import android.content.Intent;
@@ -38,15 +45,17 @@
 import android.view.ViewTreeObserver;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.test.platform.app.InstrumentationRegistry;
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.LauncherRootView;
 import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.statemanager.BaseState;
-import com.android.launcher3.statemanager.StatefulActivity;
+import com.android.launcher3.statemanager.StatefulContainer;
 import com.android.launcher3.util.SystemUiController;
-import com.android.quickstep.util.ActivityInitListener;
+import com.android.quickstep.fallback.window.RecentsWindowManager;
+import com.android.quickstep.util.ContextInitListener;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.RecentsViewContainer;
 import com.android.systemui.shared.system.InputConsumerController;
@@ -54,6 +63,7 @@
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
@@ -62,18 +72,14 @@
 import java.util.HashMap;
 
 public abstract class AbsSwipeUpHandlerTestCase<
-        RECENTS_CONTAINER extends Context & RecentsViewContainer,
-        STATE extends BaseState<STATE>,
-        RECENTS_VIEW extends RecentsView<RECENTS_CONTAINER, STATE>,
-        ACTIVITY_TYPE extends  StatefulActivity<STATE> & RecentsViewContainer,
-        ACTIVITY_INTERFACE extends BaseActivityInterface<STATE, ACTIVITY_TYPE>,
-        SWIPE_HANDLER extends AbsSwipeUpHandler<RECENTS_CONTAINER, RECENTS_VIEW, STATE>> {
+        STATE_TYPE extends BaseState<STATE_TYPE>,
+        RECENTS_CONTAINER extends Context & RecentsViewContainer & StatefulContainer<STATE_TYPE>,
+        RECENTS_VIEW extends RecentsView<RECENTS_CONTAINER, STATE_TYPE>,
+        SWIPE_HANDLER extends AbsSwipeUpHandler<RECENTS_CONTAINER, RECENTS_VIEW, STATE_TYPE>,
+        CONTAINER_INTERFACE extends BaseContainerInterface<STATE_TYPE, RECENTS_CONTAINER>> {
 
     protected final Context mContext =
             InstrumentationRegistry.getInstrumentation().getTargetContext();
-    protected final TaskAnimationManager mTaskAnimationManager = new TaskAnimationManager(mContext);
-    protected final RecentsAnimationDeviceState mRecentsAnimationDeviceState =
-            new RecentsAnimationDeviceState(mContext, true);
     protected final InputConsumerController mInputConsumerController =
             InputConsumerController.getRecentsAnimationInputConsumer();
     protected final ActivityManager.RunningTaskInfo mRunningTaskInfo =
@@ -105,10 +111,13 @@
             /* minimizedHomeBounds= */ null,
             new Bundle());
 
-    @Mock protected ACTIVITY_INTERFACE mActivityInterface;
-    @Mock protected ActivityInitListener<?> mActivityInitListener;
+    protected TaskAnimationManager mTaskAnimationManager;
+    protected RecentsAnimationDeviceState mRecentsAnimationDeviceState;
+
+    @Mock protected CONTAINER_INTERFACE mActivityInterface;
+    @Mock protected ContextInitListener<?> mContextInitListener;
     @Mock protected RecentsAnimationController mRecentsAnimationController;
-    @Mock protected STATE mState;
+    @Mock protected STATE_TYPE mState;
     @Mock protected ViewTreeObserver mViewTreeObserver;
     @Mock protected DragLayer mDragLayer;
     @Mock protected LauncherRootView mRootView;
@@ -148,6 +157,7 @@
 
     @Before
     public void setUpRecentsContainer() {
+        mTaskAnimationManager = new TaskAnimationManager(mContext, getRecentsWindowManager());
         RecentsViewContainer recentsContainer = getRecentsContainer();
         RECENTS_VIEW recentsView = getRecentsView();
 
@@ -157,7 +167,7 @@
         when(recentsContainer.getRootView()).thenReturn(mRootView);
         when(recentsContainer.getSystemUiController()).thenReturn(mSystemUiController);
         when(mActivityInterface.createActivityInitListener(any()))
-                .thenReturn(mActivityInitListener);
+                .thenReturn(mContextInitListener);
         doReturn(recentsContainer).when(mActivityInterface).getCreatedContainer();
         doAnswer(answer -> {
             answer.<Runnable>getArgument(0).run();
@@ -165,12 +175,18 @@
         }).when(recentsContainer).runOnBindToTouchInteractionService(any());
     }
 
+    @Before
+    public void setUpRecentsAnimationDeviceState() {
+        runOnMainSync(() ->
+                mRecentsAnimationDeviceState = new RecentsAnimationDeviceState(mContext, true));
+    }
+
     @Test
     public void testInitWhenReady_registersActivityInitListener() {
         String reasonString = "because i said so";
 
         createSwipeHandler().initWhenReady(reasonString);
-        verify(mActivityInitListener).register(eq(reasonString));
+        verify(mContextInitListener).register(eq(reasonString));
     }
 
     @Test
@@ -178,7 +194,7 @@
         createSwipeHandler()
                 .onRecentsAnimationCanceled(new HashMap<>());
 
-        runOnMainSync(() -> verify(mActivityInitListener)
+        runOnMainSync(() -> verify(mContextInitListener)
                 .unregister(eq("AbsSwipeUpHandler.onRecentsAnimationCanceled")));
     }
 
@@ -186,7 +202,7 @@
     public void testOnConsumerAboutToBeSwitched_unregistersActivityInitListener() {
         createSwipeHandler().onConsumerAboutToBeSwitched();
 
-        runOnMainSync(() -> verify(mActivityInitListener)
+        runOnMainSync(() -> verify(mContextInitListener)
                 .unregister("AbsSwipeUpHandler.invalidateHandler"));
     }
 
@@ -195,7 +211,7 @@
         createSwipeUpHandlerForGesture(GestureState.GestureEndTarget.NEW_TASK)
                 .onConsumerAboutToBeSwitched();
 
-        runOnMainSync(() -> verify(mActivityInitListener)
+        runOnMainSync(() -> verify(mContextInitListener)
                 .unregister(eq("AbsSwipeUpHandler.cancelCurrentAnimation")));
     }
 
@@ -207,7 +223,7 @@
 
         runOnMainSync(() -> {
             absSwipeUpHandler.startNewTask(unused -> {});
-            verify(mRecentsAnimationController).finish(anyBoolean(), any());
+            verifyRecentsAnimationFinishedAndCallCallback();
         });
     }
 
@@ -217,10 +233,57 @@
 
         runOnMainSync(() -> {
             verify(mRecentsAnimationController).detachNavigationBarFromApp(true);
-            verify(mRecentsAnimationController).finish(anyBoolean(), any(), anyBoolean());
+            verifyRecentsAnimationFinishedAndCallCallback();
         });
     }
 
+    @Test
+    public void testHomeGesture_invalidatesHandlerAfterParallelAnim() {
+        ValueAnimator parallelAnim = new ValueAnimator();
+        parallelAnim.setRepeatCount(ValueAnimator.INFINITE);
+        when(mActivityInterface.getParallelAnimationToLauncher(any(), anyLong(), any()))
+                .thenReturn(parallelAnim);
+        SWIPE_HANDLER handler = createSwipeUpHandlerForGesture(GestureState.GestureEndTarget.HOME);
+        runOnMainSync(() -> {
+            parallelAnim.start();
+            verifyRecentsAnimationFinishedAndCallCallback();
+            assertFalse(handler.mStateCallback.hasStates(STATE_HANDLER_INVALIDATED));
+            parallelAnim.end();
+            assertTrue(handler.mStateCallback.hasStates(STATE_HANDLER_INVALIDATED));
+        });
+    }
+
+    @Test
+    public void testHomeGesture_invalidatesHandlerIfNoParallelAnim() {
+        when(mActivityInterface.getParallelAnimationToLauncher(any(), anyLong(), any()))
+                .thenReturn(null);
+        SWIPE_HANDLER handler = createSwipeUpHandlerForGesture(GestureState.GestureEndTarget.HOME);
+        runOnMainSync(() -> {
+            verifyRecentsAnimationFinishedAndCallCallback();
+            assertTrue(handler.mStateCallback.hasStates(STATE_HANDLER_INVALIDATED));
+        });
+    }
+
+    /**
+     * Verifies that RecentsAnimationController#finish() is called, and captures and runs any
+     * callback that was passed to it. This ensures that STATE_CURRENT_TASK_FINISHED is correctly
+     * set for example.
+     */
+    private void verifyRecentsAnimationFinishedAndCallCallback() {
+        ArgumentCaptor<Runnable> finishCallback = ArgumentCaptor.forClass(Runnable.class);
+        // Check if the 2 parameter method is called.
+        verify(mRecentsAnimationController, atLeast(0)).finish(
+                anyBoolean(), finishCallback.capture());
+        if (finishCallback.getAllValues().isEmpty()) {
+            // Check if the 3 parameter method is called.
+            verify(mRecentsAnimationController).finish(
+                    anyBoolean(), finishCallback.capture(), anyBoolean());
+        }
+        if (finishCallback.getValue() != null) {
+            finishCallback.getValue().run();
+        }
+    }
+
     private SWIPE_HANDLER createSwipeUpHandlerForGesture(GestureState.GestureEndTarget endTarget) {
         boolean isQuickSwitch = endTarget == GestureState.GestureEndTarget.NEW_TASK;
 
@@ -248,14 +311,11 @@
     }
 
     private void onRecentsAnimationStart(SWIPE_HANDLER absSwipeUpHandler) {
-        when(mActivityInterface.getOverviewWindowBounds(any(), any())).thenReturn(new Rect());
-        doNothing().when(mActivityInterface).setOnDeferredActivityLaunchCallback(any());
-
         runOnMainSync(() -> absSwipeUpHandler.onRecentsAnimationStart(
                 mRecentsAnimationController, mRecentsAnimationTargets));
     }
 
-    private static void runOnMainSync(Runnable runnable) {
+    protected static void runOnMainSync(Runnable runnable) {
         InstrumentationRegistry.getInstrumentation().runOnMainSync(runnable);
     }
 
@@ -264,6 +324,11 @@
         return createSwipeHandler(SystemClock.uptimeMillis(), false);
     }
 
+    @Nullable
+    protected RecentsWindowManager getRecentsWindowManager() {
+        return null;
+    }
+
     @NonNull
     protected abstract SWIPE_HANDLER createSwipeHandler(
             long touchTimeMs, boolean continuingLastGesture);
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/FallbackSwipeHandlerTestCase.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/FallbackSwipeHandlerTestCase.java
index dd0b4b3..88197e5 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/FallbackSwipeHandlerTestCase.java
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/FallbackSwipeHandlerTestCase.java
@@ -16,6 +16,7 @@
 
 package com.android.quickstep;
 
+import androidx.annotation.NonNull;
 import androidx.test.filters.SmallTest;
 
 import com.android.launcher3.util.LauncherMultivalentJUnit;
@@ -28,15 +29,14 @@
 @SmallTest
 @RunWith(LauncherMultivalentJUnit.class)
 public class FallbackSwipeHandlerTestCase extends AbsSwipeUpHandlerTestCase<
-        RecentsActivity,
         RecentsState,
-        FallbackRecentsView,
         RecentsActivity,
-        FallbackActivityInterface,
-        FallbackSwipeHandler> {
+        FallbackRecentsView<RecentsActivity>,
+        FallbackSwipeHandler,
+        FallbackActivityInterface> {
 
     @Mock private RecentsActivity mRecentsActivity;
-    @Mock private FallbackRecentsView mRecentsView;
+    @Mock private FallbackRecentsView<RecentsActivity> mRecentsView;
 
 
     @Override
@@ -52,13 +52,15 @@
                 mInputConsumerController);
     }
 
+    @NonNull
     @Override
     protected RecentsActivity getRecentsContainer() {
         return mRecentsActivity;
     }
 
+    @NonNull
     @Override
-    protected FallbackRecentsView getRecentsView() {
+    protected FallbackRecentsView<RecentsActivity> getRecentsView() {
         return mRecentsView;
     }
 }
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherSwipeHandlerV2Test.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherSwipeHandlerV2Test.kt
index 1f88743..32b5b85 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherSwipeHandlerV2Test.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherSwipeHandlerV2Test.kt
@@ -20,9 +20,14 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.launcher3.R
+import com.android.launcher3.dagger.LauncherAppComponent
+import com.android.launcher3.dagger.LauncherAppSingleton
 import com.android.launcher3.util.LauncherModelHelper
+import com.android.quickstep.dagger.QuickStepModule
 import com.android.systemui.contextualeducation.GestureType
 import com.android.systemui.shared.system.InputConsumerController
+import dagger.BindsInstance
+import dagger.Component
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
@@ -58,7 +63,9 @@
 
     @Before
     fun setup() {
-        sandboxContext.putObject(SystemUiProxy.INSTANCE, systemUiProxy)
+        sandboxContext.initDaggerComponent(
+            DaggerTestComponent.builder().bindSystemUiProxy(systemUiProxy)
+        )
         val deviceState = mock(RecentsAnimationDeviceState::class.java)
         whenever(deviceState.rotationTouchHelper).thenReturn(mock(RotationTouchHelper::class.java))
         gestureState = spy(GestureState(OverviewComponentObserver(sandboxContext, deviceState), 0))
@@ -71,7 +78,7 @@
                 gestureState,
                 0,
                 false,
-                inputConsumerController
+                inputConsumerController,
             )
         underTest.onGestureStarted(/* isLikelyToStartNewTask= */ false)
     }
@@ -83,7 +90,7 @@
         verify(systemUiProxy)
             .updateContextualEduStats(
                 /* isTrackpadGesture= */ eq(true),
-                eq(GestureType.HOME.toString())
+                eq(GestureType.HOME.toString()),
             )
     }
 
@@ -93,7 +100,18 @@
         verify(systemUiProxy)
             .updateContextualEduStats(
                 /* isTrackpadGesture= */ eq(false),
-                eq(GestureType.HOME.toString())
+                eq(GestureType.HOME.toString()),
             )
     }
 }
+
+@LauncherAppSingleton
+@Component(modules = [QuickStepModule::class])
+interface TestComponent : LauncherAppComponent {
+    @Component.Builder
+    interface Builder : LauncherAppComponent.Builder {
+        @BindsInstance fun bindSystemUiProxy(proxy: SystemUiProxy): Builder
+
+        override fun build(): TestComponent
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherSwipeHandlerV2TestCase.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherSwipeHandlerV2TestCase.java
index 653dc01..ec1dc8b 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherSwipeHandlerV2TestCase.java
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/LauncherSwipeHandlerV2TestCase.java
@@ -19,6 +19,7 @@
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.when;
 
+import androidx.annotation.NonNull;
 import androidx.test.filters.SmallTest;
 
 import com.android.launcher3.Hotseat;
@@ -38,12 +39,11 @@
 @SmallTest
 @RunWith(LauncherMultivalentJUnit.class)
 public class LauncherSwipeHandlerV2TestCase extends AbsSwipeUpHandlerTestCase<
-        QuickstepLauncher,
         LauncherState,
-        RecentsView<QuickstepLauncher, LauncherState>,
         QuickstepLauncher,
-        LauncherActivityInterface,
-        LauncherSwipeHandlerV2> {
+        RecentsView<QuickstepLauncher, LauncherState>,
+        LauncherSwipeHandlerV2,
+        LauncherActivityInterface> {
 
     @Mock private QuickstepLauncher mQuickstepLauncher;
     @Mock private RecentsView<QuickstepLauncher, LauncherState> mRecentsView;
@@ -67,6 +67,7 @@
         when(mWorkspace.getStateTransitionAnimation()).thenReturn(mTransitionAnimation);
     }
 
+    @NonNull
     @Override
     protected LauncherSwipeHandlerV2 createSwipeHandler(
             long touchTimeMs, boolean continuingLastGesture) {
@@ -80,11 +81,13 @@
                 mInputConsumerController);
     }
 
+    @NonNull
     @Override
     protected QuickstepLauncher getRecentsContainer() {
         return mQuickstepLauncher;
     }
 
+    @NonNull
     @Override
     protected RecentsView<QuickstepLauncher, LauncherState> getRecentsView() {
         return mRecentsView;
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentsWindowSwipeHandlerTestCase.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentsWindowSwipeHandlerTestCase.java
new file mode 100644
index 0000000..1bdf273
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentsWindowSwipeHandlerTestCase.java
@@ -0,0 +1,77 @@
+/*
+ * 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;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.test.filters.SmallTest;
+
+import com.android.launcher3.util.LauncherMultivalentJUnit;
+import com.android.quickstep.fallback.FallbackRecentsView;
+import com.android.quickstep.fallback.RecentsState;
+import com.android.quickstep.fallback.window.RecentsWindowManager;
+import com.android.quickstep.fallback.window.RecentsWindowSwipeHandler;
+import com.android.quickstep.views.RecentsViewContainer;
+
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+@SmallTest
+@RunWith(LauncherMultivalentJUnit.class)
+public class RecentsWindowSwipeHandlerTestCase extends AbsSwipeUpHandlerTestCase<
+        RecentsState,
+        RecentsWindowManager,
+        FallbackRecentsView<RecentsWindowManager>,
+        RecentsWindowSwipeHandler,
+        FallbackWindowInterface> {
+
+    @Mock private RecentsWindowManager mRecentsWindowManager;
+    @Mock private FallbackRecentsView<RecentsWindowManager> mRecentsView;
+
+    @NonNull
+    @Override
+    protected RecentsWindowSwipeHandler createSwipeHandler(long touchTimeMs,
+            boolean continuingLastGesture) {
+        return new RecentsWindowSwipeHandler(
+                mContext,
+                mRecentsAnimationDeviceState,
+                mTaskAnimationManager,
+                mGestureState,
+                touchTimeMs,
+                continuingLastGesture,
+                mInputConsumerController,
+                mRecentsWindowManager);
+    }
+
+    @Nullable
+    @Override
+    protected RecentsWindowManager getRecentsWindowManager() {
+        return mRecentsWindowManager;
+    }
+
+    @NonNull
+    @Override
+    protected RecentsViewContainer getRecentsContainer() {
+        return mRecentsWindowManager;
+    }
+
+    @NonNull
+    @Override
+    protected FallbackRecentsView<RecentsWindowManager> getRecentsView() {
+        return mRecentsView;
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressHandlerTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressHandlerTest.java
new file mode 100644
index 0000000..9018775
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressHandlerTest.java
@@ -0,0 +1,79 @@
+/*
+ * 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.inputconsumers;
+
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.quickstep.DeviceConfigWrapper;
+import com.android.quickstep.NavHandle;
+import com.android.quickstep.util.TestExtensions;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class NavHandleLongPressHandlerTest {
+
+    private NavHandleLongPressHandler mLongPressHandler;
+    @Mock private NavHandle mNavHandle;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+        mLongPressHandler = new NavHandleLongPressHandler(context);
+    }
+
+    @Test
+    public void testStartNavBarAnimation_flagDisabled() {
+        try (AutoCloseable flag = overrideAnimateLPNHFlag(false)) {
+            mLongPressHandler.startNavBarAnimation(mNavHandle);
+            verify(mNavHandle, never())
+                    .animateNavBarLongPress(anyBoolean(), anyBoolean(), anyLong());
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Test
+    public void testStartNavBarAnimation_flagEnabled() {
+        try (AutoCloseable flag = overrideAnimateLPNHFlag(true)) {
+            mLongPressHandler.startNavBarAnimation(mNavHandle);
+            verify(mNavHandle).animateNavBarLongPress(anyBoolean(), anyBoolean(), anyLong());
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private AutoCloseable overrideAnimateLPNHFlag(boolean value) {
+        return TestExtensions.overrideNavConfigFlag(
+                "ANIMATE_LPNH", value, () -> DeviceConfigWrapper.get().getAnimateLpnh());
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumerTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumerTest.java
index c18f604..98a3607 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumerTest.java
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumerTest.java
@@ -92,14 +92,15 @@
         when(mTopTaskTracker.getCachedTopTask(anyBoolean())).thenReturn(mTaskInfo);
         when(mDeviceState.getSquaredTouchSlop()).thenReturn(SQUARED_TOUCH_SLOP);
         when(mDelegate.allowInterceptByParent()).thenReturn(true);
-        MAIN_EXECUTOR.getHandler().removeCallbacks(mLongPressRunnable);
         mLongPressTriggered.set(false);
         when(mNavHandleLongPressHandler.getLongPressRunnable(any())).thenReturn(mLongPressRunnable);
         initializeObjectUnderTest();
     }
 
     @After
-    public void tearDown() {
+    public void tearDown() throws Exception {
+        MAIN_EXECUTOR.getHandler().removeCallbacks(mLongPressRunnable);
+        MAIN_EXECUTOR.submit(() -> null).get();
         mContext.onDestroy();
     }
 
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/logging/SettingsChangeLoggerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/logging/SettingsChangeLoggerTest.kt
index 0a60774..7c48ea4 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/logging/SettingsChangeLoggerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/logging/SettingsChangeLoggerTest.kt
@@ -34,7 +34,6 @@
 import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_NOTIFICATION_DOT_ENABLED
 import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_THEMED_ICON_DISABLED
 import com.android.launcher3.states.RotationHelper.ALLOW_ROTATION_PREFERENCE_KEY
-import com.android.launcher3.util.DaggerSingletonTracker
 import com.google.common.truth.Truth.assertThat
 import org.junit.After
 import org.junit.Before
@@ -63,7 +62,6 @@
     @Mock private lateinit var mMockLogger: StatsLogManager.StatsLogger
 
     @Captor private lateinit var mEventCaptor: ArgumentCaptor<StatsLogManager.EventEnum>
-    @Mock private lateinit var mTracker: DaggerSingletonTracker
 
     private var mDefaultThemedIcons = false
     private var mDefaultAllowRotation = false
@@ -81,7 +79,7 @@
         // To match the default value of ALLOW_ROTATION
         LauncherPrefs.get(mContext).put(item = ALLOW_ROTATION, value = false)
 
-        mSystemUnderTest = SettingsChangeLogger(mContext, mStatsLogManager, mTracker)
+        mSystemUnderTest = SettingsChangeLogger(mContext, mStatsLogManager)
     }
 
     @After
@@ -92,7 +90,7 @@
 
     @Test
     fun loggingPrefs_correctDefaultValue() {
-        val systemUnderTest = SettingsChangeLogger(mContext, mStatsLogManager, mTracker)
+        val systemUnderTest = SettingsChangeLogger(mContext, mStatsLogManager)
 
         assertThat(systemUnderTest.loggingPrefs[ALLOW_ROTATION_PREFERENCE_KEY]!!.defaultValue)
             .isFalse()
@@ -119,7 +117,7 @@
         LauncherPrefs.get(mContext).put(item = ALLOW_ROTATION, value = true)
 
         // This a new object so the values of mLoggablePrefs will be different
-        SettingsChangeLogger(mContext, mStatsLogManager, mTracker).logSnapshot(mInstanceId)
+        SettingsChangeLogger(mContext, mStatsLogManager).logSnapshot(mInstanceId)
 
         verify(mMockLogger, atLeastOnce()).log(mEventCaptor.capture())
         val capturedEvents = mEventCaptor.allValues
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 7a17872..d6688d6 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
@@ -16,6 +16,7 @@
 
 package com.android.quickstep.recents.data
 
+import android.graphics.drawable.Drawable
 import com.android.systemui.shared.recents.model.Task
 import com.android.systemui.shared.recents.model.ThumbnailData
 import kotlinx.coroutines.flow.Flow
@@ -25,9 +26,9 @@
 
 class FakeTasksRepository : RecentTasksRepository {
     private var thumbnailDataMap: Map<Int, ThumbnailData> = emptyMap()
-    private var taskIconDataMap: Map<Int, TaskIconQueryResponse> = emptyMap()
+    private var taskIconDataMap: Map<Int, FakeIconData> = emptyMap()
     private var tasks: MutableStateFlow<List<Task>> = MutableStateFlow(emptyList())
-    private var visibleTasks: MutableStateFlow<List<Int>> = MutableStateFlow(emptyList())
+    private var visibleTasks: MutableStateFlow<Set<Int>> = MutableStateFlow(emptySet())
 
     override fun getAllTaskData(forceRefresh: Boolean): Flow<List<Task>> = tasks
 
@@ -48,16 +49,16 @@
     override fun getThumbnailById(taskId: Int): Flow<ThumbnailData?> =
         getTaskDataById(taskId).map { it?.thumbnail }
 
-    override fun setVisibleTasks(visibleTaskIdList: List<Int>) {
+    override fun setVisibleTasks(visibleTaskIdList: Set<Int>) {
         visibleTasks.value = visibleTaskIdList
         tasks.value =
             tasks.value.map {
                 it.apply {
                     thumbnail = thumbnailDataMap[it.key.id]
-                    taskIconDataMap[it.key.id].let { taskIconData ->
-                        icon = taskIconData?.icon
-                        titleDescription = taskIconData?.contentDescription
-                        title = taskIconData?.title
+                    taskIconDataMap[it.key.id].let { data ->
+                        title = data?.title
+                        titleDescription = data?.titleDescription
+                        icon = data?.icon
                     }
                 }
             }
@@ -71,7 +72,14 @@
         this.thumbnailDataMap = thumbnailDataMap
     }
 
-    fun seedIconData(iconDataMap: Map<Int, TaskIconQueryResponse>) {
-        this.taskIconDataMap = iconDataMap
+    fun seedIconData(id: Int, title: String, contentDescription: String, icon: Drawable) {
+        val iconData = FakeIconData(icon, contentDescription, title)
+        this.taskIconDataMap = mapOf(id to iconData)
     }
+
+    private data class FakeIconData(
+        val icon: Drawable,
+        val titleDescription: String,
+        val title: String,
+    )
 }
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 f31467f..357df6e 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
@@ -20,8 +20,8 @@
 import android.content.Intent
 import android.graphics.Bitmap
 import android.graphics.drawable.Drawable
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.android.launcher3.util.TestDispatcherProvider
-import com.android.quickstep.task.thumbnail.TaskThumbnailViewModelTest
 import com.android.quickstep.util.DesktopTask
 import com.android.quickstep.util.GroupTask
 import com.android.systemui.shared.recents.model.Task
@@ -36,17 +36,19 @@
 import kotlinx.coroutines.test.UnconfinedTestDispatcher
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
+import org.junit.runner.RunWith
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.whenever
 
 @OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidJUnit4::class)
 class TasksRepositoryTest {
     private val tasks = (0..5).map(::createTaskWithId)
     private val defaultTaskList =
         listOf(
             GroupTask(tasks[0]),
             GroupTask(tasks[1], tasks[2], null),
-            DesktopTask(tasks.subList(3, 6))
+            DesktopTask(tasks.subList(3, 6)),
         )
     private val recentsModel = FakeRecentTasksDataSource()
     private val taskThumbnailDataSource = FakeTaskThumbnailDataSource()
@@ -65,14 +67,13 @@
             taskIconDataSource,
             taskVisualsChangedDelegate,
             testScope.backgroundScope,
-            TestDispatcherProvider(dispatcher)
+            TestDispatcherProvider(dispatcher),
         )
 
     @Test
     fun getAllTaskDataReturnsFlattenedListOfTasks() =
         testScope.runTest {
             recentsModel.seedTasks(defaultTaskList)
-
             assertThat(systemUnderTest.getAllTaskData(forceRefresh = true).first()).isEqualTo(tasks)
         }
 
@@ -93,7 +94,7 @@
             val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2]
             systemUnderTest.getAllTaskData(forceRefresh = true)
 
-            systemUnderTest.setVisibleTasks(listOf(1, 2))
+            systemUnderTest.setVisibleTasks(setOf(1, 2))
 
             assertThat(systemUnderTest.getTaskDataById(1).first()!!.thumbnail!!.thumbnail)
                 .isEqualTo(bitmap1)
@@ -107,7 +108,7 @@
             recentsModel.seedTasks(defaultTaskList)
             systemUnderTest.getAllTaskData(forceRefresh = true)
 
-            systemUnderTest.setVisibleTasks(listOf(1, 2))
+            systemUnderTest.setVisibleTasks(setOf(1, 2))
 
             systemUnderTest
                 .getTaskDataById(1)
@@ -126,14 +127,14 @@
             val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2]
             systemUnderTest.getAllTaskData(forceRefresh = true)
 
-            systemUnderTest.setVisibleTasks(listOf(1, 2))
+            systemUnderTest.setVisibleTasks(setOf(1, 2))
 
             assertThat(systemUnderTest.getTaskDataById(2).first()!!.thumbnail!!.thumbnail)
                 .isEqualTo(bitmap2)
 
             // Prevent new loading of Bitmaps
             taskThumbnailDataSource.shouldLoadSynchronously = false
-            systemUnderTest.setVisibleTasks(listOf(2, 3))
+            systemUnderTest.setVisibleTasks(setOf(2, 3))
 
             assertThat(systemUnderTest.getTaskDataById(2).first()!!.thumbnail!!.thumbnail)
                 .isEqualTo(bitmap2)
@@ -145,7 +146,7 @@
             recentsModel.seedTasks(defaultTaskList)
             systemUnderTest.getAllTaskData(forceRefresh = true)
 
-            systemUnderTest.setVisibleTasks(listOf(1, 2))
+            systemUnderTest.setVisibleTasks(setOf(1, 2))
 
             systemUnderTest
                 .getTaskDataById(2)
@@ -154,7 +155,7 @@
 
             // Prevent new loading of Drawables
             taskThumbnailDataSource.shouldLoadSynchronously = false
-            systemUnderTest.setVisibleTasks(listOf(2, 3))
+            systemUnderTest.setVisibleTasks(setOf(2, 3))
 
             systemUnderTest
                 .getTaskDataById(2)
@@ -169,7 +170,7 @@
             val bitmap2 = taskThumbnailDataSource.taskIdToBitmap[2]
             systemUnderTest.getAllTaskData(forceRefresh = true)
 
-            systemUnderTest.setVisibleTasks(listOf(1, 2))
+            systemUnderTest.setVisibleTasks(setOf(1, 2))
 
             val task2 = systemUnderTest.getTaskDataById(2).first()!!
             assertThat(task2.thumbnail!!.thumbnail).isEqualTo(bitmap2)
@@ -178,7 +179,7 @@
             // Prevent new loading of Bitmaps
             taskThumbnailDataSource.shouldLoadSynchronously = false
             taskIconDataSource.shouldLoadSynchronously = false
-            systemUnderTest.setVisibleTasks(listOf(0, 1))
+            systemUnderTest.setVisibleTasks(setOf(0, 1))
 
             val task2AfterVisibleTasksChanged = systemUnderTest.getTaskDataById(2).first()!!
             assertThat(task2AfterVisibleTasksChanged.thumbnail).isNull()
@@ -197,7 +198,7 @@
 
             // Setup TasksRepository
             systemUnderTest.getAllTaskData(forceRefresh = true)
-            systemUnderTest.setVisibleTasks(listOf(1, 2))
+            systemUnderTest.setVisibleTasks(setOf(1, 2))
 
             // Assert there is no bitmap in first emission
             assertThat(systemUnderTest.getTaskDataById(2).first()!!.thumbnail).isNull()
@@ -215,8 +216,7 @@
         testScope.runTest {
             recentsModel.seedTasks(defaultTaskList)
             systemUnderTest.getAllTaskData(forceRefresh = true)
-
-            systemUnderTest.setVisibleTasks(listOf(1))
+            systemUnderTest.setVisibleTasks(setOf(1))
 
             val expectedThumbnailData = createThumbnailData()
             val expectedPreviousBitmap = taskThumbnailDataSource.taskIdToBitmap[1]
@@ -228,7 +228,7 @@
             }
             taskVisualsChangedDelegate.onTaskThumbnailChanged(1, expectedThumbnailData)
 
-            assertThat(task1ThumbnailValues[1]!!.thumbnail).isEqualTo(expectedPreviousBitmap)
+            assertThat(task1ThumbnailValues.first()!!.thumbnail).isEqualTo(expectedPreviousBitmap)
             assertThat(task1ThumbnailValues.last()).isEqualTo(expectedThumbnailData)
         }
 
@@ -238,7 +238,7 @@
             recentsModel.seedTasks(defaultTaskList)
             systemUnderTest.getAllTaskData(forceRefresh = true)
 
-            systemUnderTest.setVisibleTasks(listOf(1))
+            systemUnderTest.setVisibleTasks(setOf(1))
 
             val expectedBitmap = mock<Bitmap>()
             val expectedPreviousBitmap = taskThumbnailDataSource.taskIdToBitmap[1]
@@ -248,10 +248,11 @@
             testScope.backgroundScope.launch {
                 taskDataFlow.map { it?.thumbnail?.thumbnail }.toList(task1ThumbnailValues)
             }
+
             taskThumbnailDataSource.taskIdToBitmap[1] = expectedBitmap
             taskVisualsChangedDelegate.onHighResLoadingStateChanged(true)
 
-            assertThat(task1ThumbnailValues[1]).isEqualTo(expectedPreviousBitmap)
+            assertThat(task1ThumbnailValues.first()).isEqualTo(expectedPreviousBitmap)
             assertThat(task1ThumbnailValues.last()).isEqualTo(expectedBitmap)
         }
 
@@ -261,7 +262,7 @@
             recentsModel.seedTasks(defaultTaskList)
             systemUnderTest.getAllTaskData(forceRefresh = true)
 
-            systemUnderTest.setVisibleTasks(listOf(1))
+            systemUnderTest.setVisibleTasks(setOf(1))
 
             val expectedIcon = FakeTaskIconDataSource.mockCopyableDrawable()
             val expectedPreviousIcon = taskIconDataSource.taskIdToDrawable[1]
@@ -274,18 +275,47 @@
             taskIconDataSource.taskIdToDrawable[1] = expectedIcon
             taskVisualsChangedDelegate.onTaskIconChanged(1)
 
-            assertThat(task1IconValues[1]).isEqualTo(expectedPreviousIcon)
+            assertThat(task1IconValues.first()).isEqualTo(expectedPreviousIcon)
             assertThat(task1IconValues.last()).isEqualTo(expectedIcon)
         }
 
+    @Test
+    fun setVisibleTasks_multipleTimesWithDifferentTasks_reusesThumbnailRequests() =
+        testScope.runTest {
+            recentsModel.seedTasks(defaultTaskList)
+            systemUnderTest.getAllTaskData(forceRefresh = true)
+            taskThumbnailDataSource.shouldLoadSynchronously = false
+
+            val taskDataFlow = systemUnderTest.getTaskDataById(1)
+            val task1IconValues = mutableListOf<Drawable?>()
+            testScope.backgroundScope.launch {
+                taskDataFlow.map { it?.icon }.toList(task1IconValues)
+            }
+
+            systemUnderTest.setVisibleTasks(setOf(1))
+            val task1UpdatingTaskOld = taskThumbnailDataSource.taskIdToUpdatingTask[1]
+            println(task1UpdatingTaskOld)
+
+            systemUnderTest.setVisibleTasks(setOf(1, 2))
+            val task1UpdatingTaskNew = taskThumbnailDataSource.taskIdToUpdatingTask[1]
+            println(task1UpdatingTaskNew)
+
+            assertThat(task1UpdatingTaskNew).isEqualTo(task1UpdatingTaskOld)
+        }
+
     private fun createTaskWithId(taskId: Int) =
         Task(Task.TaskKey(taskId, 0, Intent(), ComponentName("", ""), 0, 2000))
 
     private fun createThumbnailData(): ThumbnailData {
         val bitmap = mock<Bitmap>()
-        whenever(bitmap.width).thenReturn(TaskThumbnailViewModelTest.THUMBNAIL_WIDTH)
-        whenever(bitmap.height).thenReturn(TaskThumbnailViewModelTest.THUMBNAIL_HEIGHT)
+        whenever(bitmap.width).thenReturn(THUMBNAIL_WIDTH)
+        whenever(bitmap.height).thenReturn(THUMBNAIL_HEIGHT)
 
         return ThumbnailData(thumbnail = bitmap)
     }
+
+    companion object {
+        const val THUMBNAIL_WIDTH = 100
+        const val THUMBNAIL_HEIGHT = 200
+    }
 }
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailPositionUseCaseTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailPositionUseCaseTest.kt
index 02f1d11..bd7d970 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailPositionUseCaseTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailPositionUseCaseTest.kt
@@ -66,7 +66,7 @@
             deviceProfileRepository,
             rotationStateRepository,
             tasksRepository,
-            previewPositionHelper
+            previewPositionHelper,
         )
 
     @Test
@@ -80,7 +80,7 @@
     @Test
     fun visibleTaskWithoutThumbnailData_returnsIdentityMatrix() = runTest {
         tasksRepository.seedTasks(listOf(task))
-        tasksRepository.setVisibleTasks(listOf(TASK_ID))
+        tasksRepository.setVisibleTasks(setOf(TASK_ID))
 
         assertThat(systemUnderTest.run(TASK_ID, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl = true))
             .isInstanceOf(MissingThumbnail::class.java)
@@ -90,7 +90,7 @@
     fun visibleTaskWithThumbnailData_returnsTransformedMatrix() = runTest {
         tasksRepository.seedThumbnailData(mapOf(TASK_ID to thumbnailData))
         tasksRepository.seedTasks(listOf(task))
-        tasksRepository.setVisibleTasks(listOf(TASK_ID))
+        tasksRepository.setVisibleTasks(setOf(TASK_ID))
 
         val isLargeScreen = true
         deviceProfileRepository.setRecentsDeviceProfile(
@@ -119,7 +119,7 @@
                 CANVAS_HEIGHT,
                 isLargeScreen,
                 activityRotation,
-                isRtl
+                isRtl,
             )
     }
 
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailUseCaseTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailUseCaseTest.kt
index 12a94cf..73aa460 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailUseCaseTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailUseCaseTest.kt
@@ -71,7 +71,7 @@
     fun taskVisible_returnsThumbnail() {
         tasksRepository.seedTasks(listOf(task))
         tasksRepository.seedThumbnailData(mapOf(TaskOverlayViewModelTest.TASK_ID to thumbnailData))
-        tasksRepository.setVisibleTasks(listOf(TaskOverlayViewModelTest.TASK_ID))
+        tasksRepository.setVisibleTasks(setOf(TaskOverlayViewModelTest.TASK_ID))
 
         assertThat(systemUnderTest.run(TASK_ID)).isEqualTo(thumbnailData.thumbnail)
     }
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/SysUiStatusNavFlagsUseCaseTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/SysUiStatusNavFlagsUseCaseTest.kt
index ba4e206..92f2efd 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/SysUiStatusNavFlagsUseCaseTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/SysUiStatusNavFlagsUseCaseTest.kt
@@ -71,7 +71,7 @@
                         whenever(width).thenReturn(THUMBNAIL_WIDTH)
                         whenever(height).thenReturn(THUMBNAIL_HEIGHT)
                     },
-                appearance = APPEARANCE_LIGHT_THEME
+                appearance = APPEARANCE_LIGHT_THEME,
             )
 
         val secondTask =
@@ -85,14 +85,14 @@
                         whenever(width).thenReturn(THUMBNAIL_WIDTH)
                         whenever(height).thenReturn(THUMBNAIL_HEIGHT)
                     },
-                appearance = APPEARANCE_DARK_THEME
+                appearance = APPEARANCE_DARK_THEME,
             )
 
         tasksRepository.seedTasks(listOf(firstTask, secondTask))
         tasksRepository.seedThumbnailData(
             mapOf(FIRST_TASK_ID to firstThumbnailData, SECOND_TASK_ID to secondThumbnailData)
         )
-        tasksRepository.setVisibleTasks(listOf(FIRST_TASK_ID, SECOND_TASK_ID))
+        tasksRepository.setVisibleTasks(setOf(FIRST_TASK_ID, SECOND_TASK_ID))
     }
 
     companion object {
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/viewmodel/RecentsViewModelTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/viewmodel/RecentsViewModelTest.kt
index 33d96a8..a32e07d 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/viewmodel/RecentsViewModelTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/viewmodel/RecentsViewModelTest.kt
@@ -22,7 +22,6 @@
 import android.graphics.Color
 import android.view.Surface
 import com.android.quickstep.recents.data.FakeTasksRepository
-import com.android.quickstep.task.thumbnail.TaskThumbnailViewModelTest
 import com.android.systemui.shared.recents.model.Task
 import com.android.systemui.shared.recents.model.ThumbnailData
 import com.google.common.truth.Truth.assertThat
@@ -98,7 +97,7 @@
         )
         // setVisibleTasks forces FakeTasksRepository to update the flows returned by
         // getThumbnailById
-        tasksRepository.setVisibleTasks(listOf(1, 2))
+        tasksRepository.setVisibleTasks(setOf(1, 2))
 
         // Then wait for thumbnailData should complete, and the previous getThumbnailById flow
         // should return updated values
@@ -126,9 +125,14 @@
 
     private fun createThumbnailData(rotation: Int = Surface.ROTATION_0): ThumbnailData {
         val bitmap = mock<Bitmap>()
-        whenever(bitmap.width).thenReturn(TaskThumbnailViewModelTest.THUMBNAIL_WIDTH)
-        whenever(bitmap.height).thenReturn(TaskThumbnailViewModelTest.THUMBNAIL_HEIGHT)
+        whenever(bitmap.width).thenReturn(THUMBNAIL_WIDTH)
+        whenever(bitmap.height).thenReturn(THUMBNAIL_HEIGHT)
 
         return ThumbnailData(thumbnail = bitmap, rotation = rotation)
     }
+
+    companion object {
+        const val THUMBNAIL_WIDTH = 100
+        const val THUMBNAIL_HEIGHT = 200
+    }
 }
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/SplashAlphaUseCaseTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/SplashAlphaUseCaseTest.kt
index a584d71..0767fb9 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/SplashAlphaUseCaseTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/SplashAlphaUseCaseTest.kt
@@ -25,7 +25,6 @@
 import android.view.Surface.ROTATION_90
 import com.android.quickstep.recents.data.FakeRecentsRotationStateRepository
 import com.android.quickstep.recents.data.FakeTasksRepository
-import com.android.quickstep.recents.data.TaskIconQueryResponse
 import com.android.quickstep.recents.viewmodel.RecentsViewData
 import com.android.quickstep.task.viewmodel.TaskContainerData
 import com.android.systemui.shared.recents.model.Task
@@ -49,7 +48,7 @@
             taskContainerData,
             taskThumbnailViewData,
             recentTasksRepository,
-            recentsRotationStateRepository
+            recentsRotationStateRepository,
         )
 
     @Test
@@ -117,16 +116,16 @@
 
     private fun setupTask(taskId: Int, thumbnailData: ThumbnailData = createThumbnailData()) {
         recentTasksRepository.seedThumbnailData(mapOf(taskId to thumbnailData))
-        val expectedIconData = createIconData("Task $taskId")
-        recentTasksRepository.seedIconData(mapOf(taskId to expectedIconData))
+        val expectedIconData = mock<Drawable>()
+        recentTasksRepository.seedIconData(taskId, "Task $taskId", "", expectedIconData)
         recentTasksRepository.seedTasks(tasks)
-        recentTasksRepository.setVisibleTasks(listOf(taskId))
+        recentTasksRepository.setVisibleTasks(setOf(taskId))
     }
 
     private fun createThumbnailData(
         rotation: Int = Surface.ROTATION_0,
         width: Int = THUMBNAIL_WIDTH,
-        height: Int = THUMBNAIL_HEIGHT
+        height: Int = THUMBNAIL_HEIGHT,
     ): ThumbnailData {
         val bitmap = mock<Bitmap>()
         whenever(bitmap.width).thenReturn(width)
@@ -135,8 +134,6 @@
         return ThumbnailData(thumbnail = bitmap, rotation = rotation)
     }
 
-    private fun createIconData(title: String) = TaskIconQueryResponse(mock<Drawable>(), "", title)
-
     private fun createTaskWithId(taskId: Int) =
         Task(Task.TaskKey(taskId, 0, Intent(), ComponentName("", ""), 0, 2000)).apply {
             colorBackground = Color.argb(taskId, taskId, taskId, taskId)
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelImplTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelImplTest.kt
new file mode 100644
index 0000000..e3a6adf
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelImplTest.kt
@@ -0,0 +1,333 @@
+/*
+ * 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.ComponentName
+import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.Color
+import android.graphics.Matrix
+import android.graphics.drawable.Drawable
+import android.view.Surface
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.launcher3.util.TestDispatcherProvider
+import com.android.quickstep.recents.data.FakeTasksRepository
+import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
+import com.android.quickstep.recents.usecase.ThumbnailPositionState.MatrixScaling
+import com.android.quickstep.recents.usecase.ThumbnailPositionState.MissingThumbnail
+import com.android.quickstep.recents.viewmodel.RecentsViewData
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.BackgroundOnly
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.LiveTile
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.SnapshotSplash
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
+import com.android.quickstep.task.viewmodel.TaskContainerData
+import com.android.quickstep.task.viewmodel.TaskThumbnailViewModelImpl
+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
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+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 [TaskThumbnailView] */
+@RunWith(AndroidJUnit4::class)
+class TaskThumbnailViewModelImplTest {
+    private val dispatcher = StandardTestDispatcher()
+    private val testScope = TestScope(dispatcher)
+
+    private var taskViewType = TaskViewType.SINGLE
+    private val recentsViewData = RecentsViewData()
+    private val taskViewData by lazy { TaskViewData(taskViewType) }
+    private val taskContainerData = TaskContainerData()
+    private val dispatcherProvider = TestDispatcherProvider(dispatcher)
+    private val tasksRepository = FakeTasksRepository()
+    private val mGetThumbnailPositionUseCase = mock<GetThumbnailPositionUseCase>()
+    private val splashAlphaUseCase: SplashAlphaUseCase = mock()
+
+    private val systemUnderTest by lazy {
+        TaskThumbnailViewModelImpl(
+            recentsViewData,
+            taskViewData,
+            taskContainerData,
+            dispatcherProvider,
+            tasksRepository,
+            mGetThumbnailPositionUseCase,
+            splashAlphaUseCase,
+        )
+    }
+
+    private val tasks = (0..5).map(::createTaskWithId)
+
+    @Test
+    fun initialStateIsUninitialized() =
+        testScope.runTest { assertThat(systemUnderTest.uiState.first()).isEqualTo(Uninitialized) }
+
+    @Test
+    fun bindRunningTask_thenStateIs_LiveTile() =
+        testScope.runTest {
+            val taskId = 1
+            tasksRepository.seedTasks(tasks)
+            tasksRepository.setVisibleTasks(setOf(taskId))
+            recentsViewData.runningTaskIds.value = setOf(taskId)
+            systemUnderTest.bind(taskId)
+
+            assertThat(systemUnderTest.uiState.first()).isEqualTo(LiveTile)
+        }
+
+    @Test
+    fun bindRunningTaskShouldShowScreenshot_thenStateIs_SnapshotSplash() =
+        testScope.runTest {
+            val taskId = 1
+            val expectedThumbnailData = createThumbnailData()
+            tasksRepository.seedThumbnailData(mapOf(taskId to expectedThumbnailData))
+            val expectedIconData = mock<Drawable>()
+            tasksRepository.seedIconData(taskId, "Task $taskId", "", expectedIconData)
+            tasksRepository.seedTasks(tasks)
+            tasksRepository.setVisibleTasks(setOf(taskId))
+            recentsViewData.runningTaskIds.value = setOf(taskId)
+            recentsViewData.runningTaskShowScreenshot.value = true
+            systemUnderTest.bind(taskId)
+
+            assertThat(systemUnderTest.uiState.first())
+                .isEqualTo(
+                    SnapshotSplash(
+                        Snapshot(
+                            backgroundColor = Color.rgb(1, 1, 1),
+                            bitmap = expectedThumbnailData.thumbnail!!,
+                            thumbnailRotation = Surface.ROTATION_0,
+                        ),
+                        expectedIconData,
+                    )
+                )
+        }
+
+    @Test
+    fun setRecentsFullscreenProgress_thenCornerRadiusProgressIsPassedThrough() =
+        testScope.runTest {
+            recentsViewData.fullscreenProgress.value = 0.5f
+
+            assertThat(systemUnderTest.cornerRadiusProgress.first()).isEqualTo(0.5f)
+
+            recentsViewData.fullscreenProgress.value = 0.6f
+
+            assertThat(systemUnderTest.cornerRadiusProgress.first()).isEqualTo(0.6f)
+        }
+
+    @Test
+    fun setRecentsFullscreenProgress_thenCornerRadiusProgressIsConstantForDesktop() =
+        testScope.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
+    fun setAncestorScales_thenScaleIsCalculated() =
+        testScope.runTest {
+            recentsViewData.scale.value = 0.5f
+            taskViewData.scale.value = 0.6f
+
+            assertThat(systemUnderTest.inheritedScale.first()).isEqualTo(0.3f)
+        }
+
+    @Test
+    fun bindRunningTaskThenStoppedTaskWithoutThumbnail_thenStateChangesToBackgroundOnly() =
+        testScope.runTest {
+            val runningTaskId = 1
+            val stoppedTaskId = 2
+            tasksRepository.seedTasks(tasks)
+            tasksRepository.setVisibleTasks(setOf(runningTaskId, stoppedTaskId))
+            recentsViewData.runningTaskIds.value = setOf(runningTaskId)
+            systemUnderTest.bind(runningTaskId)
+            assertThat(systemUnderTest.uiState.first()).isEqualTo(LiveTile)
+
+            systemUnderTest.bind(stoppedTaskId)
+            assertThat(systemUnderTest.uiState.first())
+                .isEqualTo(BackgroundOnly(backgroundColor = Color.rgb(2, 2, 2)))
+        }
+
+    @Test
+    fun bindStoppedTaskWithoutThumbnail_thenStateIs_BackgroundOnly_withAlphaRemoved() =
+        testScope.runTest {
+            val stoppedTaskId = 2
+            tasksRepository.seedTasks(tasks)
+            tasksRepository.setVisibleTasks(setOf(stoppedTaskId))
+
+            systemUnderTest.bind(stoppedTaskId)
+            assertThat(systemUnderTest.uiState.first())
+                .isEqualTo(BackgroundOnly(backgroundColor = Color.rgb(2, 2, 2)))
+        }
+
+    @Test
+    fun bindLockedTaskWithThumbnail_thenStateIs_BackgroundOnly() =
+        testScope.runTest {
+            val taskId = 2
+            tasksRepository.seedThumbnailData(mapOf(taskId to createThumbnailData()))
+            tasks[taskId].isLocked = true
+            tasksRepository.seedTasks(tasks)
+            tasksRepository.setVisibleTasks(setOf(taskId))
+
+            systemUnderTest.bind(taskId)
+            assertThat(systemUnderTest.uiState.first())
+                .isEqualTo(BackgroundOnly(backgroundColor = Color.rgb(2, 2, 2)))
+        }
+
+    @Test
+    fun bindStoppedTaskWithThumbnail_thenStateIs_SnapshotSplash_withAlphaRemoved() =
+        testScope.runTest {
+            val taskId = 2
+            val expectedThumbnailData = createThumbnailData(rotation = Surface.ROTATION_270)
+            tasksRepository.seedThumbnailData(mapOf(taskId to expectedThumbnailData))
+            val expectedIconData = mock<Drawable>()
+            tasksRepository.seedIconData(taskId, "Task $taskId", "", expectedIconData)
+            tasksRepository.seedTasks(tasks)
+            tasksRepository.setVisibleTasks(setOf(taskId))
+
+            systemUnderTest.bind(taskId)
+            assertThat(systemUnderTest.uiState.first())
+                .isEqualTo(
+                    SnapshotSplash(
+                        Snapshot(
+                            backgroundColor = Color.rgb(2, 2, 2),
+                            bitmap = expectedThumbnailData.thumbnail!!,
+                            thumbnailRotation = Surface.ROTATION_270,
+                        ),
+                        expectedIconData,
+                    )
+                )
+        }
+
+    @Test
+    fun bindNonVisibleStoppedTask_whenMadeVisible_thenStateIsSnapshotSplash() =
+        testScope.runTest {
+            val taskId = 2
+            val expectedThumbnailData = createThumbnailData()
+            tasksRepository.seedThumbnailData(mapOf(taskId to expectedThumbnailData))
+            val expectedIconData = mock<Drawable>()
+            tasksRepository.seedIconData(taskId, "Task $taskId", "", expectedIconData)
+            tasksRepository.seedTasks(tasks)
+
+            systemUnderTest.bind(taskId)
+            assertThat(systemUnderTest.uiState.first()).isEqualTo(Uninitialized)
+
+            tasksRepository.setVisibleTasks(setOf(taskId))
+            assertThat(systemUnderTest.uiState.first())
+                .isEqualTo(
+                    SnapshotSplash(
+                        Snapshot(
+                            backgroundColor = Color.rgb(2, 2, 2),
+                            bitmap = expectedThumbnailData.thumbnail!!,
+                            thumbnailRotation = Surface.ROTATION_0,
+                        ),
+                        expectedIconData,
+                    )
+                )
+        }
+
+    @Test
+    fun getSnapshotMatrix_MissingThumbnail() =
+        testScope.runTest {
+            val taskId = 2
+            val isRtl = true
+
+            whenever(mGetThumbnailPositionUseCase.run(taskId, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
+                .thenReturn(MissingThumbnail)
+
+            systemUnderTest.bind(taskId)
+            assertThat(
+                    systemUnderTest.getThumbnailPositionState(CANVAS_WIDTH, CANVAS_HEIGHT, isRtl)
+                )
+                .isEqualTo(Matrix.IDENTITY_MATRIX)
+        }
+
+    @Test
+    fun getSnapshotMatrix_MatrixScaling() =
+        testScope.runTest {
+            val taskId = 2
+            val isRtl = true
+
+            whenever(mGetThumbnailPositionUseCase.run(taskId, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
+                .thenReturn(MatrixScaling(MATRIX, isRotated = false))
+
+            systemUnderTest.bind(taskId)
+            assertThat(
+                    systemUnderTest.getThumbnailPositionState(CANVAS_WIDTH, CANVAS_HEIGHT, isRtl)
+                )
+                .isEqualTo(MATRIX)
+        }
+
+    @Test
+    fun getForegroundScrimDimProgress_returnsForegroundMaxScrim() =
+        testScope.runTest {
+            recentsViewData.tintAmount.value = 0.32f
+            taskContainerData.taskMenuOpenProgress.value = 0f
+            assertThat(systemUnderTest.dimProgress.first()).isEqualTo(0.32f)
+        }
+
+    @Test
+    fun getTaskMenuScrimDimProgress_returnsTaskMenuScrim() =
+        testScope.runTest {
+            recentsViewData.tintAmount.value = 0f
+            taskContainerData.taskMenuOpenProgress.value = 1f
+            assertThat(systemUnderTest.dimProgress.first()).isEqualTo(0.4f)
+        }
+
+    @Test
+    fun getForegroundScrimDimProgress_returnsNoScrim() =
+        testScope.runTest {
+            recentsViewData.tintAmount.value = 0f
+            taskContainerData.taskMenuOpenProgress.value = 0f
+            assertThat(systemUnderTest.dimProgress.first()).isEqualTo(0f)
+        }
+
+    private fun createTaskWithId(taskId: Int) =
+        Task(Task.TaskKey(taskId, 0, Intent(), ComponentName("", ""), 0, 2000)).apply {
+            colorBackground = Color.argb(taskId, taskId, taskId, taskId)
+        }
+
+    private fun createThumbnailData(rotation: Int = Surface.ROTATION_0): ThumbnailData {
+        val bitmap = mock<Bitmap>()
+        whenever(bitmap.width).thenReturn(THUMBNAIL_WIDTH)
+        whenever(bitmap.height).thenReturn(THUMBNAIL_HEIGHT)
+
+        return ThumbnailData(thumbnail = bitmap, rotation = rotation)
+    }
+
+    companion object {
+        const val THUMBNAIL_WIDTH = 100
+        const val THUMBNAIL_HEIGHT = 200
+        const val CANVAS_WIDTH = 300
+        const val CANVAS_HEIGHT = 600
+        val MATRIX =
+            Matrix().apply {
+                setValues(floatArrayOf(2.3f, 4.5f, 2.6f, 7.4f, 3.4f, 2.3f, 2.5f, 6.0f, 3.4f))
+            }
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelTest.kt
deleted file mode 100644
index fcf4e56..0000000
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelTest.kt
+++ /dev/null
@@ -1,310 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.quickstep.task.thumbnail
-
-import android.content.ComponentName
-import android.content.Intent
-import android.graphics.Bitmap
-import android.graphics.Color
-import android.graphics.Matrix
-import android.graphics.drawable.Drawable
-import android.view.Surface
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.android.quickstep.recents.data.FakeTasksRepository
-import com.android.quickstep.recents.data.TaskIconQueryResponse
-import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
-import com.android.quickstep.recents.usecase.ThumbnailPositionState.MatrixScaling
-import com.android.quickstep.recents.usecase.ThumbnailPositionState.MissingThumbnail
-import com.android.quickstep.recents.viewmodel.RecentsViewData
-import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.BackgroundOnly
-import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.LiveTile
-import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot
-import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.SnapshotSplash
-import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
-import com.android.quickstep.task.viewmodel.TaskContainerData
-import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel
-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
-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 [TaskThumbnailView] */
-@RunWith(AndroidJUnit4::class)
-class TaskThumbnailViewModelTest {
-    private var taskViewType = TaskViewType.SINGLE
-    private val recentsViewData = RecentsViewData()
-    private val taskViewData by lazy { TaskViewData(taskViewType) }
-    private val taskContainerData = TaskContainerData()
-    private val tasksRepository = FakeTasksRepository()
-    private val mGetThumbnailPositionUseCase = mock<GetThumbnailPositionUseCase>()
-    private val splashAlphaUseCase: SplashAlphaUseCase = mock()
-    private val systemUnderTest by lazy {
-        TaskThumbnailViewModel(
-            recentsViewData,
-            taskViewData,
-            taskContainerData,
-            tasksRepository,
-            mGetThumbnailPositionUseCase,
-            splashAlphaUseCase,
-        )
-    }
-
-    private val tasks = (0..5).map(::createTaskWithId)
-
-    @Test
-    fun initialStateIsUninitialized() = runTest {
-        assertThat(systemUnderTest.uiState.first()).isEqualTo(Uninitialized)
-    }
-
-    @Test
-    fun bindRunningTask_thenStateIs_LiveTile() = runTest {
-        val taskId = 1
-        tasksRepository.seedTasks(tasks)
-        tasksRepository.setVisibleTasks(listOf(taskId))
-        recentsViewData.runningTaskIds.value = setOf(taskId)
-        systemUnderTest.bind(taskId)
-
-        assertThat(systemUnderTest.uiState.first()).isEqualTo(LiveTile)
-    }
-
-    @Test
-    fun bindRunningTaskShouldShowScreenshot_thenStateIs_SnapshotSplash() = runTest {
-        val taskId = 1
-        val expectedThumbnailData = createThumbnailData()
-        tasksRepository.seedThumbnailData(mapOf(taskId to expectedThumbnailData))
-        val expectedIconData = createIconData("Task 1")
-        tasksRepository.seedIconData(mapOf(taskId to expectedIconData))
-        tasksRepository.seedTasks(tasks)
-        tasksRepository.setVisibleTasks(listOf(taskId))
-        recentsViewData.runningTaskIds.value = setOf(taskId)
-        recentsViewData.runningTaskShowScreenshot.value = true
-        systemUnderTest.bind(taskId)
-
-        assertThat(systemUnderTest.uiState.first())
-            .isEqualTo(
-                SnapshotSplash(
-                    Snapshot(
-                        backgroundColor = Color.rgb(1, 1, 1),
-                        bitmap = expectedThumbnailData.thumbnail!!,
-                        thumbnailRotation = Surface.ROTATION_0,
-                    ),
-                    expectedIconData.icon
-                )
-            )
-    }
-
-    @Test
-    fun setRecentsFullscreenProgress_thenCornerRadiusProgressIsPassedThrough() = runTest {
-        recentsViewData.fullscreenProgress.value = 0.5f
-
-        assertThat(systemUnderTest.cornerRadiusProgress.first()).isEqualTo(0.5f)
-
-        recentsViewData.fullscreenProgress.value = 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
-    fun setAncestorScales_thenScaleIsCalculated() = runTest {
-        recentsViewData.scale.value = 0.5f
-        taskViewData.scale.value = 0.6f
-
-        assertThat(systemUnderTest.inheritedScale.first()).isEqualTo(0.3f)
-    }
-
-    @Test
-    fun bindRunningTaskThenStoppedTaskWithoutThumbnail_thenStateChangesToBackgroundOnly() =
-        runTest {
-            val runningTaskId = 1
-            val stoppedTaskId = 2
-            tasksRepository.seedTasks(tasks)
-            tasksRepository.setVisibleTasks(listOf(runningTaskId, stoppedTaskId))
-            recentsViewData.runningTaskIds.value = setOf(runningTaskId)
-            systemUnderTest.bind(runningTaskId)
-            assertThat(systemUnderTest.uiState.first()).isEqualTo(LiveTile)
-
-            systemUnderTest.bind(stoppedTaskId)
-            assertThat(systemUnderTest.uiState.first())
-                .isEqualTo(BackgroundOnly(backgroundColor = Color.rgb(2, 2, 2)))
-        }
-
-    @Test
-    fun bindStoppedTaskWithoutThumbnail_thenStateIs_BackgroundOnly_withAlphaRemoved() = runTest {
-        val stoppedTaskId = 2
-        tasksRepository.seedTasks(tasks)
-        tasksRepository.setVisibleTasks(listOf(stoppedTaskId))
-
-        systemUnderTest.bind(stoppedTaskId)
-        assertThat(systemUnderTest.uiState.first())
-            .isEqualTo(BackgroundOnly(backgroundColor = Color.rgb(2, 2, 2)))
-    }
-
-    @Test
-    fun bindLockedTaskWithThumbnail_thenStateIs_BackgroundOnly() = runTest {
-        val taskId = 2
-        tasksRepository.seedThumbnailData(mapOf(taskId to createThumbnailData()))
-        tasks[taskId].isLocked = true
-        tasksRepository.seedTasks(tasks)
-        tasksRepository.setVisibleTasks(listOf(taskId))
-
-        systemUnderTest.bind(taskId)
-        assertThat(systemUnderTest.uiState.first())
-            .isEqualTo(BackgroundOnly(backgroundColor = Color.rgb(2, 2, 2)))
-    }
-
-    @Test
-    fun bindStoppedTaskWithThumbnail_thenStateIs_SnapshotSplash_withAlphaRemoved() = runTest {
-        val taskId = 2
-        val expectedThumbnailData = createThumbnailData(rotation = Surface.ROTATION_270)
-        tasksRepository.seedThumbnailData(mapOf(taskId to expectedThumbnailData))
-        val expectedIconData = createIconData("Task 2")
-        tasksRepository.seedIconData(mapOf(taskId to expectedIconData))
-        tasksRepository.seedTasks(tasks)
-        tasksRepository.setVisibleTasks(listOf(taskId))
-
-        systemUnderTest.bind(taskId)
-        assertThat(systemUnderTest.uiState.first())
-            .isEqualTo(
-                SnapshotSplash(
-                    Snapshot(
-                        backgroundColor = Color.rgb(2, 2, 2),
-                        bitmap = expectedThumbnailData.thumbnail!!,
-                        thumbnailRotation = Surface.ROTATION_270,
-                    ),
-                    expectedIconData.icon
-                )
-            )
-    }
-
-    @Test
-    fun bindNonVisibleStoppedTask_whenMadeVisible_thenStateIsSnapshotSplash() = runTest {
-        val taskId = 2
-        val expectedThumbnailData = createThumbnailData()
-        tasksRepository.seedThumbnailData(mapOf(taskId to expectedThumbnailData))
-        val expectedIconData = createIconData("Task 2")
-        tasksRepository.seedIconData(mapOf(taskId to expectedIconData))
-        tasksRepository.seedTasks(tasks)
-
-        systemUnderTest.bind(taskId)
-        assertThat(systemUnderTest.uiState.first()).isEqualTo(Uninitialized)
-
-        tasksRepository.setVisibleTasks(listOf(taskId))
-        assertThat(systemUnderTest.uiState.first())
-            .isEqualTo(
-                SnapshotSplash(
-                    Snapshot(
-                        backgroundColor = Color.rgb(2, 2, 2),
-                        bitmap = expectedThumbnailData.thumbnail!!,
-                        thumbnailRotation = Surface.ROTATION_0,
-                    ),
-                    expectedIconData.icon
-                )
-            )
-    }
-
-    @Test
-    fun getSnapshotMatrix_MissingThumbnail() = runTest {
-        val taskId = 2
-        val isRtl = true
-
-        whenever(mGetThumbnailPositionUseCase.run(taskId, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
-            .thenReturn(MissingThumbnail)
-
-        systemUnderTest.bind(taskId)
-        assertThat(systemUnderTest.getThumbnailPositionState(CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
-            .isEqualTo(Matrix.IDENTITY_MATRIX)
-    }
-
-    @Test
-    fun getSnapshotMatrix_MatrixScaling() = runTest {
-        val taskId = 2
-        val isRtl = true
-
-        whenever(mGetThumbnailPositionUseCase.run(taskId, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
-            .thenReturn(MatrixScaling(MATRIX, isRotated = false))
-
-        systemUnderTest.bind(taskId)
-        assertThat(systemUnderTest.getThumbnailPositionState(CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
-            .isEqualTo(MATRIX)
-    }
-
-    @Test
-    fun getForegroundScrimDimProgress_returnsForegroundMaxScrim() = runTest {
-        recentsViewData.tintAmount.value = 0.32f
-        taskContainerData.taskMenuOpenProgress.value = 0f
-        assertThat(systemUnderTest.dimProgress.first()).isEqualTo(0.32f)
-    }
-
-    @Test
-    fun getTaskMenuScrimDimProgress_returnsTaskMenuScrim() = runTest {
-        recentsViewData.tintAmount.value = 0f
-        taskContainerData.taskMenuOpenProgress.value = 1f
-        assertThat(systemUnderTest.dimProgress.first()).isEqualTo(0.4f)
-    }
-
-    @Test
-    fun getForegroundScrimDimProgress_returnsNoScrim() = runTest {
-        recentsViewData.tintAmount.value = 0f
-        taskContainerData.taskMenuOpenProgress.value = 0f
-        assertThat(systemUnderTest.dimProgress.first()).isEqualTo(0f)
-    }
-
-    private fun createTaskWithId(taskId: Int) =
-        Task(Task.TaskKey(taskId, 0, Intent(), ComponentName("", ""), 0, 2000)).apply {
-            colorBackground = Color.argb(taskId, taskId, taskId, taskId)
-        }
-
-    private fun createThumbnailData(rotation: Int = Surface.ROTATION_0): ThumbnailData {
-        val bitmap = mock<Bitmap>()
-        whenever(bitmap.width).thenReturn(THUMBNAIL_WIDTH)
-        whenever(bitmap.height).thenReturn(THUMBNAIL_HEIGHT)
-
-        return ThumbnailData(thumbnail = bitmap, rotation = rotation)
-    }
-
-    private fun createIconData(title: String) = TaskIconQueryResponse(mock<Drawable>(), "", title)
-
-    companion object {
-        const val THUMBNAIL_WIDTH = 100
-        const val THUMBNAIL_HEIGHT = 200
-        const val CANVAS_WIDTH = 300
-        const val CANVAS_HEIGHT = 600
-        val MATRIX =
-            Matrix().apply {
-                setValues(floatArrayOf(2.3f, 4.5f, 2.6f, 7.4f, 3.4f, 2.3f, 2.5f, 6.0f, 3.4f))
-            }
-    }
-}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModelTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModelTest.kt
index d0887df..2e91f5c 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModelTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModelTest.kt
@@ -89,12 +89,7 @@
         task.isLocked = false
 
         assertThat(systemUnderTest.overlayState.first())
-            .isEqualTo(
-                Enabled(
-                    isRealSnapshot = false,
-                    thumbnail = null,
-                )
-            )
+            .isEqualTo(Enabled(isRealSnapshot = false, thumbnail = null))
     }
 
     @Test
@@ -103,17 +98,12 @@
         recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID)
         tasksRepository.seedTasks(listOf(task))
         tasksRepository.seedThumbnailData(mapOf(TASK_ID to thumbnailData))
-        tasksRepository.setVisibleTasks(listOf(TASK_ID))
+        tasksRepository.setVisibleTasks(setOf(TASK_ID))
         thumbnailData.isRealSnapshot = true
         task.isLocked = false
 
         assertThat(systemUnderTest.overlayState.first())
-            .isEqualTo(
-                Enabled(
-                    isRealSnapshot = true,
-                    thumbnail = thumbnailData.thumbnail,
-                )
-            )
+            .isEqualTo(Enabled(isRealSnapshot = true, thumbnail = thumbnailData.thumbnail))
     }
 
     @Test
@@ -122,17 +112,12 @@
         recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID)
         tasksRepository.seedTasks(listOf(task))
         tasksRepository.seedThumbnailData(mapOf(TASK_ID to thumbnailData))
-        tasksRepository.setVisibleTasks(listOf(TASK_ID))
+        tasksRepository.setVisibleTasks(setOf(TASK_ID))
         thumbnailData.isRealSnapshot = true
         task.isLocked = true
 
         assertThat(systemUnderTest.overlayState.first())
-            .isEqualTo(
-                Enabled(
-                    isRealSnapshot = false,
-                    thumbnail = thumbnailData.thumbnail,
-                )
-            )
+            .isEqualTo(Enabled(isRealSnapshot = false, thumbnail = thumbnailData.thumbnail))
     }
 
     @Test
@@ -141,17 +126,12 @@
         recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID)
         tasksRepository.seedTasks(listOf(task))
         tasksRepository.seedThumbnailData(mapOf(TASK_ID to thumbnailData))
-        tasksRepository.setVisibleTasks(listOf(TASK_ID))
+        tasksRepository.setVisibleTasks(setOf(TASK_ID))
         thumbnailData.isRealSnapshot = false
         task.isLocked = false
 
         assertThat(systemUnderTest.overlayState.first())
-            .isEqualTo(
-                Enabled(
-                    isRealSnapshot = false,
-                    thumbnail = thumbnailData.thumbnail,
-                )
-            )
+            .isEqualTo(Enabled(isRealSnapshot = false, thumbnail = thumbnailData.thumbnail))
     }
 
     @Test
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/AppPairsControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/AppPairsControllerTest.kt
index 99d3121..ee70e0a 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/AppPairsControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/AppPairsControllerTest.kt
@@ -28,9 +28,9 @@
 import com.android.quickstep.TopTaskTracker.CachedTaskInfo
 import com.android.systemui.shared.recents.model.Task
 import com.android.systemui.shared.recents.model.Task.TaskKey
-import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_30_70
-import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_50_50
-import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_70_30
+import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_33_66
+import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50
+import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_66_33
 import java.util.function.Consumer
 import org.junit.Assert.assertEquals
 import org.junit.Before
@@ -59,23 +59,23 @@
 
     private lateinit var appPairsController: AppPairsController
 
-    private val left30: Int by lazy {
-        appPairsController.encodeRank(STAGE_POSITION_TOP_OR_LEFT, SNAP_TO_30_70)
+    private val left33: Int by lazy {
+        appPairsController.encodeRank(STAGE_POSITION_TOP_OR_LEFT, SNAP_TO_2_33_66)
     }
     private val left50: Int by lazy {
-        appPairsController.encodeRank(STAGE_POSITION_TOP_OR_LEFT, SNAP_TO_50_50)
+        appPairsController.encodeRank(STAGE_POSITION_TOP_OR_LEFT, SNAP_TO_2_50_50)
     }
-    private val left70: Int by lazy {
-        appPairsController.encodeRank(STAGE_POSITION_TOP_OR_LEFT, SNAP_TO_70_30)
+    private val left66: Int by lazy {
+        appPairsController.encodeRank(STAGE_POSITION_TOP_OR_LEFT, SNAP_TO_2_66_33)
     }
-    private val right30: Int by lazy {
-        appPairsController.encodeRank(STAGE_POSITION_BOTTOM_OR_RIGHT, SNAP_TO_30_70)
+    private val right33: Int by lazy {
+        appPairsController.encodeRank(STAGE_POSITION_BOTTOM_OR_RIGHT, SNAP_TO_2_33_66)
     }
     private val right50: Int by lazy {
-        appPairsController.encodeRank(STAGE_POSITION_BOTTOM_OR_RIGHT, SNAP_TO_50_50)
+        appPairsController.encodeRank(STAGE_POSITION_BOTTOM_OR_RIGHT, SNAP_TO_2_50_50)
     }
-    private val right70: Int by lazy {
-        appPairsController.encodeRank(STAGE_POSITION_BOTTOM_OR_RIGHT, SNAP_TO_70_30)
+    private val right66: Int by lazy {
+        appPairsController.encodeRank(STAGE_POSITION_BOTTOM_OR_RIGHT, SNAP_TO_2_66_33)
     }
 
     @Mock lateinit var mockAppPairIcon: AppPairIcon
@@ -113,26 +113,26 @@
 
     @Test
     fun shouldEncodeRankCorrectly() {
-        assertEquals("left + 30-70 should encode as 0 (0b0)", 0, left30)
+        assertEquals("left + 33-66 should encode as 0 (0b0)", 0, left33)
         assertEquals("left + 50-50 should encode as 1 (0b1)", 1, left50)
-        assertEquals("left + 70-30 should encode as 2 (0b10)", 2, left70)
+        assertEquals("left + 66-33 should encode as 2 (0b10)", 2, left66)
         // See AppPairsController#BITMASK_SIZE and BITMASK_FOR_SNAP_POSITION for context
-        assertEquals("right + 30-70 should encode as 1 followed by 16 0s", 1 shl 16, right30)
+        assertEquals("right + 33-66 should encode as 1 followed by 16 0s", 1 shl 16, right33)
         assertEquals("right + 50-50 should encode as the above value + 1", (1 shl 16) + 1, right50)
-        assertEquals("right + 70-30 should encode as the above value + 2", (1 shl 16) + 2, right70)
+        assertEquals("right + 66-33 should encode as the above value + 2", (1 shl 16) + 2, right66)
     }
 
     @Test
     fun shouldDecodeRankCorrectly() {
         assertEquals(
-            "left + 30-70 should decode to left",
+            "left + 33-66 should decode to left",
             STAGE_POSITION_TOP_OR_LEFT,
-            AppPairsController.convertRankToStagePosition(left30),
+            AppPairsController.convertRankToStagePosition(left33),
         )
         assertEquals(
-            "left + 30-70 should decode to 30-70",
-            SNAP_TO_30_70,
-            AppPairsController.convertRankToSnapPosition(left30),
+            "left + 33-66 should decode to 33-66",
+            SNAP_TO_2_33_66,
+            AppPairsController.convertRankToSnapPosition(left33),
         )
 
         assertEquals(
@@ -142,30 +142,30 @@
         )
         assertEquals(
             "left + 50-50 should decode to 50-50",
-            SNAP_TO_50_50,
+            SNAP_TO_2_50_50,
             AppPairsController.convertRankToSnapPosition(left50),
         )
 
         assertEquals(
-            "left + 70-30 should decode to left",
+            "left + 66-33 should decode to left",
             STAGE_POSITION_TOP_OR_LEFT,
-            AppPairsController.convertRankToStagePosition(left70),
+            AppPairsController.convertRankToStagePosition(left66),
         )
         assertEquals(
-            "left + 70-30 should decode to 70-30",
-            SNAP_TO_70_30,
-            AppPairsController.convertRankToSnapPosition(left70),
+            "left + 66-33 should decode to 66-33",
+            SNAP_TO_2_66_33,
+            AppPairsController.convertRankToSnapPosition(left66),
         )
 
         assertEquals(
-            "right + 30-70 should decode to right",
+            "right + 33-66 should decode to right",
             STAGE_POSITION_BOTTOM_OR_RIGHT,
-            AppPairsController.convertRankToStagePosition(right30),
+            AppPairsController.convertRankToStagePosition(right33),
         )
         assertEquals(
-            "right + 30-70 should decode to 30-70",
-            SNAP_TO_30_70,
-            AppPairsController.convertRankToSnapPosition(right30),
+            "right + 33-66 should decode to 33-66",
+            SNAP_TO_2_33_66,
+            AppPairsController.convertRankToSnapPosition(right33),
         )
 
         assertEquals(
@@ -175,19 +175,19 @@
         )
         assertEquals(
             "right + 50-50 should decode to 50-50",
-            SNAP_TO_50_50,
+            SNAP_TO_2_50_50,
             AppPairsController.convertRankToSnapPosition(right50),
         )
 
         assertEquals(
-            "right + 70-30 should decode to right",
+            "right + 66-33 should decode to right",
             STAGE_POSITION_BOTTOM_OR_RIGHT,
-            AppPairsController.convertRankToStagePosition(right70),
+            AppPairsController.convertRankToStagePosition(right66),
         )
         assertEquals(
-            "right + 70-30 should decode to 70-30",
-            SNAP_TO_70_30,
-            AppPairsController.convertRankToSnapPosition(right70),
+            "right + 66-33 should decode to 66-33",
+            SNAP_TO_2_66_33,
+            AppPairsController.convertRankToSnapPosition(right66),
         )
     }
 
@@ -202,7 +202,7 @@
         // Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback
         spyAppPairsController.handleAppPairLaunchInApp(
             mockAppPairIcon,
-            listOf(mockItemInfo1, mockItemInfo2)
+            listOf(mockItemInfo1, mockItemInfo2),
         )
         verify(splitSelectStateController)
             .findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture())
@@ -226,7 +226,7 @@
         // Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback
         spyAppPairsController.handleAppPairLaunchInApp(
             mockAppPairIcon,
-            listOf(mockItemInfo1, mockItemInfo2)
+            listOf(mockItemInfo1, mockItemInfo2),
         )
         verify(splitSelectStateController)
             .findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture())
@@ -250,7 +250,7 @@
         // Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback
         spyAppPairsController.handleAppPairLaunchInApp(
             mockAppPairIcon,
-            listOf(mockItemInfo1, mockItemInfo2)
+            listOf(mockItemInfo1, mockItemInfo2),
         )
         verify(splitSelectStateController)
             .findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture())
@@ -274,7 +274,7 @@
         // Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback
         spyAppPairsController.handleAppPairLaunchInApp(
             mockAppPairIcon,
-            listOf(mockItemInfo1, mockItemInfo2)
+            listOf(mockItemInfo1, mockItemInfo2),
         )
         verify(splitSelectStateController)
             .findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture())
@@ -298,7 +298,7 @@
         // Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback
         spyAppPairsController.handleAppPairLaunchInApp(
             mockAppPairIcon,
-            listOf(mockItemInfo1, mockItemInfo2)
+            listOf(mockItemInfo1, mockItemInfo2),
         )
         verify(splitSelectStateController)
             .findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture())
@@ -322,7 +322,7 @@
         // Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback
         spyAppPairsController.handleAppPairLaunchInApp(
             mockAppPairIcon,
-            listOf(mockItemInfo1, mockItemInfo2)
+            listOf(mockItemInfo1, mockItemInfo2),
         )
         verify(splitSelectStateController)
             .findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture())
@@ -341,12 +341,16 @@
         whenever(mockTaskKey1.getId()).thenReturn(1)
         whenever(mockTaskKey2.getId()).thenReturn(2)
         // ... with app 1 already on screen
-        whenever(mockCachedTaskInfo.taskId).thenReturn(1)
+        if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+            whenever(mockCachedTaskInfo.topGroupedTaskContainsTask(eq(1))).thenReturn(true)
+        } else {
+            whenever(mockCachedTaskInfo.taskId).thenReturn(1)
+        }
 
         // Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback
         spyAppPairsController.handleAppPairLaunchInApp(
             mockAppPairIcon,
-            listOf(mockItemInfo1, mockItemInfo2)
+            listOf(mockItemInfo1, mockItemInfo2),
         )
         verify(splitSelectStateController)
             .findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture())
@@ -365,12 +369,16 @@
         whenever(mockTaskKey1.getId()).thenReturn(1)
         whenever(mockTaskKey2.getId()).thenReturn(2)
         // ... with app 2 already on screen
-        whenever(mockCachedTaskInfo.taskId).thenReturn(2)
+        if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+            whenever(mockCachedTaskInfo.topGroupedTaskContainsTask(eq(2))).thenReturn(true)
+        } else {
+            whenever(mockCachedTaskInfo.taskId).thenReturn(2)
+        }
 
         // Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback
         spyAppPairsController.handleAppPairLaunchInApp(
             mockAppPairIcon,
-            listOf(mockItemInfo1, mockItemInfo2)
+            listOf(mockItemInfo1, mockItemInfo2),
         )
         verify(splitSelectStateController)
             .findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture())
@@ -389,12 +397,16 @@
         whenever(mockTaskKey1.getId()).thenReturn(1)
         whenever(mockTaskKey2.getId()).thenReturn(2)
         // ... with app 3 already on screen
-        whenever(mockCachedTaskInfo.taskId).thenReturn(3)
+        if (com.android.wm.shell.Flags.enableShellTopTaskTracking()) {
+            whenever(mockCachedTaskInfo.topGroupedTaskContainsTask(eq(3))).thenReturn(true)
+        } else {
+            whenever(mockCachedTaskInfo.taskId).thenReturn(3)
+        }
 
         // Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback
         spyAppPairsController.handleAppPairLaunchInApp(
             mockAppPairIcon,
-            listOf(mockItemInfo1, mockItemInfo2)
+            listOf(mockItemInfo1, mockItemInfo2),
         )
         verify(splitSelectStateController)
             .findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture())
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/ContextualSearchInvokerTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/ContextualSearchInvokerTest.java
new file mode 100644
index 0000000..543ffe6
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/ContextualSearchInvokerTest.java
@@ -0,0 +1,253 @@
+/*
+ * 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 static android.app.contextualsearch.ContextualSearchManager.FEATURE_CONTEXTUAL_SEARCH;
+
+import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
+
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_ASSISTANT_FAILED_SERVICE_ERROR;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_ATTEMPTED_OVER_KEYGUARD;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_ATTEMPTED_OVER_NOTIFICATION_SHADE;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_ATTEMPTED_SPLITSCREEN;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_FAILED_NOT_AVAILABLE;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_FAILED_SETTING_DISABLED;
+import static com.android.quickstep.util.ContextualSearchInvoker.KEYGUARD_SHOWING_SYSUI_FLAGS;
+import static com.android.quickstep.util.ContextualSearchInvoker.SHADE_EXPANDED_SYSUI_FLAGS;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import android.app.contextualsearch.ContextualSearchManager;
+import android.content.Context;
+import android.content.pm.PackageManager;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.launcher3.logging.StatsLogManager;
+import com.android.quickstep.DeviceConfigWrapper;
+import com.android.quickstep.SystemUiProxy;
+import com.android.quickstep.TopTaskTracker;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Robolectric unit tests for {@link ContextualSearchInvoker}
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ContextualSearchInvokerTest {
+
+    private static final int CONTEXTUAL_SEARCH_ENTRY_POINT = 123;
+
+    private @Mock PackageManager mMockPackageManager;
+    private @Mock ContextualSearchStateManager mMockStateManager;
+    private @Mock TopTaskTracker mMockTopTaskTracker;
+    private @Mock SystemUiProxy mMockSystemUiProxy;
+    private @Mock StatsLogManager mMockStatsLogManager;
+    private @Mock StatsLogManager.StatsLogger mMockStatsLogger;
+    private @Mock ContextualSearchHapticManager mMockContextualSearchHapticManager;
+    private @Mock ContextualSearchManager mMockContextualSearchManager;
+    private ContextualSearchInvoker mContextualSearchInvoker;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        when(mMockPackageManager.hasSystemFeature(FEATURE_CONTEXTUAL_SEARCH)).thenReturn(true);
+        Context context = spy(getApplicationContext());
+        doReturn(mMockPackageManager).when(context).getPackageManager();
+        when(mMockSystemUiProxy.getLastSystemUiStateFlags()).thenReturn(0L);
+        when(mMockTopTaskTracker.getRunningSplitTaskIds()).thenReturn(new int[]{});
+        when(mMockStateManager.isContextualSearchIntentAvailable()).thenReturn(true);
+        when(mMockStateManager.isContextualSearchSettingEnabled()).thenReturn(true);
+        when(mMockStatsLogManager.logger()).thenReturn(mMockStatsLogger);
+
+        mContextualSearchInvoker = new ContextualSearchInvoker(context, mMockStateManager,
+                mMockTopTaskTracker, mMockSystemUiProxy, mMockStatsLogManager,
+                mMockContextualSearchHapticManager, mMockContextualSearchManager);
+    }
+
+    @Test
+    public void runContextualSearchInvocationChecksAndLogFailures_contextualSearchFeatureIsNotAvailable() {
+        when(mMockPackageManager.hasSystemFeature(FEATURE_CONTEXTUAL_SEARCH)).thenReturn(false);
+
+        assertFalse("Expected invocation to fail when feature is unavailable",
+                mContextualSearchInvoker.runContextualSearchInvocationChecksAndLogFailures());
+
+        verify(mMockStatsLogger).log(LAUNCHER_LAUNCH_ASSISTANT_FAILED_SERVICE_ERROR);
+    }
+
+    @Test
+    public void runContextualSearchInvocationChecksAndLogFailures_contextualSearchIntentIsAvailable() {
+        assertTrue("Expected invocation checks to succeed",
+                mContextualSearchInvoker.runContextualSearchInvocationChecksAndLogFailures());
+
+        verifyNoMoreInteractions(mMockStatsLogManager);
+    }
+
+    @Test
+    public void runContextualSearchInvocationChecksAndLogFailures_contextualSearchIntentIsNotAvailable() {
+        when(mMockStateManager.isContextualSearchIntentAvailable()).thenReturn(false);
+
+        assertFalse("Expected invocation to fail when feature is unavailable",
+                mContextualSearchInvoker.runContextualSearchInvocationChecksAndLogFailures());
+
+        verify(mMockStatsLogger).log(LAUNCHER_LAUNCH_OMNI_FAILED_NOT_AVAILABLE);
+    }
+
+    @Test
+    public void runContextualSearchInvocationChecksAndLogFailures_settingDisabled() {
+        when(mMockStateManager.isContextualSearchSettingEnabled()).thenReturn(false);
+
+        assertFalse("Expected invocation checks to fail when setting is disabled",
+                mContextualSearchInvoker.runContextualSearchInvocationChecksAndLogFailures());
+
+        verify(mMockStatsLogger).log(LAUNCHER_LAUNCH_OMNI_FAILED_SETTING_DISABLED);
+    }
+
+    @Test
+    public void runContextualSearchInvocationChecksAndLogFailures_notificationShadeIsShowing() {
+        when(mMockSystemUiProxy.getLastSystemUiStateFlags()).thenReturn(SHADE_EXPANDED_SYSUI_FLAGS);
+
+        assertFalse("Expected invocation checks to fail when notification shade is showing",
+                mContextualSearchInvoker.runContextualSearchInvocationChecksAndLogFailures());
+
+        verify(mMockStatsLogger).log(LAUNCHER_LAUNCH_OMNI_ATTEMPTED_OVER_NOTIFICATION_SHADE);
+    }
+
+    @Test
+    public void runContextualSearchInvocationChecksAndLogFailures_keyguardIsShowing() {
+        when(mMockSystemUiProxy.getLastSystemUiStateFlags()).thenReturn(
+                KEYGUARD_SHOWING_SYSUI_FLAGS);
+
+        assertFalse("Expected invocation checks to fail when keyguard is showing",
+                mContextualSearchInvoker.runContextualSearchInvocationChecksAndLogFailures());
+
+        verify(mMockStatsLogger).log(LAUNCHER_LAUNCH_OMNI_ATTEMPTED_OVER_KEYGUARD);
+    }
+
+    @Test
+    public void runContextualSearchInvocationChecksAndLogFailures_isInSplitScreen_disallowed() {
+        when(mMockStateManager.isInvocationAllowedInSplitscreen()).thenReturn(false);
+        when(mMockTopTaskTracker.getRunningSplitTaskIds()).thenReturn(new int[]{1, 2, 3});
+
+        assertFalse("Expected invocation checks to fail over split screen",
+                mContextualSearchInvoker.runContextualSearchInvocationChecksAndLogFailures());
+
+        // Attempt is logged regardless.
+        verify(mMockStatsLogger).log(LAUNCHER_LAUNCH_OMNI_ATTEMPTED_SPLITSCREEN);
+    }
+
+    @Test
+    public void runContextualSearchInvocationChecksAndLogFailures_isInSplitScreen_allowed() {
+        when(mMockStateManager.isInvocationAllowedInSplitscreen()).thenReturn(true);
+        when(mMockTopTaskTracker.getRunningSplitTaskIds()).thenReturn(new int[]{1, 2, 3});
+
+        assertTrue("Expected invocation checks to succeed over split screen",
+                mContextualSearchInvoker.runContextualSearchInvocationChecksAndLogFailures());
+
+        // Attempt is logged regardless.
+        verify(mMockStatsLogger).log(LAUNCHER_LAUNCH_OMNI_ATTEMPTED_SPLITSCREEN);
+    }
+
+    @Test
+    public void invokeContextualSearchUncheckedWithHaptic_cssIsAvailable_commitHapticEnabled() {
+        try (AutoCloseable flag = overrideSearchHapticCommitFlag(true)) {
+            assertTrue("Expected invocation unchecked to succeed",
+                    mContextualSearchInvoker.invokeContextualSearchUncheckedWithHaptic(
+                            CONTEXTUAL_SEARCH_ENTRY_POINT));
+            verify(mMockContextualSearchHapticManager).vibrateForSearch();
+            verify(mMockContextualSearchManager).startContextualSearch(
+                    CONTEXTUAL_SEARCH_ENTRY_POINT);
+            verifyNoMoreInteractions(mMockStatsLogManager);
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Test
+    public void invokeContextualSearchUncheckedWithHaptic_cssIsAvailable_commitHapticDisabled() {
+        try (AutoCloseable flag = overrideSearchHapticCommitFlag(false)) {
+            assertTrue("Expected invocation unchecked to succeed",
+                    mContextualSearchInvoker.invokeContextualSearchUncheckedWithHaptic(
+                            CONTEXTUAL_SEARCH_ENTRY_POINT));
+            verify(mMockContextualSearchHapticManager, never()).vibrateForSearch();
+            verify(mMockContextualSearchManager).startContextualSearch(
+                    CONTEXTUAL_SEARCH_ENTRY_POINT);
+            verifyNoMoreInteractions(mMockStatsLogManager);
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Test
+    public void invokeContextualSearchUncheckedWithHaptic_cssIsNotAvailable_commitHapticEnabled() {
+        when(mMockStateManager.isContextualSearchIntentAvailable()).thenReturn(false);
+
+        try (AutoCloseable flag = overrideSearchHapticCommitFlag(true)) {
+            // Still expect true since this method doesn't run the checks.
+            assertTrue("Expected invocation unchecked to succeed",
+                    mContextualSearchInvoker.invokeContextualSearchUncheckedWithHaptic(
+                            CONTEXTUAL_SEARCH_ENTRY_POINT));
+            // Still vibrate based on the flag.
+            verify(mMockContextualSearchHapticManager).vibrateForSearch();
+            verify(mMockContextualSearchManager).startContextualSearch(
+                    CONTEXTUAL_SEARCH_ENTRY_POINT);
+            verifyNoMoreInteractions(mMockStatsLogManager);
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+
+    @Test
+    public void invokeContextualSearchUncheckedWithHaptic_cssIsNotAvailable_commitHapticDisabled() {
+        when(mMockStateManager.isContextualSearchIntentAvailable()).thenReturn(false);
+
+        try (AutoCloseable flag = overrideSearchHapticCommitFlag(false)) {
+            // Still expect true since this method doesn't run the checks.
+            assertTrue("Expected ContextualSearch invocation unchecked to succeed",
+                    mContextualSearchInvoker.invokeContextualSearchUncheckedWithHaptic(
+                            CONTEXTUAL_SEARCH_ENTRY_POINT));
+            // Still don't vibrate based on the flag.
+            verify(mMockContextualSearchHapticManager, never()).vibrateForSearch();
+            verify(mMockContextualSearchManager).startContextualSearch(
+                    CONTEXTUAL_SEARCH_ENTRY_POINT);
+            verifyNoMoreInteractions(mMockStatsLogManager);
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private AutoCloseable overrideSearchHapticCommitFlag(boolean value) {
+        return TestExtensions.overrideNavConfigFlag(
+                "ENABLE_SEARCH_HAPTIC_COMMIT",
+                value,
+                () -> DeviceConfigWrapper.get().getEnableSearchHapticCommit());
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/GroupTaskTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/GroupTaskTest.kt
index 7b1c066..108cfb5 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/GroupTaskTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/GroupTaskTest.kt
@@ -66,7 +66,7 @@
                 Rect(),
                 1,
                 2,
-                SplitScreenConstants.SNAP_TO_50_50
+                SplitScreenConstants.SNAP_TO_2_50_50
             )
         val task1 = GroupTask(createTask(1), createTask(2), splitBounds, TaskViewType.GROUPED)
         val task2 = GroupTask(createTask(1), createTask(2), splitBounds, TaskViewType.GROUPED)
@@ -81,7 +81,7 @@
                 Rect(),
                 1,
                 2,
-                SplitScreenConstants.SNAP_TO_50_50
+                SplitScreenConstants.SNAP_TO_2_50_50
             )
         val splitBounds2 =
             SplitConfigurationOptions.SplitBounds(
@@ -89,7 +89,7 @@
                 Rect(),
                 1,
                 2,
-                SplitScreenConstants.SNAP_TO_30_70
+                SplitScreenConstants.SNAP_TO_2_33_66
             )
         val task1 = GroupTask(createTask(1), createTask(2), splitBounds1, TaskViewType.GROUPED)
         val task2 = GroupTask(createTask(1), createTask(2), splitBounds2, TaskViewType.GROUPED)
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt
index fc4c4f6..cb70694 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt
@@ -22,7 +22,6 @@
 import android.content.ComponentName
 import android.content.Intent
 import android.graphics.Rect
-import android.os.Handler
 import android.os.UserHandle
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.android.launcher3.LauncherState
@@ -37,10 +36,10 @@
 import com.android.quickstep.RecentsModel
 import com.android.quickstep.SystemUiProxy
 import com.android.quickstep.util.SplitSelectStateController.SplitFromDesktopController
+import com.android.quickstep.views.RecentsView
 import com.android.quickstep.views.RecentsViewContainer
 import com.android.systemui.shared.recents.model.Task
-import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_50_50
-import java.util.function.Consumer
+import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertNull
@@ -52,8 +51,10 @@
 import org.mockito.Mockito.`when`
 import org.mockito.kotlin.argumentCaptor
 import org.mockito.kotlin.mock
+import org.mockito.kotlin.times
 import org.mockito.kotlin.verify
 import org.mockito.kotlin.whenever
+import java.util.function.Consumer
 
 @RunWith(AndroidJUnit4::class)
 class SplitSelectStateControllerTest {
@@ -63,11 +64,11 @@
     private val statsLogManager: StatsLogManager = mock()
     private val statsLogger: StatsLogger = mock()
     private val stateManager: StateManager<LauncherState, StatefulActivity<LauncherState>> = mock()
-    private val handler: Handler = mock()
     private val context: RecentsViewContainer = mock()
     private val recentsModel: RecentsModel = mock()
     private val pendingIntent: PendingIntent = mock()
     private val splitFromDesktopController: SplitFromDesktopController = mock()
+    private val recentsView: RecentsView<*, *> = mock()
 
     private lateinit var splitSelectStateController: SplitSelectStateController
 
@@ -75,6 +76,7 @@
     private val nonPrimaryUserHandle = UserHandle(ActivityManager.RunningTaskInfo().userId + 10)
 
     private var taskIdCounter = 0
+
     private fun getUniqueId(): Int {
         return ++taskIdCounter
     }
@@ -87,13 +89,12 @@
         splitSelectStateController =
             SplitSelectStateController(
                 context,
-                handler,
                 stateManager,
                 depthController,
                 statsLogManager,
                 systemUiProxy,
                 recentsModel,
-                null /*activityBackCallback*/
+                null, /*activityBackCallback*/
             )
     }
 
@@ -103,12 +104,12 @@
         val groupTask1 =
             generateGroupTask(
                 ComponentName("pomegranate", "juice"),
-                ComponentName("pumpkin", "pie")
+                ComponentName("pumpkin", "pie"),
             )
         val groupTask2 =
             generateGroupTask(
                 ComponentName("hotdog", "juice"),
-                ComponentName("personal", "computer")
+                ComponentName("personal", "computer"),
             )
         val tasks: ArrayList<GroupTask> = ArrayList()
         tasks.add(groupTask1)
@@ -125,7 +126,7 @@
                     splitSelectStateController.findLastActiveTasksAndRunCallback(
                         listOf(nonMatchingComponent),
                         false /* findExactPairMatch */,
-                        taskConsumer
+                        taskConsumer,
                     )
                     verify(recentsModel).getTasks(capture())
                 }
@@ -144,12 +145,12 @@
         val groupTask1 =
             generateGroupTask(
                 ComponentName(matchingPackage, matchingClass),
-                ComponentName("pomegranate", "juice")
+                ComponentName("pomegranate", "juice"),
             )
         val groupTask2 =
             generateGroupTask(
                 ComponentName("pumpkin", "pie"),
-                ComponentName("personal", "computer")
+                ComponentName("personal", "computer"),
             )
         val tasks: ArrayList<GroupTask> = ArrayList()
         tasks.add(groupTask1)
@@ -162,12 +163,12 @@
                 assertEquals(
                     "ComponentName package mismatched",
                     it[0].key.baseIntent.component?.packageName,
-                    matchingPackage
+                    matchingPackage,
                 )
                 assertEquals(
                     "ComponentName class mismatched",
                     it[0].key.baseIntent.component?.className,
-                    matchingClass
+                    matchingClass,
                 )
                 assertEquals(it[0], groupTask1.task1)
             }
@@ -178,7 +179,7 @@
                     splitSelectStateController.findLastActiveTasksAndRunCallback(
                         listOf(matchingComponent),
                         false /* findExactPairMatch */,
-                        taskConsumer
+                        taskConsumer,
                     )
                     verify(recentsModel).getTasks(capture())
                 }
@@ -197,12 +198,12 @@
         val groupTask1 =
             generateGroupTask(
                 ComponentName(matchingPackage, matchingClass),
-                ComponentName("pomegranate", "juice")
+                ComponentName("pomegranate", "juice"),
             )
         val groupTask2 =
             generateGroupTask(
                 ComponentName("pumpkin", "pie"),
-                ComponentName("personal", "computer")
+                ComponentName("personal", "computer"),
             )
         val tasks: ArrayList<GroupTask> = ArrayList()
         tasks.add(groupTask1)
@@ -219,7 +220,7 @@
                     splitSelectStateController.findLastActiveTasksAndRunCallback(
                         listOf(nonPrimaryUserComponent),
                         false /* findExactPairMatch */,
-                        taskConsumer
+                        taskConsumer,
                     )
                     verify(recentsModel).getTasks(capture())
                 }
@@ -240,12 +241,12 @@
                 ComponentName(matchingPackage, matchingClass),
                 nonPrimaryUserHandle,
                 ComponentName("pomegranate", "juice"),
-                nonPrimaryUserHandle
+                nonPrimaryUserHandle,
             )
         val groupTask2 =
             generateGroupTask(
                 ComponentName("pumpkin", "pie"),
-                ComponentName("personal", "computer")
+                ComponentName("personal", "computer"),
             )
         val tasks: ArrayList<GroupTask> = ArrayList()
         tasks.add(groupTask1)
@@ -258,12 +259,12 @@
                 assertEquals(
                     "ComponentName package mismatched",
                     it[0].key.baseIntent.component?.packageName,
-                    matchingPackage
+                    matchingPackage,
                 )
                 assertEquals(
                     "ComponentName class mismatched",
                     it[0].key.baseIntent.component?.className,
-                    matchingClass
+                    matchingClass,
                 )
                 assertEquals("userId mismatched", it[0].key.userId, nonPrimaryUserHandle.identifier)
                 assertEquals(it[0], groupTask1.task1)
@@ -275,7 +276,7 @@
                     splitSelectStateController.findLastActiveTasksAndRunCallback(
                         listOf(nonPrimaryUserComponent),
                         false /* findExactPairMatch */,
-                        taskConsumer
+                        taskConsumer,
                     )
                     verify(recentsModel).getTasks(capture())
                 }
@@ -294,12 +295,12 @@
         val groupTask1 =
             generateGroupTask(
                 ComponentName(matchingPackage, matchingClass),
-                ComponentName("pumpkin", "pie")
+                ComponentName("pumpkin", "pie"),
             )
         val groupTask2 =
             generateGroupTask(
                 ComponentName("pomegranate", "juice"),
-                ComponentName(matchingPackage, matchingClass)
+                ComponentName(matchingPackage, matchingClass),
             )
         val tasks: ArrayList<GroupTask> = ArrayList()
         tasks.add(groupTask2)
@@ -312,12 +313,12 @@
                 assertEquals(
                     "ComponentName package mismatched",
                     it[0].key.baseIntent.component?.packageName,
-                    matchingPackage
+                    matchingPackage,
                 )
                 assertEquals(
                     "ComponentName class mismatched",
                     it[0].key.baseIntent.component?.className,
-                    matchingClass
+                    matchingClass,
                 )
                 assertEquals(it[0], groupTask1.task1)
             }
@@ -328,7 +329,7 @@
                     splitSelectStateController.findLastActiveTasksAndRunCallback(
                         listOf(matchingComponent),
                         false /* findExactPairMatch */,
-                        taskConsumer
+                        taskConsumer,
                     )
                     verify(recentsModel).getTasks(capture())
                 }
@@ -351,7 +352,7 @@
         val groupTask2 =
             generateGroupTask(
                 ComponentName("pomegranate", "juice"),
-                ComponentName(matchingPackage, matchingClass)
+                ComponentName(matchingPackage, matchingClass),
             )
         val tasks: ArrayList<GroupTask> = ArrayList()
         tasks.add(groupTask2)
@@ -366,12 +367,12 @@
                 assertEquals(
                     "ComponentName package mismatched",
                     it[1].key.baseIntent.component?.packageName,
-                    matchingPackage
+                    matchingPackage,
                 )
                 assertEquals(
                     "ComponentName class mismatched",
                     it[1].key.baseIntent.component?.className,
-                    matchingClass
+                    matchingClass,
                 )
                 assertEquals(it[1], groupTask2.task2)
             }
@@ -382,7 +383,7 @@
                     splitSelectStateController.findLastActiveTasksAndRunCallback(
                         listOf(nonMatchingComponent, matchingComponent),
                         false /* findExactPairMatch */,
-                        taskConsumer
+                        taskConsumer,
                     )
                     verify(recentsModel).getTasks(capture())
                 }
@@ -404,7 +405,7 @@
         val groupTask2 =
             generateGroupTask(
                 ComponentName("pomegranate", "juice"),
-                ComponentName(matchingPackage, matchingClass)
+                ComponentName(matchingPackage, matchingClass),
             )
         val tasks: ArrayList<GroupTask> = ArrayList()
         tasks.add(groupTask2)
@@ -418,12 +419,12 @@
                 assertEquals(
                     "ComponentName package mismatched",
                     it[0].key.baseIntent.component?.packageName,
-                    matchingPackage
+                    matchingPackage,
                 )
                 assertEquals(
                     "ComponentName class mismatched",
                     it[0].key.baseIntent.component?.className,
-                    matchingClass
+                    matchingClass,
                 )
                 assertEquals(it[0], groupTask2.task2)
                 assertNull("No tasks should have matched", it[1] /*task*/)
@@ -435,7 +436,7 @@
                     splitSelectStateController.findLastActiveTasksAndRunCallback(
                         listOf(matchingComponent, matchingComponent),
                         false /* findExactPairMatch */,
-                        taskConsumer
+                        taskConsumer,
                     )
                     verify(recentsModel).getTasks(capture())
                 }
@@ -455,12 +456,12 @@
         val groupTask1 =
             generateGroupTask(
                 ComponentName(matchingPackage, matchingClass),
-                ComponentName("pumpkin", "pie")
+                ComponentName("pumpkin", "pie"),
             )
         val groupTask2 =
             generateGroupTask(
                 ComponentName("pomegranate", "juice"),
-                ComponentName(matchingPackage, matchingClass)
+                ComponentName(matchingPackage, matchingClass),
             )
         val tasks: ArrayList<GroupTask> = ArrayList()
         tasks.add(groupTask2)
@@ -474,23 +475,23 @@
                 assertEquals(
                     "ComponentName package mismatched",
                     it[0].key.baseIntent.component?.packageName,
-                    matchingPackage
+                    matchingPackage,
                 )
                 assertEquals(
                     "ComponentName class mismatched",
                     it[0].key.baseIntent.component?.className,
-                    matchingClass
+                    matchingClass,
                 )
                 assertEquals(it[0], groupTask1.task1)
                 assertEquals(
                     "ComponentName package mismatched",
                     it[1].key.baseIntent.component?.packageName,
-                    matchingPackage
+                    matchingPackage,
                 )
                 assertEquals(
                     "ComponentName class mismatched",
                     it[1].key.baseIntent.component?.className,
-                    matchingClass
+                    matchingClass,
                 )
                 assertEquals(it[1], groupTask2.task2)
             }
@@ -501,7 +502,7 @@
                     splitSelectStateController.findLastActiveTasksAndRunCallback(
                         listOf(matchingComponent, matchingComponent),
                         false /* findExactPairMatch */,
-                        taskConsumer
+                        taskConsumer,
                     )
                     verify(recentsModel).getTasks(capture())
                 }
@@ -527,12 +528,12 @@
         val groupTask2 =
             generateGroupTask(
                 ComponentName(matchingPackage2, matchingClass2),
-                ComponentName(matchingPackage, matchingClass)
+                ComponentName(matchingPackage, matchingClass),
             )
         val groupTask3 =
             generateGroupTask(
                 ComponentName("hotdog", "pie"),
-                ComponentName(matchingPackage, matchingClass)
+                ComponentName(matchingPackage, matchingClass),
             )
         val tasks: ArrayList<GroupTask> = ArrayList()
         tasks.add(groupTask3)
@@ -553,7 +554,7 @@
                     splitSelectStateController.findLastActiveTasksAndRunCallback(
                         listOf(matchingComponent2, matchingComponent),
                         true /* findExactPairMatch */,
-                        taskConsumer
+                        taskConsumer,
                     )
                     verify(recentsModel).getTasks(capture())
                 }
@@ -570,7 +571,7 @@
             -1 /*stagePosition*/,
             ItemInfo(),
             null /*splitEvent*/,
-            10 /*alreadyRunningTask*/
+            10, /*alreadyRunningTask*/
         )
         assertTrue(splitSelectStateController.isSplitSelectActive)
     }
@@ -582,21 +583,23 @@
             -1 /*stagePosition*/,
             ItemInfo(),
             null /*splitEvent*/,
-            -1 /*alreadyRunningTask*/
+            -1, /*alreadyRunningTask*/
         )
         assertTrue(splitSelectStateController.isSplitSelectActive)
     }
 
     @Test
     fun resetAfterInitial() {
+        whenever(context.getOverviewPanel<RecentsView<*, *>>()).thenReturn(recentsView)
         splitSelectStateController.setInitialTaskSelect(
             Intent() /*intent*/,
             -1 /*stagePosition*/,
             ItemInfo(),
             null /*splitEvent*/,
-            -1
+            -1,
         )
         splitSelectStateController.resetState()
+        verify(recentsView, times(1)).resetDesktopTaskFromSplitSelectState()
         assertFalse(splitSelectStateController.isSplitSelectActive)
     }
 
@@ -625,7 +628,7 @@
     // Generate GroupTask with default userId.
     private fun generateGroupTask(
         task1ComponentName: ComponentName,
-        task2ComponentName: ComponentName
+        task2ComponentName: ComponentName,
     ): GroupTask {
         val task1 = Task()
         var taskInfo = ActivityManager.RunningTaskInfo()
@@ -645,7 +648,7 @@
         return GroupTask(
             task1,
             task2,
-            SplitConfigurationOptions.SplitBounds(Rect(), Rect(), -1, -1, SNAP_TO_50_50)
+            SplitConfigurationOptions.SplitBounds(Rect(), Rect(), -1, -1, SNAP_TO_2_50_50),
         )
     }
 
@@ -654,7 +657,7 @@
         task1ComponentName: ComponentName,
         userHandle1: UserHandle,
         task2ComponentName: ComponentName,
-        userHandle2: UserHandle
+        userHandle2: UserHandle,
     ): GroupTask {
         val task1 = Task()
         var taskInfo = ActivityManager.RunningTaskInfo()
@@ -677,7 +680,7 @@
         return GroupTask(
             task1,
             task2,
-            SplitConfigurationOptions.SplitBounds(Rect(), Rect(), -1, -1, SNAP_TO_50_50)
+            SplitConfigurationOptions.SplitBounds(Rect(), Rect(), -1, -1, SNAP_TO_2_50_50),
         )
     }
 }
diff --git a/quickstep/tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java b/quickstep/tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java
index 7b57c81..c53c177 100644
--- a/quickstep/tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java
+++ b/quickstep/tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java
@@ -33,6 +33,7 @@
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.spy;
 
 import android.app.prediction.AppTarget;
 import android.app.prediction.AppTargetId;
@@ -42,6 +43,8 @@
 import android.content.pm.ApplicationInfo;
 import android.content.pm.LauncherApps;
 import android.os.UserHandle;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
 import android.platform.test.flag.junit.SetFlagsRule;
 import android.text.TextUtils;
 
@@ -62,9 +65,13 @@
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
 
 import java.util.Arrays;
 import java.util.List;
+import java.util.Set;
+import java.util.function.Predicate;
 import java.util.stream.Collectors;
 
 @SmallTest
@@ -72,6 +79,9 @@
 public final class WidgetsPredicationUpdateTaskTest {
 
     @Rule
+    public final MockitoRule mocks = MockitoJUnit.rule();
+
+    @Rule
     public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
 
     private AppWidgetProviderInfo mApp1Provider1;
@@ -145,6 +155,7 @@
     }
 
     @Test
+    @DisableFlags(Flags.FLAG_ENABLE_TIERED_WIDGETS_BY_DEFAULT_IN_PICKER) // Flag off
     public void widgetsRecommendationRan_shouldOnlyReturnNotAddedWidgetsInAppPredictionOrder() {
         // Run on model executor so that no other task runs in the middle.
         runOnExecutorSync(MODEL_EXECUTOR, () -> {
@@ -184,6 +195,7 @@
     }
 
     @Test
+    @DisableFlags(Flags.FLAG_ENABLE_TIERED_WIDGETS_BY_DEFAULT_IN_PICKER) // Flag off
     public void widgetsRecommendationRan_shouldReturnEmptyWidgetsWhenEmpty() {
         runOnExecutorSync(MODEL_EXECUTOR, () -> {
 
@@ -213,6 +225,50 @@
         });
     }
 
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_TIERED_WIDGETS_BY_DEFAULT_IN_PICKER)
+    public void widgetsRecommendationRan_keepsWidgetsNotOnWorkspace_addsWidgetsFromEligibleApps() {
+        runOnExecutorSync(MODEL_EXECUTOR, () -> {
+            WidgetsFilterDataProvider spiedFilterProvider = spy(
+                    mModelHelper.getModel().getWidgetsFilterDataProvider());
+            doAnswer(i -> new Predicate<WidgetItem>() {
+                @Override
+                public boolean test(WidgetItem widgetItem) {
+                    // app5's widget is already on workspace, but, app2 is not.
+                    // And app4's second widget is also not on workspace.
+                    return Set.of("app5", "app2", "app4").contains(
+                            widgetItem.componentName.getPackageName());
+                }
+            }).when(spiedFilterProvider).getPredictedWidgetsFilter();
+            mModelHelper.getBgDataModel().widgetsModel.updateWidgetFilters(spiedFilterProvider);
+            // App5's widget that's already on workspace.
+            AppTarget widget1 = new AppTarget(new AppTargetId("app5"), "app5", "provider1",
+                    mUserHandle);
+            // App4's widget eligible and not on workspace.
+            AppTarget widget2 = new AppTarget(new AppTargetId("app4"), "app4", "provider2",
+                    mUserHandle);
+
+            mCallback.mRecommendedWidgets = null;
+            mModelHelper.getModel().enqueueModelUpdateTask(
+                    newWidgetsPredicationTask(List.of(widget1, widget2)));
+            runOnExecutorSync(MAIN_EXECUTOR, () -> {
+            });
+
+            List<PendingAddWidgetInfo> recommendedWidgets = mCallback.mRecommendedWidgets.items
+                    .stream()
+                    .map(itemInfo -> (PendingAddWidgetInfo) itemInfo)
+                    .collect(Collectors.toList());
+            assertThat(recommendedWidgets).hasSize(2);
+            List<ComponentName> componentNames = recommendedWidgets.stream().map(
+                    w -> w.componentName).toList();
+            assertThat(componentNames).containsExactly(
+                    // Locally added, not on workspace, eligible app per filter
+                    mApp2Provider1.provider,
+                    // From prediction service, not on workspace, eligible app per filter
+                    mApp4Provider2.provider);
+        });
+    }
+
     private void assertWidgetInfo(
             LauncherAppWidgetProviderInfo actual, AppWidgetProviderInfo expected) {
         assertThat(actual.provider).isEqualTo(expected.provider);
diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/FallbackTaskbarUIControllerTest.kt b/quickstep/tests/src/com/android/launcher3/taskbar/FallbackTaskbarUIControllerTest.kt
index 04012c0..df98606 100644
--- a/quickstep/tests/src/com/android/launcher3/taskbar/FallbackTaskbarUIControllerTest.kt
+++ b/quickstep/tests/src/com/android/launcher3/taskbar/FallbackTaskbarUIControllerTest.kt
@@ -33,7 +33,7 @@
 @RunWith(AndroidJUnit4::class)
 class FallbackTaskbarUIControllerTest : TaskbarBaseTestCase() {
 
-    lateinit var fallbackTaskbarUIController: FallbackTaskbarUIController
+    lateinit var fallbackTaskbarUIController: FallbackTaskbarUIController<RecentsActivity>
     lateinit var stateListener: StateManager.StateListener<RecentsState>
 
     private val recentsActivity: RecentsActivity = mock()
diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
index b67bc5a..066ddc0 100644
--- a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
+++ b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
@@ -26,11 +26,13 @@
 import android.platform.test.rule.TestWatcher
 import android.testing.AndroidTestingRunner
 import com.android.internal.R
+import com.android.launcher3.BubbleTextView.RunningAppState
 import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT
 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.model.data.WorkspaceItemInfo
 import com.android.quickstep.RecentsModel
 import com.android.quickstep.RecentsModel.RecentTasksChangedListener
 import com.android.quickstep.TaskIconCache
@@ -77,7 +79,9 @@
     private var taskListChangeId: Int = 1
 
     private lateinit var recentAppsController: TaskbarRecentAppsController
-    private lateinit var userHandle: UserHandle
+    private lateinit var myUserHandle: UserHandle
+    private val USER_HANDLE_1 = UserHandle.of(1)
+    private val USER_HANDLE_2 = UserHandle.of(2)
 
     private var canShowRunningAndRecentAppsAtInit = true
     private var recentTasksChangedListener: RecentTasksChangedListener? = null
@@ -85,7 +89,7 @@
     @Before
     fun setUp() {
         super.setup()
-        userHandle = Process.myUserHandle()
+        myUserHandle = Process.myUserHandle()
 
         // Set desktop mode supported
         whenever(mockContext.getResources()).thenReturn(mockResources)
@@ -148,6 +152,115 @@
     }
 
     @Test
+    fun getDesktopItemState_nullItemInfo_returnsNotRunning() {
+        setInDesktopMode(true)
+        assertThat(recentAppsController.getDesktopItemState(/* itemInfo= */ null))
+            .isEqualTo(RunningAppState.NOT_RUNNING)
+    }
+
+    @Test
+    fun getDesktopItemState_noItemPackage_returnsNotRunning() {
+        setInDesktopMode(true)
+        assertThat(recentAppsController.getDesktopItemState(ItemInfo()))
+            .isEqualTo(RunningAppState.NOT_RUNNING)
+    }
+
+    @Test
+    fun getDesktopItemState_noMatchingTasks_returnsNotRunning() {
+        setInDesktopMode(true)
+        val itemInfo = createItemInfo("package")
+        assertThat(recentAppsController.getDesktopItemState(itemInfo))
+            .isEqualTo(RunningAppState.NOT_RUNNING)
+    }
+
+    @Test
+    fun getDesktopItemState_matchingVisibleTask_returnsVisible() {
+        setInDesktopMode(true)
+        val visibleTask = createTask(id = 1, "visiblePackage", isVisible = true)
+        updateRecentTasks(runningTasks = listOf(visibleTask), recentTaskPackages = emptyList())
+        val itemInfo = createItemInfo("visiblePackage")
+
+        assertThat(recentAppsController.getDesktopItemState(itemInfo))
+            .isEqualTo(RunningAppState.RUNNING)
+    }
+
+    @Test
+    fun getDesktopItemState_matchingMinimizedTask_returnsMinimized() {
+        setInDesktopMode(true)
+        val minimizedTask = createTask(id = 1, "minimizedPackage", isVisible = false)
+        updateRecentTasks(runningTasks = listOf(minimizedTask), recentTaskPackages = emptyList())
+        val itemInfo = createItemInfo("minimizedPackage")
+
+        assertThat(recentAppsController.getDesktopItemState(itemInfo))
+            .isEqualTo(RunningAppState.MINIMIZED)
+    }
+
+    @Test
+    fun getDesktopItemState_matchingMinimizedAndRunningTask_returnsVisible() {
+        setInDesktopMode(true)
+        updateRecentTasks(
+            runningTasks =
+                listOf(
+                    createTask(id = 1, "package", isVisible = false),
+                    createTask(id = 2, "package", isVisible = true),
+                ),
+            recentTaskPackages = emptyList(),
+        )
+        val itemInfo = createItemInfo("package")
+
+        assertThat(recentAppsController.getDesktopItemState(itemInfo))
+            .isEqualTo(RunningAppState.RUNNING)
+    }
+
+    @Test
+    fun getDesktopItemState_noMatchingUserId_returnsNotRunning() {
+        setInDesktopMode(true)
+        updateRecentTasks(
+            runningTasks =
+                listOf(
+                    createTask(id = 1, "package", isVisible = false, USER_HANDLE_1),
+                    createTask(id = 2, "package", isVisible = true, USER_HANDLE_1),
+                ),
+            recentTaskPackages = emptyList(),
+        )
+        val itemInfo = createItemInfo("package", USER_HANDLE_2)
+
+        assertThat(recentAppsController.getDesktopItemState(itemInfo))
+            .isEqualTo(RunningAppState.NOT_RUNNING)
+    }
+
+    @Test
+    fun getRunningAppState_taskNotRunningOrMinimized_returnsNotRunning() {
+        setInDesktopMode(true)
+        updateRecentTasks(runningTasks = emptyList(), recentTaskPackages = emptyList())
+
+        assertThat(recentAppsController.getRunningAppState(taskId = 1))
+            .isEqualTo(RunningAppState.NOT_RUNNING)
+    }
+
+    @Test
+    fun getRunningAppState_taskNotVisible_returnsMinimized() {
+        setInDesktopMode(true)
+        val task1 = createTask(id = 1, packageName = RUNNING_APP_PACKAGE_1, isVisible = false)
+        val task2 = createTask(id = 2, packageName = RUNNING_APP_PACKAGE_1, isVisible = true)
+        updateRecentTasks(runningTasks = listOf(task1, task2), recentTaskPackages = emptyList())
+
+        assertThat(recentAppsController.getRunningAppState(taskId = 1))
+            .isEqualTo(RunningAppState.MINIMIZED)
+    }
+
+    @Test
+    fun getRunningAppState_taskVisible_returnsRunning() {
+        setInDesktopMode(true)
+        val task1 = createTask(id = 1, packageName = RUNNING_APP_PACKAGE_1, isVisible = false)
+        val task2 = createTask(id = 2, packageName = RUNNING_APP_PACKAGE_1, isVisible = true)
+        updateRecentTasks(runningTasks = listOf(task1, task2), recentTaskPackages = emptyList())
+
+        assertThat(recentAppsController.getRunningAppState(taskId = 2))
+            .isEqualTo(RunningAppState.RUNNING)
+    }
+
+    @Test
     fun updateHotseatItemInfos_cantShowRunning_inDesktopMode_returnsAllHotseatItems() {
         recentAppsController.canShowRunningApps = false
         setInDesktopMode(true)
@@ -782,7 +895,13 @@
     private fun createTestAppInfo(
         packageName: String = "testPackageName",
         className: String = "testClassName",
-    ) = AppInfo(ComponentName(packageName, className), className /* title */, userHandle, Intent())
+    ) =
+        AppInfo(
+            ComponentName(packageName, className),
+            className /* title */,
+            myUserHandle,
+            Intent(),
+        )
 
     private fun createRecentTasksFromPackageNames(packageNames: List<String>): List<GroupTask> {
         return packageNames.map { packageName ->
@@ -801,14 +920,19 @@
         }
     }
 
-    private fun createTask(id: Int, packageName: String, isVisible: Boolean = true): Task {
+    private fun createTask(
+        id: Int,
+        packageName: String,
+        isVisible: Boolean = true,
+        localUserHandle: UserHandle? = null,
+    ): Task {
         return Task(
                 Task.TaskKey(
                     id,
                     WINDOWING_MODE_FREEFORM,
                     Intent().apply { `package` = packageName },
                     ComponentName(packageName, "TestActivity"),
-                    userHandle.identifier,
+                    localUserHandle?.identifier ?: myUserHandle.identifier,
                     0,
                 )
             )
@@ -820,6 +944,16 @@
             .thenReturn(inDesktopMode)
     }
 
+    private fun createItemInfo(
+        packageName: String,
+        userHandle: UserHandle = myUserHandle,
+    ): ItemInfo {
+        val appInfo = AppInfo()
+        appInfo.intent = Intent().setComponent(ComponentName(packageName, "className"))
+        appInfo.user = userHandle
+        return WorkspaceItemInfo(appInfo)
+    }
+
     private val GroupTask.packageNames: List<String>
         get() = tasks.map { task -> task.key.packageName }
 
diff --git a/quickstep/tests/src/com/android/quickstep/AbstractQuickStepTest.java b/quickstep/tests/src/com/android/quickstep/AbstractQuickStepTest.java
index 44c23ba..6a7b6f8 100644
--- a/quickstep/tests/src/com/android/quickstep/AbstractQuickStepTest.java
+++ b/quickstep/tests/src/com/android/quickstep/AbstractQuickStepTest.java
@@ -26,6 +26,7 @@
 import com.android.launcher3.tapl.LaunchedAppState;
 import com.android.launcher3.ui.AbstractLauncherUiTest;
 import com.android.launcher3.uioverrides.QuickstepLauncher;
+import com.android.launcher3.util.TestUtil;
 import com.android.quickstep.views.RecentsView;
 
 import org.junit.rules.RuleChain;
@@ -56,7 +57,7 @@
     protected void assertTestActivityIsRunning(int activityNumber, String message) {
         assertTrue(message, mDevice.wait(
                 Until.hasObject(By.pkg(getAppPackageName()).text("TestActivity" + activityNumber)),
-                DEFAULT_UI_TIMEOUT));
+                TestUtil.DEFAULT_UI_TIMEOUT));
     }
 
     protected LaunchedAppState getAndAssertLaunchedApp() {
diff --git a/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt b/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt
index 885a7f6..231c113 100644
--- a/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt
+++ b/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt
@@ -24,6 +24,7 @@
 import com.android.dx.mockito.inline.extended.StaticMockitoSession
 import com.android.launcher3.AbstractFloatingView
 import com.android.launcher3.AbstractFloatingViewHelper
+import com.android.launcher3.Flags.enableRefactorTaskThumbnail
 import com.android.launcher3.logging.StatsLogManager
 import com.android.launcher3.logging.StatsLogManager.LauncherEvent
 import com.android.launcher3.model.data.WorkspaceItemInfo
@@ -31,6 +32,7 @@
 import com.android.launcher3.util.SplitConfigurationOptions
 import com.android.launcher3.util.TransformingTouchDelegate
 import com.android.quickstep.TaskOverlayFactory.TaskOverlay
+import com.android.quickstep.task.thumbnail.TaskThumbnailView
 import com.android.quickstep.views.LauncherRecentsView
 import com.android.quickstep.views.TaskContainer
 import com.android.quickstep.views.TaskThumbnailViewDeprecated
@@ -67,7 +69,6 @@
     private val taskView: TaskView = mock()
     private val workspaceItemInfo: WorkspaceItemInfo = mock()
     private val abstractFloatingViewHelper: AbstractFloatingViewHelper = mock()
-    private val thumbnailViewDeprecated: TaskThumbnailViewDeprecated = mock()
     private val iconView: TaskViewIcon = mock()
     private val transformingTouchDelegate: TransformingTouchDelegate = mock()
     private val factory: TaskShortcutFactory =
@@ -175,7 +176,7 @@
             .moveTaskToDesktop(
                 eq(taskContainer),
                 eq(DesktopModeTransitionSource.APP_FROM_OVERVIEW),
-                any()
+                any(),
             )
         verify(statsLogger).withItemInfo(workspaceItemInfo)
         verify(statsLogger).log(LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_DESKTOP_TAP)
@@ -188,16 +189,19 @@
     }
 
     private fun createTaskContainer(task: Task): TaskContainer {
+        val snapshotView =
+            if (enableRefactorTaskThumbnail()) mock<TaskThumbnailView>()
+            else mock<TaskThumbnailViewDeprecated>()
         return TaskContainer(
             taskView,
             task,
-            thumbnailViewDeprecated,
+            snapshotView,
             iconView,
             transformingTouchDelegate,
             SplitConfigurationOptions.STAGE_POSITION_UNDEFINED,
             digitalWellBeingToast = null,
             showWindowsView = null,
-            overlayFactory
+            overlayFactory,
         )
     }
 }
diff --git a/quickstep/tests/src/com/android/quickstep/TaplDigitalWellBeingToastTest.java b/quickstep/tests/src/com/android/quickstep/DigitalWellBeingToastTest.java
similarity index 73%
rename from quickstep/tests/src/com/android/quickstep/TaplDigitalWellBeingToastTest.java
rename to quickstep/tests/src/com/android/quickstep/DigitalWellBeingToastTest.java
index e981570..5b46dc8 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplDigitalWellBeingToastTest.java
+++ b/quickstep/tests/src/com/android/quickstep/DigitalWellBeingToastTest.java
@@ -15,7 +15,9 @@
  */
 package com.android.quickstep;
 
-import static androidx.test.InstrumentationRegistry.getInstrumentation;
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static com.android.launcher3.util.TestUtil.resolveSystemAppInfo;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -25,10 +27,13 @@
 import android.app.usage.UsageStatsManager;
 import android.content.Intent;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.LargeTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.uioverrides.QuickstepLauncher;
+import com.android.launcher3.util.BaseLauncherActivityTest;
 import com.android.quickstep.views.DigitalWellBeingToast;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskContainer;
@@ -41,30 +46,31 @@
 
 @LargeTest
 @RunWith(AndroidJUnit4.class)
-public class TaplDigitalWellBeingToastTest extends AbstractQuickStepTest {
-    private static final String CALCULATOR_PACKAGE =
-            resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR);
+public class DigitalWellBeingToastTest extends BaseLauncherActivityTest<QuickstepLauncher> {
+
+    public final String calculatorPackage =
+            resolveSystemAppInfo(Intent.CATEGORY_APP_CALCULATOR).packageName;
 
     @Test
-    public void testToast() throws Exception {
-        startAppFast(CALCULATOR_PACKAGE);
+    public void testToast() {
+        startAppFast(calculatorPackage);
 
         final UsageStatsManager usageStatsManager =
-                mTargetContext.getSystemService(UsageStatsManager.class);
+                targetContext().getSystemService(UsageStatsManager.class);
         final int observerId = 0;
 
         try {
-            final String[] packages = new String[]{CALCULATOR_PACKAGE};
+            final String[] packages = new String[]{calculatorPackage};
 
             // Set time limit for app.
             runWithShellPermission(() ->
                     usageStatsManager.registerAppUsageLimitObserver(observerId, packages,
                             Duration.ofSeconds(600), Duration.ofSeconds(300),
-                            PendingIntent.getActivity(mTargetContext, -1, new Intent()
-                                            .setPackage(mTargetContext.getPackageName()),
+                            PendingIntent.getActivity(targetContext(), -1, new Intent()
+                                            .setPackage(targetContext().getPackageName()),
                                     PendingIntent.FLAG_MUTABLE)));
 
-            mLauncher.goHome();
+            loadLauncherSync();
             final DigitalWellBeingToast toast = getToast();
 
             waitForLauncherCondition("Toast is not visible", launcher -> toast.getHasLimit());
@@ -74,7 +80,7 @@
             runWithShellPermission(
                     () -> usageStatsManager.unregisterAppUsageLimitObserver(observerId));
 
-            mLauncher.goHome();
+            goToState(LauncherState.NORMAL);
             assertFalse("Toast is visible", getToast().getHasLimit());
         } finally {
             runWithShellPermission(
@@ -83,12 +89,12 @@
     }
 
     private DigitalWellBeingToast getToast() {
-        mLauncher.getWorkspace().switchToOverview();
+        goToState(LauncherState.OVERVIEW);
         final TaskView task = getOnceNotNull("No latest task", launcher -> getLatestTask(launcher));
 
         return getFromLauncher(launcher -> {
             TaskContainer taskContainer = task.getTaskContainers().get(0);
-            assertTrue("Latest task is not Calculator", CALCULATOR_PACKAGE.equals(
+            assertTrue("Latest task is not Calculator", calculatorPackage.equals(
                     taskContainer.getTask().getTopComponent().getPackageName()));
             return taskContainer.getDigitalWellBeingToast();
         });
@@ -105,6 +111,5 @@
         } finally {
             getInstrumentation().getUiAutomation().dropShellPermissionIdentity();
         }
-
     }
 }
diff --git a/quickstep/tests/src/com/android/quickstep/ExternalDisplaySystemShortcutTest.kt b/quickstep/tests/src/com/android/quickstep/ExternalDisplaySystemShortcutTest.kt
new file mode 100644
index 0000000..8968b9c
--- /dev/null
+++ b/quickstep/tests/src/com/android/quickstep/ExternalDisplaySystemShortcutTest.kt
@@ -0,0 +1,193 @@
+/*
+ * 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
+
+import android.content.ComponentName
+import android.content.Intent
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
+import com.android.dx.mockito.inline.extended.ExtendedMockito
+import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
+import com.android.dx.mockito.inline.extended.StaticMockitoSession
+import com.android.launcher3.AbstractFloatingView
+import com.android.launcher3.AbstractFloatingViewHelper
+import com.android.launcher3.Flags.enableRefactorTaskThumbnail
+import com.android.launcher3.logging.StatsLogManager
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent
+import com.android.launcher3.model.data.WorkspaceItemInfo
+import com.android.launcher3.uioverrides.QuickstepLauncher
+import com.android.launcher3.util.SplitConfigurationOptions
+import com.android.launcher3.util.TransformingTouchDelegate
+import com.android.quickstep.TaskOverlayFactory.TaskOverlay
+import com.android.quickstep.task.thumbnail.TaskThumbnailView
+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
+import com.android.systemui.shared.recents.model.Task
+import com.android.systemui.shared.recents.model.Task.TaskKey
+import com.android.window.flags.Flags
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+import org.mockito.quality.Strictness
+
+/** Test for ExternalDisplaySystemShortcut */
+class ExternalDisplaySystemShortcutTest {
+
+    @get:Rule val setFlagsRule = SetFlagsRule(SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT)
+
+    private val launcher: QuickstepLauncher = mock()
+    private val statsLogManager: StatsLogManager = mock()
+    private val statsLogger: StatsLogManager.StatsLogger = mock()
+    private val recentsView: LauncherRecentsView = mock()
+    private val taskView: TaskView = mock()
+    private val workspaceItemInfo: WorkspaceItemInfo = mock()
+    private val abstractFloatingViewHelper: AbstractFloatingViewHelper = mock()
+    private val iconView: TaskViewIcon = mock()
+    private val transformingTouchDelegate: TransformingTouchDelegate = mock()
+    private val factory: TaskShortcutFactory =
+        ExternalDisplaySystemShortcut.createFactory(abstractFloatingViewHelper)
+    private val overlayFactory: TaskOverlayFactory = mock()
+    private val overlay: TaskOverlay<*> = mock()
+
+    private lateinit var mockitoSession: StaticMockitoSession
+
+    @Before
+    fun setUp() {
+        mockitoSession =
+            mockitoSession()
+                .strictness(Strictness.LENIENT)
+                .spyStatic(DesktopModeStatus::class.java)
+                .startMocking()
+        ExtendedMockito.doReturn(true).`when` { DesktopModeStatus.enforceDeviceRestrictions() }
+        ExtendedMockito.doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+        whenever(overlayFactory.createOverlay(any())).thenReturn(overlay)
+    }
+
+    @After
+    fun tearDown() {
+        mockitoSession.finishMocking()
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+    @EnableFlags(Flags.FLAG_MOVE_TO_EXTERNAL_DISPLAY_SHORTCUT)
+    fun createExternalDisplayTaskShortcut_desktopModeDisabled() {
+        val task = createTask()
+        val taskContainer = createTaskContainer(task)
+
+        val shortcuts = factory.getShortcuts(launcher, taskContainer)
+        assertThat(shortcuts).isNull()
+    }
+
+    @Test
+    @EnableFlags(
+        Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+        Flags.FLAG_MOVE_TO_EXTERNAL_DISPLAY_SHORTCUT,
+    )
+    fun createExternalDisplayTaskShortcut_desktopModeEnabled_deviceNotSupported() {
+        ExtendedMockito.doReturn(false).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+
+        val taskContainer = createTaskContainer(createTask())
+
+        val shortcuts = factory.getShortcuts(launcher, taskContainer)
+        assertThat(shortcuts).isNull()
+    }
+
+    @Test
+    @EnableFlags(
+        Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+        Flags.FLAG_MOVE_TO_EXTERNAL_DISPLAY_SHORTCUT,
+    )
+    fun createExternalDisplayTaskShortcut_desktopModeEnabled_deviceNotSupported_overrideEnabled() {
+        ExtendedMockito.doReturn(false).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+        ExtendedMockito.doReturn(false).`when` { DesktopModeStatus.enforceDeviceRestrictions() }
+
+        val taskContainer = spy(createTaskContainer(createTask()))
+        doReturn(workspaceItemInfo).whenever(taskContainer).itemInfo
+
+        val shortcuts = factory.getShortcuts(launcher, taskContainer)
+        assertThat(shortcuts).isNotNull()
+    }
+
+    @Test
+    @EnableFlags(
+        Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+        Flags.FLAG_MOVE_TO_EXTERNAL_DISPLAY_SHORTCUT,
+    )
+    fun externalDisplaySystemShortcutClicked() {
+        val task = createTask()
+        val taskContainer = spy(createTaskContainer(task))
+
+        whenever(launcher.getOverviewPanel<LauncherRecentsView>()).thenReturn(recentsView)
+        whenever(launcher.statsLogManager).thenReturn(statsLogManager)
+        whenever(statsLogManager.logger()).thenReturn(statsLogger)
+        whenever(statsLogger.withItemInfo(any())).thenReturn(statsLogger)
+        whenever(recentsView.moveTaskToExternalDisplay(any(), any())).thenAnswer {
+            val successCallback = it.getArgument<Runnable>(1)
+            successCallback.run()
+        }
+        doReturn(workspaceItemInfo).whenever(taskContainer).itemInfo
+
+        val shortcuts = factory.getShortcuts(launcher, taskContainer)
+        assertThat(shortcuts).hasSize(1)
+        assertThat(shortcuts!!.first()).isInstanceOf(ExternalDisplaySystemShortcut::class.java)
+
+        val externalDisplayShortcut = shortcuts.first() as ExternalDisplaySystemShortcut
+
+        externalDisplayShortcut.onClick(taskView)
+
+        val allTypesExceptRebindSafe =
+            AbstractFloatingView.TYPE_ALL and AbstractFloatingView.TYPE_REBIND_SAFE.inv()
+        verify(abstractFloatingViewHelper).closeOpenViews(launcher, true, allTypesExceptRebindSafe)
+        verify(recentsView).moveTaskToExternalDisplay(eq(taskContainer), any())
+        verify(statsLogger).withItemInfo(workspaceItemInfo)
+        verify(statsLogger).log(LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_EXTERNAL_DISPLAY_TAP)
+    }
+
+    private fun createTask(): Task = Task(TaskKey(1, 0, Intent(), ComponentName("", ""), 0, 2000))
+
+    private fun createTaskContainer(task: Task): TaskContainer {
+        val snapshotView =
+            if (enableRefactorTaskThumbnail()) mock<TaskThumbnailView>()
+            else mock<TaskThumbnailViewDeprecated>()
+        return TaskContainer(
+            taskView,
+            task,
+            snapshotView,
+            iconView,
+            transformingTouchDelegate,
+            SplitConfigurationOptions.STAGE_POSITION_UNDEFINED,
+            digitalWellBeingToast = null,
+            showWindowsView = null,
+            overlayFactory,
+        )
+    }
+}
diff --git a/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java b/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
index 2858929..aa105f9 100644
--- a/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
+++ b/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
@@ -22,9 +22,7 @@
 import static com.android.launcher3.tapl.LauncherInstrumentation.WAIT_TIME_MS;
 import static com.android.launcher3.tapl.TestHelpers.getHomeIntentInPackage;
 import static com.android.launcher3.tapl.TestHelpers.getLauncherInMyProcess;
-import static com.android.launcher3.ui.AbstractLauncherUiTest.DEFAULT_ACTIVITY_TIMEOUT;
 import static com.android.launcher3.ui.AbstractLauncherUiTest.DEFAULT_BROADCAST_TIMEOUT_SECS;
-import static com.android.launcher3.ui.AbstractLauncherUiTest.DEFAULT_UI_TIMEOUT;
 import static com.android.launcher3.ui.AbstractLauncherUiTest.resolveSystemApp;
 import static com.android.launcher3.ui.AbstractLauncherUiTest.startAppFast;
 import static com.android.launcher3.ui.AbstractLauncherUiTest.startTestActivity;
@@ -56,6 +54,7 @@
 import com.android.launcher3.tapl.TestHelpers;
 import com.android.launcher3.testcomponent.TestCommandReceiver;
 import com.android.launcher3.ui.AbstractLauncherUiTest;
+import com.android.launcher3.util.TestUtil;
 import com.android.launcher3.util.Wait;
 import com.android.launcher3.util.rule.ExtendedLongPressTimeoutRule;
 import com.android.launcher3.util.rule.FailureWatcher;
@@ -142,7 +141,7 @@
         };
 
         final ViewCaptureRule viewCaptureRule = new ViewCaptureRule(
-                RecentsActivity.ACTIVITY_TRACKER::getCreatedActivity);
+                RecentsActivity.ACTIVITY_TRACKER::getCreatedContext);
         mOrderSensitiveRules = RuleChain
                 .outerRule(new SamplerRule())
                 .around(new TestStabilityRule())
@@ -208,13 +207,13 @@
         if (!TestHelpers.isInLauncherProcess()) return null;
         Object[] result = new Object[1];
         Wait.atMost("Failed to get from recents", () -> MAIN_EXECUTOR.submit(() -> {
-            RecentsActivity activity = RecentsActivity.ACTIVITY_TRACKER.getCreatedActivity();
+            RecentsActivity activity = RecentsActivity.ACTIVITY_TRACKER.getCreatedContext();
             if (activity == null) {
                 return false;
             }
             result[0] = f.apply(activity);
             return true;
-        }).get(), DEFAULT_UI_TIMEOUT, mLauncher);
+        }).get(), mLauncher);
         return (T) result[0];
     }
 
@@ -231,7 +230,7 @@
     private void waitForRecentsActivityStop() {
         try {
             final boolean recentsActivityIsNull = MAIN_EXECUTOR.submit(
-                    () -> RecentsActivity.ACTIVITY_TRACKER.getCreatedActivity() == null).get();
+                    () -> RecentsActivity.ACTIVITY_TRACKER.getCreatedContext() == null).get();
             if (recentsActivityIsNull) {
                 // Null activity counts as a "stopped" one.
                 return;
@@ -244,7 +243,7 @@
 
         Wait.atMost("Recents activity didn't stop",
                 () -> getFromRecents(recents -> !recents.isStarted()),
-                DEFAULT_UI_TIMEOUT, mLauncher);
+                mLauncher);
     }
 
     @Test
@@ -254,7 +253,8 @@
         startTestActivity(2);
         waitForRecentsActivityStop();
         Wait.atMost("Expected three apps in the task list",
-                () -> mLauncher.getRecentTasks().size() >= 3, DEFAULT_ACTIVITY_TIMEOUT, mLauncher);
+                () -> mLauncher.getRecentTasks().size() >= 3,
+                mLauncher);
 
         checkTestLauncher();
         BaseOverview overview = mLauncher.getLaunchedAppState().switchToOverview();
@@ -282,7 +282,7 @@
         assertNotNull("OverviewTask.open returned null", task.open());
         assertTrue("Test activity didn't open from Overview", TestHelpers.wait(Until.hasObject(
                 By.pkg(getAppPackageName()).text("TestActivity2")),
-                DEFAULT_UI_TIMEOUT));
+                TestUtil.DEFAULT_UI_TIMEOUT));
 
 
         // Test dismissing a task.
diff --git a/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java b/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java
index 4459ed6..77f4c05 100644
--- a/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java
+++ b/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java
@@ -57,8 +57,6 @@
 
     static final String TAG = "QuickStepOnOffRule";
 
-    public static final int WAIT_TIME_MS = 10000;
-
     public enum Mode {
         THREE_BUTTON, ZERO_BUTTON, ALL
     }
@@ -179,12 +177,13 @@
         }
 
         Wait.atMost("Couldn't switch to " + overlayPackage,
-                () -> launcher.getNavigationModel() == expectedMode, WAIT_TIME_MS, launcher);
+                () -> launcher.getNavigationModel() == expectedMode,
+                launcher);
 
         Wait.atMost(() -> "Switching nav mode: "
                         + launcher.getNavigationModeMismatchError(false),
                 () -> launcher.getNavigationModeMismatchError(false) == null,
-                WAIT_TIME_MS, launcher);
+                launcher);
         AbstractLauncherUiTest.checkDetectedLeaks(launcher, false);
         return true;
     }
diff --git a/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java b/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java
index 244b897..b3c486c 100644
--- a/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java
+++ b/quickstep/tests/src/com/android/quickstep/RecentTasksListTest.java
@@ -28,7 +28,9 @@
 import static org.mockito.Mockito.when;
 
 import android.app.ActivityManager;
+import android.app.ActivityManager.RecentTaskInfo;
 import android.app.KeyguardManager;
+import android.app.TaskInfo;
 import android.content.Context;
 import android.content.res.Resources;
 
@@ -39,7 +41,7 @@
 import com.android.quickstep.util.GroupTask;
 import com.android.quickstep.views.TaskViewType;
 import com.android.systemui.shared.recents.model.Task;
-import com.android.wm.shell.shared.GroupedRecentTaskInfo;
+import com.android.wm.shell.shared.GroupedTaskInfo;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -91,8 +93,8 @@
 
     @Test
     public void loadTasksInBackground_onlyKeys_noValidTaskDescription() throws Exception  {
-        GroupedRecentTaskInfo recentTaskInfos = GroupedRecentTaskInfo.forSplitTasks(
-                new ActivityManager.RecentTaskInfo(), new ActivityManager.RecentTaskInfo(), null);
+        GroupedTaskInfo recentTaskInfos = GroupedTaskInfo.forSplitTasks(
+                new RecentTaskInfo(), new RecentTaskInfo(), null);
         when(mSystemUiProxy.getRecentTasks(anyInt(), anyInt()))
                 .thenReturn(new ArrayList<>(Collections.singletonList(recentTaskInfos)));
 
@@ -119,12 +121,11 @@
     @Test
     public void loadTasksInBackground_moreThanKeys_hasValidTaskDescription() throws Exception  {
         String taskDescription = "Wheeee!";
-        ActivityManager.RecentTaskInfo task1 = new ActivityManager.RecentTaskInfo();
+        RecentTaskInfo task1 = new RecentTaskInfo();
         task1.taskDescription = new ActivityManager.TaskDescription(taskDescription);
-        ActivityManager.RecentTaskInfo task2 = new ActivityManager.RecentTaskInfo();
+        RecentTaskInfo task2 = new RecentTaskInfo();
         task2.taskDescription = new ActivityManager.TaskDescription();
-        GroupedRecentTaskInfo recentTaskInfos = GroupedRecentTaskInfo.forSplitTasks(task1, task2,
-                null);
+        GroupedTaskInfo recentTaskInfos = GroupedTaskInfo.forSplitTasks(task1, task2, null);
         when(mSystemUiProxy.getRecentTasks(anyInt(), anyInt()))
                 .thenReturn(new ArrayList<>(Collections.singletonList(recentTaskInfos)));
 
@@ -138,11 +139,11 @@
 
     @Test
     public void loadTasksInBackground_freeformTask_createsDesktopTask() throws Exception  {
-        ActivityManager.RecentTaskInfo[] tasks = {
+        List<TaskInfo> tasks = Arrays.asList(
                 createRecentTaskInfo(1 /* taskId */),
                 createRecentTaskInfo(4 /* taskId */),
-                createRecentTaskInfo(5 /* taskId */)};
-        GroupedRecentTaskInfo recentTaskInfos = GroupedRecentTaskInfo.forFreeformTasks(
+                createRecentTaskInfo(5 /* taskId */));
+        GroupedTaskInfo recentTaskInfos = GroupedTaskInfo.forFreeformTasks(
                 tasks, Collections.emptySet() /* minimizedTaskIds */);
         when(mSystemUiProxy.getRecentTasks(anyInt(), anyInt()))
                 .thenReturn(new ArrayList<>(Collections.singletonList(recentTaskInfos)));
@@ -162,14 +163,13 @@
     @Test
     public void loadTasksInBackground_freeformTask_onlyMinimizedTasks_doesNotCreateDesktopTask()
             throws Exception {
-        ActivityManager.RecentTaskInfo[] tasks = {
+        List<TaskInfo> tasks = Arrays.asList(
                 createRecentTaskInfo(1 /* taskId */),
                 createRecentTaskInfo(4 /* taskId */),
-                createRecentTaskInfo(5 /* taskId */)};
+                createRecentTaskInfo(5 /* taskId */));
         Set<Integer> minimizedTaskIds =
                 Arrays.stream(new Integer[]{1, 4, 5}).collect(Collectors.toSet());
-        GroupedRecentTaskInfo recentTaskInfos =
-                GroupedRecentTaskInfo.forFreeformTasks(tasks, minimizedTaskIds);
+        GroupedTaskInfo recentTaskInfos = GroupedTaskInfo.forFreeformTasks(tasks, minimizedTaskIds);
         when(mSystemUiProxy.getRecentTasks(anyInt(), anyInt()))
                 .thenReturn(new ArrayList<>(Collections.singletonList(recentTaskInfos)));
 
@@ -179,8 +179,8 @@
         assertEquals(0, taskList.size());
     }
 
-    private ActivityManager.RecentTaskInfo createRecentTaskInfo(int taskId) {
-        ActivityManager.RecentTaskInfo recentTaskInfo = new ActivityManager.RecentTaskInfo();
+    private TaskInfo createRecentTaskInfo(int taskId) {
+        RecentTaskInfo recentTaskInfo = new RecentTaskInfo();
         recentTaskInfo.taskId = taskId;
         return recentTaskInfo;
     }
diff --git a/quickstep/tests/src/com/android/quickstep/TaplOverviewIconTest.java b/quickstep/tests/src/com/android/quickstep/TaplOverviewIconTest.java
index 9bc1c59..2c275f4 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplOverviewIconTest.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplOverviewIconTest.java
@@ -22,7 +22,7 @@
 import android.platform.test.annotations.PlatinumTest;
 
 import com.android.launcher3.tapl.Overview;
-import com.android.launcher3.tapl.OverviewTask.OverviewSplitTask;
+import com.android.launcher3.tapl.OverviewTask.OverviewTaskContainer;
 import com.android.launcher3.tapl.OverviewTaskMenu;
 import com.android.launcher3.ui.AbstractLauncherUiTest;
 import com.android.launcher3.uioverrides.QuickstepLauncher;
@@ -76,7 +76,7 @@
         taskMenu.touchOutsideTaskMenuToDismiss();
 
         OverviewTaskMenu splitMenu = overview.getCurrentTask().tapMenu(
-                        OverviewSplitTask.SPLIT_BOTTOM_OR_RIGHT);
+                        OverviewTaskContainer.SPLIT_BOTTOM_OR_RIGHT);
         assertTrue("App info item not appearing in expanded split task's menu.",
                 splitMenu.hasMenuItem("App info"));
         splitMenu.touchOutsideTaskMenuToDismiss();
diff --git a/quickstep/tests/src/com/android/quickstep/TaplPrivateSpaceTest.java b/quickstep/tests/src/com/android/quickstep/TaplPrivateSpaceTest.java
index 800fd4a..b15b78e 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplPrivateSpaceTest.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplPrivateSpaceTest.java
@@ -56,8 +56,6 @@
     @Override
     public void setUp() throws Exception {
         super.setUp();
-        initialize(this);
-
         createAndStartPrivateProfileUser();
 
         mDevice.pressHome();
diff --git a/quickstep/tests/src/com/android/quickstep/TaplStartLauncherViaGestureTests.java b/quickstep/tests/src/com/android/quickstep/TaplStartLauncherViaGestureTests.java
index a8f39af..2fb08dd 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplStartLauncherViaGestureTests.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplStartLauncherViaGestureTests.java
@@ -16,6 +16,8 @@
 
 package com.android.quickstep;
 
+import android.util.Log;
+
 import androidx.test.filters.LargeTest;
 import androidx.test.runner.AndroidJUnit4;
 
@@ -29,6 +31,8 @@
 @RunWith(AndroidJUnit4.class)
 public class TaplStartLauncherViaGestureTests extends AbstractQuickStepTest {
 
+    public static final String TAG = "TaplStartLauncherViaGestureTests";
+
     static final int STRESS_REPEAT_COUNT = 10;
 
     private enum TestCase {
@@ -69,7 +73,9 @@
     }
 
     private void runTest(TestCase testCase) {
+        long testStartTime = System.currentTimeMillis();
         for (int i = 0; i < STRESS_REPEAT_COUNT; ++i) {
+            long loopStartTime = System.currentTimeMillis();
             // Destroy Launcher activity.
             closeLauncherActivity();
 
@@ -84,7 +90,10 @@
                 default:
                     throw new IllegalStateException("Cannot run test case: " + testCase);
             }
+            Log.d(TAG, "Loop " + (i + 1) + " runtime="
+                    + (System.currentTimeMillis() - loopStartTime) + "ms");
         }
+        Log.d(TAG, "Test runtime=" + (System.currentTimeMillis() - testStartTime) + "ms");
         switch (testCase) {
             case TO_OVERVIEW:
                 closeLauncherActivity();
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsKeyboardQuickSwitch.java b/quickstep/tests/src/com/android/quickstep/TaplTestsKeyboardQuickSwitch.java
index 43ebb17..3c4f1d9 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsKeyboardQuickSwitch.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsKeyboardQuickSwitch.java
@@ -49,6 +49,7 @@
         DISMISS(0),
         LAUNCH_LAST_APP(0),
         LAUNCH_SELECTED_APP(1),
+        DISMISS_WHEN_GOING_HOME(1),
         LAUNCH_OVERVIEW(KeyboardQuickSwitchController.MAX_TASKS - 1);
 
         private final int mNumAdditionalRunningTasks;
@@ -156,6 +157,11 @@
         mLauncher.goHome().showQuickSwitchView().launchFocusedAppTask(CALCULATOR_APP_PACKAGE);
     }
 
+    @Test
+    public void testDismissedWhenGoingHome() {
+        runTest(TestSurface.LAUNCHED_APP, TestCase.DISMISS_WHEN_GOING_HOME);
+    }
+
     private void runTest(@NonNull TestSurface testSurface, @NonNull TestCase testCase) {
         for (int i = 0; i < testCase.mNumAdditionalRunningTasks; i++) {
             startTestActivity(3 + i);
@@ -197,6 +203,9 @@
                 }
                 kqs.launchFocusedAppTask(CALCULATOR_APP_PACKAGE);
                 break;
+            case DISMISS_WHEN_GOING_HOME:
+                kqs.dismissByGoingHome();
+                break;
             case LAUNCH_OVERVIEW:
                 kqs.moveFocusBackward();
                 if (!testSurface.mInitialFocusAtZero) {
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsOverviewDesktop.kt b/quickstep/tests/src/com/android/quickstep/TaplTestsOverviewDesktop.kt
index 694a382..f58c84e 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsOverviewDesktop.kt
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsOverviewDesktop.kt
@@ -21,9 +21,12 @@
 import androidx.test.uiautomator.By
 import androidx.test.uiautomator.Until
 import com.android.launcher3.BuildConfig
+import com.android.launcher3.tapl.LaunchedAppState
+import com.android.launcher3.tapl.OverviewTask
 import com.android.launcher3.ui.AbstractLauncherUiTest
 import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape
 import com.android.launcher3.uioverrides.QuickstepLauncher
+import com.android.launcher3.util.TestUtil
 import com.google.common.truth.Truth.assertWithMessage
 import org.junit.Before
 import org.junit.Test
@@ -45,22 +48,16 @@
     @Test
     @PortraitLandscape
     fun enterDesktopViaOverviewMenu() {
-        // Move last launched TEST_ACTIVITY_2 into Desktop
-        mLauncher.workspace
-            .switchToOverview()
-            .getTestActivityTask(TEST_ACTIVITY_2)
-            .tapMenu()
-            .tapDesktopMenuItem()
-        assertTestAppLaunched(TEST_ACTIVITY_2)
+        mLauncher.workspace.switchToOverview()
+        moveTaskToDesktop(TEST_ACTIVITY_2) // Move last launched TEST_ACTIVITY_2 into Desktop
 
         // Scroll back to TEST_ACTIVITY_1, then move it into Desktop
         mLauncher
             .goHome()
             .switchToOverview()
             .apply { flingForward() }
-            .getTestActivityTask(TEST_ACTIVITY_1)
-            .tapMenu()
-            .tapDesktopMenuItem()
+            .also { moveTaskToDesktop(TEST_ACTIVITY_1) }
+
         TEST_ACTIVITIES.forEach { assertTestAppLaunched(it) }
 
         // Launch static DesktopTaskView
@@ -73,6 +70,91 @@
         TEST_ACTIVITIES.forEach { assertTestAppLaunched(it) }
     }
 
+    @Test
+    @PortraitLandscape
+    fun dismissFocusedTasks_thenDesktopIsCentered() {
+        // Create DesktopTaskView
+        mLauncher.goHome().switchToOverview()
+        moveTaskToDesktop(TEST_ACTIVITY_2)
+
+        // Create a new task activity to be the focused task
+        mLauncher.goHome()
+        startTestActivity(TEST_ACTIVITY_EXTRA)
+
+        val overview = mLauncher.goHome().switchToOverview()
+
+        // Dismiss focused task
+        val focusedTask1 = overview.currentTask
+        assertTaskContentDescription(focusedTask1, TEST_ACTIVITY_EXTRA)
+        focusedTask1.dismiss()
+
+        // Dismiss new focused task
+        val focusedTask2 = overview.currentTask
+        assertTaskContentDescription(focusedTask2, TEST_ACTIVITY_1)
+        focusedTask2.dismiss()
+
+        // Dismiss DesktopTaskView
+        val desktopTask = overview.currentTask
+        assertWithMessage("The current task is not a Desktop.").that(desktopTask.isDesktop).isTrue()
+        desktopTask.dismiss()
+
+        assertWithMessage("Still have tasks after dismissing all the tasks")
+            .that(mLauncher.workspace.switchToOverview().hasTasks())
+            .isFalse()
+    }
+
+    @Test
+    @PortraitLandscape
+    fun dismissTasks_whenDesktopTask_IsInTheCenter() {
+        // Create extra activity to be DesktopTaskView
+        startTestActivity(TEST_ACTIVITY_EXTRA)
+        mLauncher.goHome().switchToOverview()
+
+        val desktop = moveTaskToDesktop(TEST_ACTIVITY_EXTRA)
+        var overview = desktop.switchToOverview()
+
+        // Open focused task and go back to Overview to validate whether it has adjacent tasks in
+        // its both sides (grid task on left and desktop tasks at its right side)
+        val focusedTaskOpened = overview.getTestActivityTask(TEST_ACTIVITY_2).open()
+
+        // Fling to desktop task and dismiss the focused task to check repositioning of
+        // grid tasks.
+        overview = focusedTaskOpened.switchToOverview().apply { flingBackward() }
+        val desktopTask = overview.currentTask
+        assertWithMessage("The current task is not a Desktop.").that(desktopTask.isDesktop).isTrue()
+
+        // Get focused task (previously opened task) then dismiss this task
+        val focusedTaskInOverview = overview.getTestActivityTask(TEST_ACTIVITY_2)
+        assertTaskContentDescription(focusedTaskInOverview, TEST_ACTIVITY_2)
+        focusedTaskInOverview.dismiss()
+
+        // Dismiss DesktopTask to validate whether the new focused task will take its position
+        desktopTask.dismiss()
+
+        // Dismiss last focused task
+        val lastFocusedTask = overview.currentTask
+        assertTaskContentDescription(lastFocusedTask, TEST_ACTIVITY_1)
+        lastFocusedTask.dismiss()
+
+        assertWithMessage("Still have tasks after dismissing all the tasks")
+            .that(mLauncher.workspace.switchToOverview().hasTasks())
+            .isFalse()
+    }
+
+    private fun assertTaskContentDescription(task: OverviewTask, activityIndex: Int) {
+        assertWithMessage("The current task content description is not TestActivity$activityIndex.")
+            .that(task.containsContentDescription("TestActivity$activityIndex"))
+            .isTrue()
+    }
+
+    private fun moveTaskToDesktop(activityIndex: Int): LaunchedAppState {
+        return mLauncher.overview
+            .getTestActivityTask(activityIndex)
+            .tapMenu()
+            .tapDesktopMenuItem()
+            .also { assertTestAppLaunched(activityIndex) }
+    }
+
     private fun startTestAppsWithCheck() {
         TEST_ACTIVITIES.forEach {
             startTestActivity(it)
@@ -91,7 +173,7 @@
             .that(
                 mDevice.wait(
                     Until.hasObject(By.pkg(getAppPackageName()).text("TestActivity$index")),
-                    DEFAULT_UI_TIMEOUT
+                    TestUtil.DEFAULT_UI_TIMEOUT,
                 )
             )
             .isTrue()
@@ -100,6 +182,7 @@
     companion object {
         const val TEST_ACTIVITY_1 = 2
         const val TEST_ACTIVITY_2 = 3
+        const val TEST_ACTIVITY_EXTRA = 4
         val TEST_ACTIVITIES = listOf(TEST_ACTIVITY_1, TEST_ACTIVITY_2)
     }
 }
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
index 113b8a4..f1fe2d2 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
@@ -47,6 +47,7 @@
 import com.android.launcher3.tapl.SelectModeButtons;
 import com.android.launcher3.tapl.Workspace;
 import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
+import com.android.launcher3.util.TestUtil;
 import com.android.launcher3.util.Wait;
 import com.android.launcher3.util.rule.ScreenRecordRule.ScreenRecord;
 import com.android.launcher3.util.rule.TestStabilityRule;
@@ -145,7 +146,7 @@
         assertNotNull("OverviewTask.open returned null", task.open());
         assertTrue("Test activity didn't open from Overview", mDevice.wait(Until.hasObject(
                         By.pkg(getAppPackageName()).text("TestActivity2")),
-                DEFAULT_UI_TIMEOUT));
+                TestUtil.DEFAULT_UI_TIMEOUT));
         executeOnLauncher(launcher -> assertTrue(
                 "Launcher activity is the top activity; expecting another activity to be the top "
                         + "one",
@@ -395,7 +396,6 @@
     @Test
     @NavigationModeSwitch
     @PortraitLandscape
-    @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/325659406
     public void testQuickSwitchFromHome() throws Exception {
         startTestActivity(2);
         mLauncher.goHome().quickSwitchToPreviousApp();
@@ -449,7 +449,7 @@
                 mDevice.wait(Until.hasObject(By.pkg(getAppPackageName()).text(
                                 mLauncher.isGridOnlyOverviewEnabled() ? "TestActivity12"
                                         : "TestActivity13")),
-                        DEFAULT_UI_TIMEOUT));
+                        TestUtil.DEFAULT_UI_TIMEOUT));
 
         // Scroll the task offscreen as it is now first
         overview = mLauncher.goHome().switchToOverview();
@@ -564,7 +564,7 @@
             mLauncher.getDevice().setOrientationLeft();
             startTestActivity(7);
             Wait.atMost("Device should not be in natural orientation",
-                    () -> !mDevice.isNaturalOrientation(), DEFAULT_UI_TIMEOUT, mLauncher);
+                    () -> !mDevice.isNaturalOrientation(), mLauncher);
             mLauncher.goHome();
         } finally {
             mLauncher.setExpectedRotationCheckEnabled(true);
diff --git a/quickstep/tests/src/com/android/quickstep/TaskAnimationManagerTest.java b/quickstep/tests/src/com/android/quickstep/TaskAnimationManagerTest.java
index 28c8a4a..633a575 100644
--- a/quickstep/tests/src/com/android/quickstep/TaskAnimationManagerTest.java
+++ b/quickstep/tests/src/com/android/quickstep/TaskAnimationManagerTest.java
@@ -17,8 +17,8 @@
 package com.android.quickstep;
 
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assume.assumeTrue;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
@@ -29,6 +29,8 @@
 
 import androidx.test.filters.SmallTest;
 
+import com.android.quickstep.fallback.window.RecentsWindowManager;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.mockito.ArgumentCaptor;
@@ -42,6 +44,9 @@
     private Context mContext;
 
     @Mock
+    private RecentsWindowManager mRecentsWindowManager;
+
+    @Mock
     private SystemUiProxy mSystemUiProxy;
 
     private TaskAnimationManager mTaskAnimationManager;
@@ -49,7 +54,7 @@
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
-        mTaskAnimationManager = new TaskAnimationManager(mContext) {
+        mTaskAnimationManager = new TaskAnimationManager(mContext, mRecentsWindowManager) {
             @Override
             SystemUiProxy getSystemUiProxy() {
                 return mSystemUiProxy;
@@ -68,7 +73,8 @@
 
         final ArgumentCaptor<ActivityOptions> optionsCaptor =
                 ArgumentCaptor.forClass(ActivityOptions.class);
-        verify(mSystemUiProxy).startRecentsActivity(any(), optionsCaptor.capture(), any());
+        verify(mSystemUiProxy)
+                .startRecentsActivity(any(), optionsCaptor.capture(), any(), anyBoolean());
         assertEquals(ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS,
                 optionsCaptor.getValue().getPendingIntentBackgroundActivityStartMode());
     }
diff --git a/quickstep/tests/src/com/android/quickstep/desktop/DesktopAppLaunchTransitionManagerTest.kt b/quickstep/tests/src/com/android/quickstep/desktop/DesktopAppLaunchTransitionManagerTest.kt
new file mode 100644
index 0000000..26189df
--- /dev/null
+++ b/quickstep/tests/src/com/android/quickstep/desktop/DesktopAppLaunchTransitionManagerTest.kt
@@ -0,0 +1,110 @@
+/*
+ * 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.desktop
+
+import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
+import android.content.Context
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
+import android.view.WindowManager.TRANSIT_OPEN
+import android.view.WindowManager.TRANSIT_TO_FRONT
+import android.window.TransitionFilter
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
+import com.android.quickstep.SystemUiProxy
+import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+import org.mockito.quality.Strictness
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class DesktopAppLaunchTransitionManagerTest {
+
+    @get:Rule val mSetFlagsRule = SetFlagsRule()
+
+    private val mockitoSession =
+        mockitoSession()
+            .strictness(Strictness.LENIENT)
+            .mockStatic(DesktopModeStatus::class.java)
+            .startMocking()
+
+    private val context = mock<Context>()
+    private val systemUiProxy = mock<SystemUiProxy>()
+    private lateinit var transitionManager: DesktopAppLaunchTransitionManager
+
+    @Before
+    fun setUp() {
+        whenever(context.resources).thenReturn(mock())
+        whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(true)
+        transitionManager = DesktopAppLaunchTransitionManager(context, systemUiProxy)
+    }
+
+    @After
+    fun tearDown() {
+        mockitoSession.finishMocking()
+    }
+
+    @Test
+    @EnableFlags(FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS)
+    fun registerTransitions_appLaunchFlagEnabled_registersTransition() {
+        transitionManager.registerTransitions()
+
+        verify(systemUiProxy, times(1)).registerRemoteTransition(any(), any())
+    }
+
+    @Test
+    @DisableFlags(FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS)
+    fun registerTransitions_appLaunchFlagDisabled_doesntRegisterTransition() {
+        transitionManager.registerTransitions()
+
+        verify(systemUiProxy, times(0)).registerRemoteTransition(any(), any())
+    }
+
+    @Test
+    @EnableFlags(FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS)
+    fun registerTransitions_usesCorrectFilter() {
+        transitionManager.registerTransitions()
+        val filterArgumentCaptor = argumentCaptor<TransitionFilter>()
+
+        verify(systemUiProxy, times(1))
+            .registerRemoteTransition(any(), filterArgumentCaptor.capture())
+
+        assertThat(filterArgumentCaptor.lastValue).isNotNull()
+        assertThat(filterArgumentCaptor.lastValue.mTypeSet)
+            .isEqualTo(intArrayOf(TRANSIT_OPEN, TRANSIT_TO_FRONT))
+        assertThat(filterArgumentCaptor.lastValue.mRequirements).hasLength(1)
+        val launchRequirement = filterArgumentCaptor.lastValue.mRequirements!![0]
+        assertThat(launchRequirement.mModes).isEqualTo(intArrayOf(TRANSIT_OPEN, TRANSIT_TO_FRONT))
+        assertThat(launchRequirement.mActivityType).isEqualTo(ACTIVITY_TYPE_STANDARD)
+        assertThat(launchRequirement.mWindowingMode).isEqualTo(WINDOWING_MODE_FREEFORM)
+    }
+}
diff --git a/quickstep/tests/src/com/android/quickstep/util/SplitScreenTestUtils.kt b/quickstep/tests/src/com/android/quickstep/util/SplitScreenTestUtils.kt
index 82361aa..99c74be 100644
--- a/quickstep/tests/src/com/android/quickstep/util/SplitScreenTestUtils.kt
+++ b/quickstep/tests/src/com/android/quickstep/util/SplitScreenTestUtils.kt
@@ -43,11 +43,11 @@
         val currentTask = overviewWithSplitPair.currentTask
         currentTask.containsContentDescription(
             By.pkg(AbstractLauncherUiTest.getAppPackageName()).text("TestActivity3").toString(),
-            OverviewTask.OverviewSplitTask.SPLIT_TOP_OR_LEFT
+            OverviewTask.OverviewTaskContainer.SPLIT_TOP_OR_LEFT,
         )
         currentTask.containsContentDescription(
             By.pkg(AbstractLauncherUiTest.getAppPackageName()).text("TestActivity2").toString(),
-            OverviewTask.OverviewSplitTask.SPLIT_BOTTOM_OR_RIGHT
+            OverviewTask.OverviewTaskContainer.SPLIT_BOTTOM_OR_RIGHT,
         )
         return overviewWithSplitPair
     }
diff --git a/res/drawable/all_apps_tabs_background.xml b/res/drawable/all_apps_tabs_background.xml
index 62927af..d200b9f 100644
--- a/res/drawable/all_apps_tabs_background.xml
+++ b/res/drawable/all_apps_tabs_background.xml
@@ -13,36 +13,25 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<ripple xmlns:android="http://schemas.android.com/apk/res/android"
-    android:color="@color/accent_ripple_color">
-
-    <item android:id="@android:id/mask">
-        <shape android:shape="rectangle">
-            <corners android:radius="@dimen/all_apps_header_pill_corner_radius" />
-            <solid android:color="@color/accent_ripple_color" />
-        </shape>
-    </item>
-
-    <item>
-        <selector android:enterFadeDuration="100">
-            <item
-                android:id="@+id/unselected"
-                android:state_selected="false">
-                <shape android:shape="rectangle">
-                    <corners android:radius="@dimen/all_apps_header_pill_corner_radius" />
-                    <solid android:color="?attr/materialColorSurfaceBright" />
-                </shape>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:drawable="@drawable/all_apps_tabs_background_unselected_focused" android:state_focused="true" android:state_selected="false" />
+    <item android:drawable="@drawable/all_apps_tabs_background_selected_focused" android:state_focused="true" android:state_selected="true" />
+    <item android:id="@+id/unselected" android:state_focused="false" android:state_selected="false">
+        <ripple android:color="@color/accent_ripple_color">
+            <item>
+                <selector android:enterFadeDuration="100">
+                    <item android:drawable="@drawable/all_apps_tabs_background_unselected" />
+                </selector>
             </item>
-
-            <item
-                android:id="@+id/selected"
-                android:state_selected="true">
-                <shape android:shape="rectangle">
-                    <corners android:radius="@dimen/all_apps_header_pill_corner_radius" />
-                    <solid android:color="?attr/materialColorPrimary" />
-                </shape>
-            </item>
-        </selector>
+        </ripple>
     </item>
-
-</ripple>
\ No newline at end of file
+    <item android:id="@+id/selected" android:state_focused="false" android:state_selected="true">
+        <ripple android:color="@color/accent_ripple_color">
+            <item>
+                <selector android:enterFadeDuration="100">
+                    <item android:drawable="@drawable/all_apps_tabs_background_selected" />
+                </selector>
+            </item>
+        </ripple>
+    </item>
+</selector>
\ No newline at end of file
diff --git a/res/drawable/all_apps_tabs_background_selected.xml b/res/drawable/all_apps_tabs_background_selected.xml
new file mode 100644
index 0000000..47f95dd
--- /dev/null
+++ b/res/drawable/all_apps_tabs_background_selected.xml
@@ -0,0 +1,27 @@
+<?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.
+-->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+        <item
+        android:bottom="@dimen/all_apps_tabs_focus_width"
+        android:end="@dimen/all_apps_tabs_focus_width"
+        android:start="@dimen/all_apps_tabs_focus_width"
+        android:top="@dimen/all_apps_tabs_focus_width">
+        <shape android:shape="rectangle">
+            <corners android:radius="@dimen/all_apps_header_pill_corner_radius" />
+            <solid android:color="?attr/materialColorPrimary" />
+        </shape>
+    </item>
+</layer-list>
\ No newline at end of file
diff --git a/res/drawable/all_apps_tabs_background_selected_focused.xml b/res/drawable/all_apps_tabs_background_selected_focused.xml
new file mode 100644
index 0000000..e3d86c0
--- /dev/null
+++ b/res/drawable/all_apps_tabs_background_selected_focused.xml
@@ -0,0 +1,37 @@
+<?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.
+-->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item>
+        <shape>
+            <corners android:radius="16dp" />
+            <solid android:color="?attr/materialColorPrimary" />
+        </shape>
+    </item>
+
+    <item
+        android:bottom="@dimen/all_apps_tabs_focus_border"
+        android:end="@dimen/all_apps_tabs_focus_border"
+        android:start="@dimen/all_apps_tabs_focus_border"
+        android:top="@dimen/all_apps_tabs_focus_border">
+        <shape android:shape="rectangle">
+            <corners android:radius="13dp" />
+            <solid android:color="?attr/materialColorPrimary" />
+            <stroke
+                android:width="@dimen/all_apps_tabs_focus_padding"
+                android:color="?attr/materialColorSurfaceDim" />
+        </shape>
+    </item>
+</layer-list>
\ No newline at end of file
diff --git a/res/drawable/all_apps_tabs_background_unselected.xml b/res/drawable/all_apps_tabs_background_unselected.xml
new file mode 100644
index 0000000..ab592a8
--- /dev/null
+++ b/res/drawable/all_apps_tabs_background_unselected.xml
@@ -0,0 +1,27 @@
+<?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.
+-->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+     <item
+        android:bottom="@dimen/all_apps_tabs_focus_width"
+        android:end="@dimen/all_apps_tabs_focus_width"
+        android:start="@dimen/all_apps_tabs_focus_width"
+        android:top="@dimen/all_apps_tabs_focus_width">
+        <shape android:shape="rectangle">
+            <corners android:radius="@dimen/all_apps_header_pill_corner_radius" />
+            <solid android:color="?attr/materialColorSurfaceBright" />
+        </shape>
+    </item>
+</layer-list>
\ No newline at end of file
diff --git a/res/drawable/all_apps_tabs_background_unselected_focused.xml b/res/drawable/all_apps_tabs_background_unselected_focused.xml
new file mode 100644
index 0000000..0016102
--- /dev/null
+++ b/res/drawable/all_apps_tabs_background_unselected_focused.xml
@@ -0,0 +1,37 @@
+<?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.
+-->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item>
+        <shape>
+            <corners android:radius="16dp" />
+            <solid android:color="?attr/materialColorPrimary" />
+        </shape>
+    </item>
+
+    <item
+        android:bottom="@dimen/all_apps_tabs_focus_border"
+        android:end="@dimen/all_apps_tabs_focus_border"
+        android:start="@dimen/all_apps_tabs_focus_border"
+        android:top="@dimen/all_apps_tabs_focus_border">
+        <shape android:shape="rectangle">
+            <corners android:radius="13dp" />
+            <solid android:color="?attr/materialColorSurfaceBright" />
+            <stroke
+                android:width="@dimen/all_apps_tabs_focus_padding"
+                android:color="?attr/materialColorSurfaceDim" />
+        </shape>
+    </item>
+</layer-list>
\ No newline at end of file
diff --git a/res/drawable/bg_letter_list_text.xml b/res/drawable/bg_letter_list_text.xml
index 427702b..bfdd35c 100644
--- a/res/drawable/bg_letter_list_text.xml
+++ b/res/drawable/bg_letter_list_text.xml
@@ -15,7 +15,7 @@
   -->
 <shape xmlns:android="http://schemas.android.com/apk/res/android"
     android:shape="oval">
-    <solid android:color="?attr/materialColorSurfaceContainer" />
+    <solid android:color="?attr/materialColorSurface" />
     <corners android:radius="100dp"/>
     <size
         android:width="@dimen/bg_letter_list_text_size"
diff --git a/res/drawable/bg_widgets_header_states_two_pane.xml b/res/drawable/bg_widgets_header_states_two_pane.xml
index 5f4b8c6..1ec41a9 100644
--- a/res/drawable/bg_widgets_header_states_two_pane.xml
+++ b/res/drawable/bg_widgets_header_states_two_pane.xml
@@ -14,18 +14,16 @@
      limitations under the License.
 -->
 <selector xmlns:android="http://schemas.android.com/apk/res/android">
-
-    <item android:state_expanded="true">
-        <shape android:shape="rectangle">
-            <solid android:color="?attr/widgetPickerHeaderBackgroundColor" />
-            <corners android:radius="@dimen/widget_list_top_bottom_corner_radius" />
-        </shape>
+    <item android:state_expanded="true" android:state_focused="false">
+        <ripple android:color="@color/accent_ripple_color">
+            <item android:drawable="@drawable/bg_widgets_header_two_pane_expanded_unfocused" />
+        </ripple>
     </item>
-
-    <item android:state_expanded="false">
-        <shape android:shape="rectangle">
-            <solid android:color="@android:color/transparent" />
-            <corners android:radius="@dimen/widget_list_top_bottom_corner_radius" />
-        </shape>
+    <item android:state_expanded="false" android:state_focused="false">
+        <ripple android:color="@color/accent_ripple_color">
+            <item android:drawable="@drawable/bg_widgets_header_two_pane_unexpanded_unfocused" />
+        </ripple>
     </item>
+    <item android:drawable="@drawable/bg_widgets_header_two_pane_expanded_focused" android:state_expanded="true" android:state_focused="true" />
+    <item android:drawable="@drawable/bg_widgets_header_two_pane_unexpanded_focused" android:state_expanded="false" android:state_focused="true" />
 </selector>
diff --git a/res/drawable/bg_widgets_header_two_pane.xml b/res/drawable/bg_widgets_header_two_pane.xml
index ca3feef..e237002 100644
--- a/res/drawable/bg_widgets_header_two_pane.xml
+++ b/res/drawable/bg_widgets_header_two_pane.xml
@@ -14,13 +14,10 @@
      limitations under the License.
 -->
 <inset xmlns:android="http://schemas.android.com/apk/res/android"
-    android:insetTop="@dimen/widget_list_entry_spacing" >
-    <ripple
-        android:color="@color/accent_ripple_color"
-        android:paddingTop="@dimen/widget_list_header_view_vertical_padding"
-        android:paddingBottom="@dimen/widget_list_header_view_vertical_padding" >
-        <item android:id="@android:id/mask"
-            android:drawable="@drawable/bg_widgets_header_states_two_pane" />
+    android:insetTop="@dimen/widget_list_entry_spacing">
+    <layer-list
+        android:paddingBottom="@dimen/widget_list_header_view_vertical_padding"
+        android:paddingTop="@dimen/widget_list_header_view_vertical_padding">
         <item android:drawable="@drawable/bg_widgets_header_states_two_pane" />
-    </ripple>
-</inset>
+    </layer-list>
+</inset>
\ No newline at end of file
diff --git a/res/drawable/bg_widgets_header_two_pane_expanded_focused.xml b/res/drawable/bg_widgets_header_two_pane_expanded_focused.xml
new file mode 100644
index 0000000..0ee3d14
--- /dev/null
+++ b/res/drawable/bg_widgets_header_two_pane_expanded_focused.xml
@@ -0,0 +1,38 @@
+<?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.
+  -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <!-- Draw the focus ring -->
+    <item>
+        <shape>
+            <corners android:radius="@dimen/widget_focus_ring_corner_radius" />
+            <stroke
+                android:width="@dimen/widget_header_focus_ring_width"
+                android:color="?attr/widgetPickerTabBackgroundSelected" />
+        </shape>
+    </item>
+
+    <!-- Draw the background with padding to make it spaced within the focus ring. -->
+    <item
+        android:bottom="@dimen/widget_header_background_border"
+        android:end="@dimen/widget_header_background_border"
+        android:start="@dimen/widget_header_background_border"
+        android:top="@dimen/widget_header_background_border">
+        <shape android:shape="rectangle">
+            <corners android:radius="@dimen/widget_list_top_bottom_corner_radius" />
+            <solid android:color="?attr/widgetPickerHeaderBackgroundColor" />
+        </shape>
+    </item>
+</layer-list>
\ No newline at end of file
diff --git a/res/drawable/bg_widgets_header_two_pane_expanded_unfocused.xml b/res/drawable/bg_widgets_header_two_pane_expanded_unfocused.xml
new file mode 100644
index 0000000..9028ebe
--- /dev/null
+++ b/res/drawable/bg_widgets_header_two_pane_expanded_unfocused.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.
+  -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
+    android:paddingBottom="@dimen/widget_list_header_view_vertical_padding"
+    android:paddingTop="@dimen/widget_list_header_view_vertical_padding">
+    <item>
+        <shape android:shape="rectangle">
+            <corners android:radius="@dimen/widget_list_top_bottom_corner_radius" />
+            <solid android:color="?attr/widgetPickerHeaderBackgroundColor" />
+        </shape>
+    </item>
+</layer-list>
\ No newline at end of file
diff --git a/res/drawable/bg_widgets_header_two_pane_unexpanded_focused.xml b/res/drawable/bg_widgets_header_two_pane_unexpanded_focused.xml
new file mode 100644
index 0000000..12dc907
--- /dev/null
+++ b/res/drawable/bg_widgets_header_two_pane_unexpanded_focused.xml
@@ -0,0 +1,27 @@
+<?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.
+  -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <!-- Draw the focus ring and a transparent background -->
+    <item>
+        <shape>
+            <corners android:radius="@dimen/widget_focus_ring_corner_radius" />
+            <solid android:color="@android:color/transparent" />
+            <stroke
+                android:width="@dimen/widget_header_focus_ring_width"
+                android:color="?attr/widgetPickerTabBackgroundSelected" />
+        </shape>
+    </item>
+</layer-list>
\ No newline at end of file
diff --git a/res/drawable/bg_widgets_header_two_pane_unexpanded_unfocused.xml b/res/drawable/bg_widgets_header_two_pane_unexpanded_unfocused.xml
new file mode 100644
index 0000000..ba26f9f
--- /dev/null
+++ b/res/drawable/bg_widgets_header_two_pane_unexpanded_unfocused.xml
@@ -0,0 +1,23 @@
+<?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.
+  -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item>
+        <shape android:shape="rectangle">
+            <corners android:radius="@dimen/widget_list_top_bottom_corner_radius" />
+            <solid android:color="@android:color/transparent" />
+        </shape>
+    </item>
+</layer-list>
\ No newline at end of file
diff --git a/res/drawable/ic_corp_off.xml b/res/drawable/ic_corp_off.xml
index 117258e..d4bb2f3 100644
--- a/res/drawable/ic_corp_off.xml
+++ b/res/drawable/ic_corp_off.xml
@@ -16,9 +16,9 @@
     android:width="24dp"
     android:height="24dp"
     android:viewportWidth="24"
-    android:viewportHeight="24"
-    android:tint="?android:attr/textColorHint">
+    android:viewportHeight="24">
     <path
-        android:fillColor="@android:color/white"
-        android:pathData="M20,6h-4L16,4c0,-1.11 -0.89,-2 -2,-2h-4c-1.11,0 -2,0.89 -2,2v1.17L10.83,8L20,8v9.17l1.98,1.98c0,-0.05 0.02,-0.1 0.02,-0.16L22,8c0,-1.11 -0.89,-2 -2,-2zM14,6h-4L10,4h4v2zM19,19L8,8 6,6 2.81,2.81 1.39,4.22 3.3,6.13C2.54,6.41 2.01,7.14 2.01,8L2,19c0,1.11 0.89,2 2,2h14.17l1.61,1.61 1.41,-1.41 -0.37,-0.37L19,19zM4,19L4,8h1.17l11,11L4,19z" />
-</vector>
\ No newline at end of file
+        android:pathData="M16,6H20C21.11,6 22,6.89 22,8V18.99C22,19.021 21.994,19.05 21.989,19.077C21.984,19.102 21.98,19.126 21.98,19.15L20,17.17V8H10.83L8,5.17V4C8,2.89 8.89,2 10,2H14C15.11,2 16,2.89 16,4V6ZM10,6H14V4H10V6ZM19,19L8,8L6,6L2.81,2.81L1.39,4.22L3.3,6.13C2.54,6.41 2.01,7.14 2.01,8L2,19C2,20.11 2.89,21 4,21H18.17L19.78,22.61L21.19,21.2L20.82,20.83L19,19ZM4,8V19H16.17L5.17,8H4Z"
+        android:fillColor="?attr/materialColorOnPrimary"
+        android:fillType="evenOdd"/>
+</vector>
diff --git a/res/drawable/ic_more_horiz_24.xml b/res/drawable/ic_more_horiz_24.xml
new file mode 100644
index 0000000..d46827c
--- /dev/null
+++ b/res/drawable/ic_more_horiz_24.xml
@@ -0,0 +1,26 @@
+<!--
+  ~ 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:tint="#000000"
+    android:viewportHeight="24"
+    android:viewportWidth="24">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M6,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM18,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z" />
+</vector>
diff --git a/res/drawable/ic_private_profile_divider_badge.xml b/res/drawable/ic_private_profile_divider_badge.xml
new file mode 100644
index 0000000..07c740d
--- /dev/null
+++ b/res/drawable/ic_private_profile_divider_badge.xml
@@ -0,0 +1,26 @@
+<!--
+  ~ 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="20dp"
+    android:height="20dp"
+    android:viewportWidth="20"
+    android:viewportHeight="20">
+    <group>
+      <path
+          android:pathData="M5,9L15,9A1,1 0,0 1,16 10L16,10A1,1 0,0 1,15 11L5,11A1,1 0,0 1,4 10L4,10A1,1 0,0 1,5 9z"
+          android:fillColor="?attr/materialColorOnSurface"/>
+    </group>
+</vector>
diff --git a/res/drawable/ic_private_profile_letter_list_fast_scroller_badge.xml b/res/drawable/ic_private_profile_letter_list_fast_scroller_badge.xml
new file mode 100644
index 0000000..8d12598
--- /dev/null
+++ b/res/drawable/ic_private_profile_letter_list_fast_scroller_badge.xml
@@ -0,0 +1,28 @@
+<!--
+  ~ 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="12dp"
+    android:height="15dp"
+    android:viewportWidth="12"
+    android:viewportHeight="15">
+    <path
+        android:pathData="M5.952,0.911L0.645,2.902V6.942C0.645,10.292 2.907,13.417 5.952,14.18C8.997,13.417 11.26,10.292 11.26,6.942V2.902L5.952,0.911ZM7.943,9.536V10.863H6.616V11.526H5.289V8.103C4.333,7.818 3.63,6.942 3.63,5.887C3.63,4.607 4.672,3.565 5.952,3.565C7.233,3.565 8.274,4.607 8.274,5.887C8.274,6.935 7.571,7.818 6.616,8.103V9.536H7.943Z"
+        android:fillColor="#3C4043"
+        android:fillType="evenOdd"/>
+    <path
+        android:pathData="M5.952,6.882C6.502,6.882 6.947,6.436 6.947,5.887C6.947,5.337 6.502,4.892 5.952,4.892C5.403,4.892 4.957,5.337 4.957,5.887C4.957,6.436 5.403,6.882 5.952,6.882Z"
+        android:fillColor="#3C4043"/>
+</vector>
diff --git a/res/drawable/ic_schedule.xml b/res/drawable/ic_schedule.xml
new file mode 100644
index 0000000..3eeb6a2
--- /dev/null
+++ b/res/drawable/ic_schedule.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"
+    android:tint="?attr/colorControlNormal">
+    <path
+        android:fillColor="?attr/materialColorOnPrimary"
+        android:pathData="M612,668L668,612L520,464L520,280L440,280L440,496L612,668ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880Z"/>
+</vector>
diff --git a/res/drawable/widgets_list_expand_button_background.xml b/res/drawable/widgets_list_expand_button_background.xml
new file mode 100644
index 0000000..068b26d
--- /dev/null
+++ b/res/drawable/widgets_list_expand_button_background.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.
+  -->
+<inset xmlns:android="http://schemas.android.com/apk/res/android">
+    <ripple android:color="?android:attr/colorControlHighlight">
+        <item>
+            <shape android:shape="rectangle">
+                <corners android:radius="50dp" />
+                <solid android:color="?attr/widgetPickerExpandButtonBackgroundColor" />
+            </shape>
+        </item>
+    </ripple>
+</inset>
\ No newline at end of file
diff --git a/res/drawable/work_mode_fab_background.xml b/res/drawable/work_mode_fab_background.xml
index 6be33e8..5bad965 100644
--- a/res/drawable/work_mode_fab_background.xml
+++ b/res/drawable/work_mode_fab_background.xml
@@ -18,7 +18,10 @@
     <item>
         <shape android:shape="rectangle">
             <corners android:radius="@dimen/work_fab_radius" />
-            <solid android:color="@color/work_fab_bg_color" />
+            <solid android:color="?attr/materialColorPrimary" />
+            <padding
+                android:left="@dimen/work_mode_fab_background_horizontal_padding"
+                android:right="@dimen/work_mode_fab_background_horizontal_padding"/>
         </shape>
     </item>
 </ripple>
diff --git a/res/drawable/work_scheduler_background.xml b/res/drawable/work_scheduler_background.xml
new file mode 100644
index 0000000..6bbf029
--- /dev/null
+++ b/res/drawable/work_scheduler_background.xml
@@ -0,0 +1,26 @@
+<?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.
+  -->
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+    android:color="@color/accent_ripple_color">
+    <item>
+        <shape android:shape="rectangle">
+            <corners android:radius="@dimen/work_fab_radius" />
+            <solid android:color="?attr/materialColorPrimary" />
+            <padding
+                android:padding="@dimen/work_scheduler_background_padding" />
+        </shape>
+    </item>
+</ripple>
\ No newline at end of file
diff --git a/res/layout/all_apps_personal_work_tabs.xml b/res/layout/all_apps_personal_work_tabs.xml
index e04b207..ecc5a14 100644
--- a/res/layout/all_apps_personal_work_tabs.xml
+++ b/res/layout/all_apps_personal_work_tabs.xml
@@ -21,8 +21,8 @@
     android:layout_width="match_parent"
     android:layout_height="@dimen/all_apps_header_pill_height"
     android:layout_gravity="center_horizontal"
-    android:paddingTop="@dimen/all_apps_tabs_vertical_padding"
-    android:paddingBottom="@dimen/all_apps_tabs_vertical_padding"
+    android:paddingTop="@dimen/all_apps_tabs_vertical_padding_focus"
+    android:paddingBottom="@dimen/all_apps_tabs_vertical_padding_focus"
     android:layout_marginTop="@dimen/all_apps_tabs_margin_top"
     android:orientation="horizontal"
     style="@style/TextHeadline"
diff --git a/res/layout/user_folder_icon_normalized.xml b/res/layout/user_folder_icon_normalized.xml
index 43a8aac..002e7b7 100644
--- a/res/layout/user_folder_icon_normalized.xml
+++ b/res/layout/user_folder_icon_normalized.xml
@@ -40,12 +40,11 @@
         <com.android.launcher3.folder.FolderNameEditText
             android:id="@+id/folder_name"
             android:layout_width="0dp"
-            android:layout_height="wrap_content"
-            android:layout_gravity="center_vertical"
+            android:layout_height="match_parent"
             style="@style/TextHeadline"
             android:layout_weight="1"
             android:background="@android:color/transparent"
-            android:gravity="center_horizontal"
+            android:gravity="center"
             android:hint="@string/folder_hint_text"
             android:imeOptions="flagNoExtractUi"
             android:importantForAutofill="no"
diff --git a/res/layout/widgets_full_sheet.xml b/res/layout/widgets_full_sheet.xml
index 009359c..1f14f69 100644
--- a/res/layout/widgets_full_sheet.xml
+++ b/res/layout/widgets_full_sheet.xml
@@ -24,9 +24,7 @@
     <com.android.launcher3.views.SpringRelativeLayout
         android:id="@+id/container"
         android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:focusable="true"
-        android:importantForAccessibility="no">
+        android:layout_height="match_parent">
 
         <View
             android:id="@+id/collapse_handle"
@@ -74,4 +72,4 @@
             android:clipToPadding="false" />
 
     </com.android.launcher3.views.SpringRelativeLayout>
-</com.android.launcher3.widget.picker.WidgetsFullSheet>
\ No newline at end of file
+</com.android.launcher3.widget.picker.WidgetsFullSheet>
diff --git a/res/layout/widgets_list_expand_button.xml b/res/layout/widgets_list_expand_button.xml
new file mode 100644
index 0000000..ff2d777
--- /dev/null
+++ b/res/layout/widgets_list_expand_button.xml
@@ -0,0 +1,34 @@
+<?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.
+  -->
+
+<Button xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/widget_list_expand_button"
+    style="@style/Button.Rounded.Colored"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:layout_marginTop="@dimen/widgets_list_expand_button_top_margin"
+    android:background="@drawable/widgets_list_expand_button_background"
+    android:drawablePadding="@dimen/widgets_list_expand_button_drawable_padding"
+    android:drawableStart="@drawable/ic_more_horiz_24"
+    android:drawableTint="?attr/widgetPickerExpandButtonTextColor"
+    android:maxLines="1"
+    android:minHeight="48dp"
+    android:paddingEnd="@dimen/widgets_list_expand_button_end_padding"
+    android:paddingStart="@dimen/widgets_list_expand_button_start_padding"
+    android:paddingVertical="@dimen/widgets_list_expand_button_vertical_padding"
+    android:text="@string/widgets_list_expand_button_label"
+    android:contentDescription="@string/widgets_list_expand_button_content_description"
+    android:textColor="?attr/widgetPickerExpandButtonTextColor" />
\ No newline at end of file
diff --git a/res/layout/widgets_two_pane_sheet.xml b/res/layout/widgets_two_pane_sheet.xml
index ce5eed9..5dc1b47 100644
--- a/res/layout/widgets_two_pane_sheet.xml
+++ b/res/layout/widgets_two_pane_sheet.xml
@@ -23,9 +23,7 @@
     <com.android.launcher3.views.SpringRelativeLayout
         android:id="@+id/container"
         android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:focusable="true"
-        android:importantForAccessibility="no">
+        android:layout_height="match_parent">
 
         <View
             android:id="@+id/collapse_handle"
@@ -72,7 +70,7 @@
                 android:layout_height="match_parent"
                 android:clipChildren="false"
                 android:clipToPadding="false"
-                android:paddingBottom="24dp"
+                android:paddingBottom="8dp"
                 android:layout_gravity="start"
                 android:layout_weight="0.33">
                 <TextView
@@ -86,14 +84,6 @@
                     android:layout_width="@dimen/fastscroll_width"
                     android:layout_height="match_parent"
                     android:layout_marginEnd="@dimen/fastscroll_end_margin" />
-
-                <com.android.launcher3.widget.picker.WidgetsRecyclerView
-                    android:id="@+id/search_widgets_list_view"
-                    android:layout_width="match_parent"
-                    android:layout_height="wrap_content"
-                    android:clipToPadding="false"
-                    android:paddingHorizontal="@dimen/widget_list_horizontal_margin_two_pane"
-                    android:visibility="gone" />
             </FrameLayout>
 
             <FrameLayout
diff --git a/res/layout/widgets_two_pane_sheet_paged_view.xml b/res/layout/widgets_two_pane_sheet_paged_view.xml
index 1cbd2ba..0528d3b 100644
--- a/res/layout/widgets_two_pane_sheet_paged_view.xml
+++ b/res/layout/widgets_two_pane_sheet_paged_view.xml
@@ -15,44 +15,26 @@
 <merge xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:launcher="http://schemas.android.com/apk/res-auto">
 
-    <FrameLayout
+    <LinearLayout
         android:id="@+id/widgets_two_pane_sheet_paged_view"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:gravity="start"
-        android:paddingHorizontal="@dimen/widget_list_horizontal_margin_two_pane"
         android:layout_gravity="start"
         android:clipChildren="false"
         android:clipToPadding="false"
-        android:layout_alignParentStart="true">
-        <com.android.launcher3.widget.picker.WidgetPagedView
-            android:id="@+id/widgets_view_pager"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:clipToPadding="false"
-            android:descendantFocusability="afterDescendants"
-            launcher:pageIndicator="@+id/tabs" >
-
-            <com.android.launcher3.widget.picker.WidgetsRecyclerView
-                android:id="@+id/primary_widgets_list_view"
-                android:layout_width="match_parent"
-                android:layout_height="match_parent"
-                android:clipToPadding="false" />
-
-            <com.android.launcher3.widget.picker.WidgetsRecyclerView
-                android:id="@+id/work_widgets_list_view"
-                android:layout_width="match_parent"
-                android:layout_height="match_parent"
-                android:clipToPadding="false" />
-
-        </com.android.launcher3.widget.picker.WidgetPagedView>
-
+        android:layout_alignParentStart="true"
+        android:orientation="vertical">
         <!-- SearchAndRecommendationsView without the tab layout as well -->
-        <com.android.launcher3.views.StickyHeaderLayout
+        <!-- Note: the horizontal padding matches with the WidgetPagedView -->
+        <LinearLayout
             android:id="@+id/search_and_recommendations_container"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:clipToOutline="true"
+            android:elevation="1dp"
+            android:background="?attr/widgetPickerPrimarySurfaceColor"
+            android:paddingHorizontal="@dimen/widget_list_horizontal_margin_two_pane"
             android:orientation="vertical">
 
             <LinearLayout
@@ -62,6 +44,7 @@
                 android:orientation="horizontal"
                 android:background="?attr/widgetPickerPrimarySurfaceColor"
                 android:gravity="center_vertical"
+                android:layout_marginBottom="8dp"
                 launcher:layout_sticky="true">
                 <FrameLayout
                     android:layout_width="0dp"
@@ -93,7 +76,6 @@
                 android:layout_width="match_parent"
                 android:layout_height="match_parent"
                 android:id="@+id/suggestions_header"
-                android:layout_marginTop="8dp"
                 android:orientation="horizontal"
                 android:background="?attr/widgetPickerPrimarySurfaceColor"
                 launcher:layout_sticky="true">
@@ -135,6 +117,39 @@
                     style="?android:attr/borderlessButtonStyle" />
 
             </com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip>
-        </com.android.launcher3.views.StickyHeaderLayout>
-    </FrameLayout>
+        </LinearLayout>
+        <!-- Note: the paddingHorizontal has to be on WidgetPagedView level so that talkback
+ correctly orders the lists to be after the search and suggestions header. See b/209579563.
+  -->
+        <com.android.launcher3.widget.picker.WidgetPagedView
+            android:id="@+id/widgets_view_pager"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:clipToPadding="false"
+            android:paddingHorizontal="@dimen/widget_list_horizontal_margin_two_pane"
+            android:descendantFocusability="afterDescendants"
+            launcher:pageIndicator="@+id/tabs" >
+
+            <com.android.launcher3.widget.picker.WidgetsRecyclerView
+                android:id="@+id/primary_widgets_list_view"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:clipToPadding="false" />
+
+            <com.android.launcher3.widget.picker.WidgetsRecyclerView
+                android:id="@+id/work_widgets_list_view"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:clipToPadding="false" />
+
+        </com.android.launcher3.widget.picker.WidgetPagedView>
+
+        <com.android.launcher3.widget.picker.WidgetsRecyclerView
+            android:id="@+id/search_widgets_list_view"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:clipToPadding="false"
+            android:paddingHorizontal="@dimen/widget_list_horizontal_margin_two_pane"
+            android:visibility="gone" />
+    </LinearLayout>
 </merge>
diff --git a/res/layout/widgets_two_pane_sheet_recyclerview.xml b/res/layout/widgets_two_pane_sheet_recyclerview.xml
index c6b3b74..45a9ac0 100644
--- a/res/layout/widgets_two_pane_sheet_recyclerview.xml
+++ b/res/layout/widgets_two_pane_sheet_recyclerview.xml
@@ -15,28 +15,22 @@
 <merge xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:launcher="http://schemas.android.com/apk/res-auto">
 
-    <FrameLayout
+    <LinearLayout
         android:id="@+id/widgets_two_pane_sheet_recyclerview"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:gravity="start"
         android:layout_gravity="start"
         android:clipChildren="false"
-        android:layout_alignParentStart="true">
-
-        <com.android.launcher3.widget.picker.WidgetsRecyclerView
-            android:id="@+id/primary_widgets_list_view"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:layout_marginHorizontal="@dimen/widget_list_horizontal_margin_two_pane"
-            android:clipToPadding="false" />
-
+        android:layout_alignParentStart="true"
+        android:orientation="vertical">
         <!-- SearchAndRecommendationsView without the tab layout as well -->
-        <com.android.launcher3.views.StickyHeaderLayout
+        <LinearLayout
             android:id="@+id/search_and_recommendations_container"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:clipToOutline="true"
+            android:background="?attr/widgetPickerPrimarySurfaceColor"
             android:orientation="vertical">
 
             <LinearLayout
@@ -83,6 +77,21 @@
                 android:background="?attr/widgetPickerPrimarySurfaceColor"
                 launcher:layout_sticky="true">
             </FrameLayout>
-        </com.android.launcher3.views.StickyHeaderLayout>
-    </FrameLayout>
+        </LinearLayout>
+
+        <com.android.launcher3.widget.picker.WidgetsRecyclerView
+            android:id="@+id/primary_widgets_list_view"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:layout_marginHorizontal="@dimen/widget_list_horizontal_margin_two_pane"
+            android:clipToPadding="false" />
+
+        <com.android.launcher3.widget.picker.WidgetsRecyclerView
+            android:id="@+id/search_widgets_list_view"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:clipToPadding="false"
+            android:paddingHorizontal="@dimen/widget_list_horizontal_margin_two_pane"
+            android:visibility="gone" />
+    </LinearLayout>
 </merge>
\ No newline at end of file
diff --git a/res/layout/work_mode_fab.xml b/res/layout/work_mode_fab.xml
index b3484c9..e2f0e09 100644
--- a/res/layout/work_mode_fab.xml
+++ b/res/layout/work_mode_fab.xml
@@ -12,42 +12,38 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<com.android.launcher3.allapps.WorkModeSwitch
+<LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@+id/work_mode_toggle"
-    android:layout_alignParentBottom="true"
-    android:layout_alignParentEnd="true"
     android:layout_height="@dimen/work_fab_height"
     android:layout_width="wrap_content"
+    android:elevation="@dimen/work_fab_elevation"
     android:minHeight="@dimen/work_fab_height"
     android:gravity="center_vertical"
     android:background="@drawable/work_mode_fab_background"
     android:forceHasOverlappingRendering="false"
-    android:contentDescription="@string/work_apps_pause_btn_text"
-    android:paddingStart="@dimen/work_mode_fab_background_start_padding"
-    android:paddingEnd="@dimen/work_mode_fab_background_end_padding"
-    android:animateLayoutChanges="true">
+    android:contentDescription="@string/work_apps_pause_btn_text">
     <ImageView
         android:id="@+id/work_icon"
         android:layout_width="@dimen/work_fab_icon_size"
         android:layout_height="@dimen/work_fab_icon_size"
+        android:layout_marginVertical="@dimen/work_fab_icon_vertical_margin"
         android:importantForAccessibility="no"
-        android:layout_marginEnd="@dimen/work_fab_icon_end_margin"
         android:src="@drawable/ic_corp_off"
-        android:tint="@color/work_fab_icon_color"
+        android:layout_marginStart="@dimen/work_fab_icon_start_margin_expanded"
         android:scaleType="center"/>
     <TextView
         android:id="@+id/pause_text"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:maxWidth="@dimen/work_fab_width"
-        android:textColor="@color/work_fab_icon_color"
+        android:textColor="?attr/materialColorOnPrimary"
         android:textSize="14sp"
         android:includeFontPadding="false"
         android:textDirection="locale"
         android:text="@string/work_apps_pause_btn_text"
+        android:layout_marginStart="@dimen/work_fab_text_start_margin"
         android:layout_marginEnd="@dimen/work_fab_text_end_margin"
-        android:ellipsize="end"
         android:maxLines="1"
         style="@style/TextHeadline"/>
-</com.android.launcher3.allapps.WorkModeSwitch>
+</LinearLayout>
diff --git a/res/layout/work_mode_utility_view.xml b/res/layout/work_mode_utility_view.xml
new file mode 100644
index 0000000..fc112ce
--- /dev/null
+++ b/res/layout/work_mode_utility_view.xml
@@ -0,0 +1,32 @@
+<!--
+  ~ 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.launcher3.allapps.WorkUtilityView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_height="wrap_content"
+    android:layout_width="wrap_content"
+    android:orientation="vertical"
+    android:layout_alignParentBottom="true"
+    android:layout_alignParentEnd="true">
+    <ImageButton
+        android:id="@+id/work_scheduler"
+        android:layout_width="@dimen/work_scheduler_size"
+        android:layout_height="@dimen/work_scheduler_size"
+        android:layout_marginBottom="@dimen/work_scheduler_bottom_margin"
+        android:contentDescription="@string/work_scheduler_button_content_description"
+        android:src="@drawable/ic_schedule"
+        android:layout_gravity="end"
+        android:background="@drawable/work_scheduler_background" />
+    <include layout="@layout/work_mode_fab" />
+</com.android.launcher3.allapps.WorkUtilityView>
diff --git a/res/values-af/strings.xml b/res/values-af/strings.xml
index 490a7c2..5181bb7 100644
--- a/res/values-af/strings.xml
+++ b/res/values-af/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Nuwe venster"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Stoor apppaar"</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">"Hierdie apppaar word nie op hierdie toestel gesteun nie"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Neem notas"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Voeg by"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Voeg <xliff:g id="WIDGET_NAME">%1$s</xliff:g>-legstuk by"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Tik om legstukinstellings te verander"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Verander legstukinstellings"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Deursoek programme"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Gedeaktiveer deur jou administrateur"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Laat toe dat tuisskerm gedraai word"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Wanneer foon gedraai word"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Kennisgewingkolle"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Aan"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Af"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> installeer tans; <xliff:g id="PROGRESS">%2$s</xliff:g> voltooi"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> laai tans af, <xliff:g id="PROGRESS">%2$s</xliff:g> voltooid"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> wag tans om te installeer"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> is geargiveer. Tik om af te laai en terug te stel."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> is geargiveer."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"laai af en stel terug"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Programopdatering word vereis"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Die program vir hierdie ikoon is nie opgedateer nie. Jy kan dit handmatig opdateer om hierdie kortpad weer te aktiveer, of die ikoon verwyder."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Dateer op"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Het dit"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Onderbreek werkprogramme"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Hervat"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filter"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Misluk: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Privaat ruimte"</string>
diff --git a/res/values-am/strings.xml b/res/values-am/strings.xml
index 7292eec..a217bb6 100644
--- a/res/values-am/strings.xml
+++ b/res/values-am/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"አዲስ መስኮት"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"የመተግበሪያ ጥምረትን ያስቀምጡ"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"ይህ የመተግበሪያ ጥምረት በዚህ መሣሪያ ላይ አይደገፍም"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"የማስታወሻ አያያዝ"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"አክል"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"ምግብር <xliff:g id="WIDGET_NAME">%1$s</xliff:g>ን አክል"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"የምግብር ቅንብሮችን ለመለወጥ መታ ያድርጉ"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"የምግብር ቅንብሮችን ይለውጡ"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"መተግበሪያዎችን ፈልግ"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"በእርስዎ አስተዳዳሪ የተሰናከለ"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"የመነሻ ማያ ገፅ ማሽከርከርን ይፍቀዱ"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"ስልኩ ሲዞር"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"የማሳወቂያ ነጥቦች"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"አብራ"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"ጠፍቷል"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> በመጫን ላይ፣ <xliff:g id="PROGRESS">%2$s</xliff:g> ተጠናቅቋል"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<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> ለመጫን በመጠበቅ ላይ"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> በማህደር ተቀምጧል። ለማወረድ እና ወደነበረበት ለመመለስ መታ ያድርጉ።"</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> በማህደር ተቀምጧል።"</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"አውርድ እና ወደነበረበት መልስ"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"መተግበሪያ ማዘመን አስፈላጊ ነው"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"የዚህ አዶ መተግበሪያ አልተዘመነም። ይህን አቋራጭ ዳግም ለማንቃት በራስዎ ማዘመን ወይም አዶውን ማስወገድ ይችላሉ።"</string>
     <string name="dialog_update" msgid="2178028071796141234">"አዘምን"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"ገባኝ"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"የሥራ መተግበሪያዎችን ባሉበት አቁም"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"ካቆመበት ቀጥል"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <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>
diff --git a/res/values-ar/strings.xml b/res/values-ar/strings.xml
index 06fc0a8..c08254a 100644
--- a/res/values-ar/strings.xml
+++ b/res/values-ar/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"نافذة جديدة"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"حفظ استخدام التطبيقين معًا"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | ‏<xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"لا يمكن استخدام هذين التطبيقَين في الوقت نفسه على هذا الجهاز"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"تدوين الملاحظات"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"إضافة"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"إضافة التطبيق المصغّر \"<xliff:g id="WIDGET_NAME">%1$s</xliff:g>\""</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"انقر لتغيير إعدادات الأداة"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"تغيير إعدادات الأداة"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"بحث في التطبيقات"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"أوقف المشرف هذه الميزة"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"السماح بتدوير الشاشة الرئيسية"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"عند تدوير الهاتف"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"نقاط الإشعارات"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"الإعداد مفعّل"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"غير مفعّل"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"جارٍ تثبيت <xliff:g id="NAME">%1$s</xliff:g>، مستوى التقدم: <xliff:g id="PROGRESS">%2$s</xliff:g>"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"جارٍ تنزيل <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> في انتظار التثبيت"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"تمت أرشفة تطبيق \"<xliff:g id="NAME">%1$s</xliff:g>\". انقر لتنزيله واستعادته."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"تمت أرشفة \"<xliff:g id="NAME">%1$s</xliff:g>\"."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"تنزيل التطبيق واستعادته"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"مطلوب تحديث التطبيق"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"لم يتمّ تحديث التطبيق الخاص بهذا الرمز. يمكنك تحديث التطبيق يدويًا لإعادة تفعيل هذا الاختصار أو إزالة الرمز."</string>
     <string name="dialog_update" msgid="2178028071796141234">"تحديث"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"حسنًا"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"إيقاف تطبيقات العمل مؤقتًا"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"إلغاء الإيقاف المؤقت"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <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>
diff --git a/res/values-as/strings.xml b/res/values-as/strings.xml
index cd6e347..ccfb10e 100644
--- a/res/values-as/strings.xml
+++ b/res/values-as/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"নতুন ৱিণ্ড’"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"এপৰ পেয়াৰ ছেভ কৰক"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"এই ডিভাইচটোত এই এপ্‌ পেয়াৰ কৰাৰ সুবিধাটো সমৰ্থিত নহয়"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"টোকা গ্ৰহণ কৰা"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"যোগ দিয়ক"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> ৱিজেট যোগ দিয়ক"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"ৱিজেটৰ ছেটিং সলনি কৰিবলৈ টিপক"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"ৱিজেটৰ ছেটিং সলনি কৰক"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"এপ্‌সমূহ সন্ধান কৰক"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"আপোনাৰ প্ৰশাসকে অক্ষম কৰি ৰাখিছে"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"গৃহ স্ক্ৰীন ঘূৰোৱাৰ অনুমতি দিয়ক"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"ফ\'নটো যেতিয়া ঘূৰোৱা হয়"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"জাননী বিন্দু"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"অন কৰা আছে"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"অফ আছে"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> ইনষ্টল কৰি থকা হৈছে, <xliff:g id="PROGRESS">%2$s</xliff:g> সম্পূৰ্ণ হৈছে"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<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> ইনষ্টল হোৱালৈ অপেক্ষা কৰি থকা হৈছে"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> আৰ্কাইভ কৰা হৈছে। ডাউনল’ড আৰু পুনঃস্থাপন কৰিবলৈ টিপক।"</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> আৰ্কাইভ কৰা হৈছে।"</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"ডাউনল’ড আৰু পুনঃস্থাপন কৰক"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"এপ্‌টো আপডে’ট কৰা প্ৰয়োজন"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"এই চিহ্নটোৰ এপ্‌টো আপডে’ট কৰা হোৱা নাই। আপুনি এই শ্বৰ্টকাটটো পুনৰ সক্ষম কৰিবলৈ মেনুৱেলী আপডে’ট কৰিব পাৰে অথবা চিহ্নটো আঁতৰাব পাৰে।"</string>
     <string name="dialog_update" msgid="2178028071796141234">"আপডে’ট কৰক"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"বুজি পালোঁ"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"কৰ্মস্থানৰ এপ্‌ পজ কৰক"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"আনপজ কৰক"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <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>
diff --git a/res/values-az/strings.xml b/res/values-az/strings.xml
index b8d660f..c85f271 100644
--- a/res/values-az/strings.xml
+++ b/res/values-az/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Yeni Pəncərə"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Tətbiq cütünü saxlayın"</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">"Bu tətbiq cütü bu cihazda dəstəklənmir"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Qeydgötürmə"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Əlavə edin"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> vidcet əlavə edin"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Vidcet ayarlarını dəyişmək üçün toxunun"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Vidcet ayarlarını dəyişin"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Tətbiqləri axtarın"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Admininiz tərəfindən deaktiv edilib"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Əsas ekran çevrilsin"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Telefon çevrilən zaman"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Bildiriş nöqtələri"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Aktiv"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Deaktiv"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> quraşdırır, <xliff:g id="PROGRESS">%2$s</xliff:g> tamamlanıb"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> endirilir, <xliff:g id="PROGRESS">%2$s</xliff:g> tamamlandı"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> yüklənmək üçün gözləyir"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> arxivləndi. Toxunaraq endirin və bərpa edin."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> arxivləndi."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"endirin və bərpa edin"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Tətbiqin güncəllənməsi tələb edilir"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Bu ikona üçün tətbiq güncəllənməyib. Bu qısayolu yenidən aktivləşdirmək üçün manual olaraq güncəlləyə və ya ikonanı silə bilərsiniz."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Güncəlləyin"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Anladım"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"İş tətbiqlərini durdurun"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Davam etdirin"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filtr"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Alınmadı: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Məxfi sahə"</string>
diff --git a/res/values-b+sr+Latn/strings.xml b/res/values-b+sr+Latn/strings.xml
index 4d4764e..e8b55b7 100644
--- a/res/values-b+sr+Latn/strings.xml
+++ b/res/values-b+sr+Latn/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Novi prozor"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Sačuvaj par aplikacija"</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">"Ovaj par aplikacija nije podržan na ovom uređaju"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Pravljenje beležaka"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Dodaj"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Dodajte vidžet <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Dodirnite da biste promenili podešavanja vidžeta"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Promenite podešavanja vidžeta"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Pretražite aplikacije"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Administrator je onemogućio"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Dozvoli rotaciju početnog ekrana"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Kada se telefon rotira"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Tačke za obaveštenja"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Uključeno"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Isključeno"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> se instalira, <xliff:g id="PROGRESS">%2$s</xliff:g> gotovo"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> se preuzima, završeno je <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> čeka na instaliranje"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"Aplikacija <xliff:g id="NAME">%1$s</xliff:g> je arhivirana. Dodirnite da biste je preuzeli i vratili."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"Aplikacija <xliff:g id="NAME">%1$s</xliff:g> je arhivirana."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"preuzmite i vratite"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Treba da ažurirate aplikaciju"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Aplikacija za ovu ikonu nije ažurirana. Možete da je ručno ažurirate da biste ponovo omogućili ovu prečicu ili uklonite ikonu."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Ažuriraj"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Važi"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Pauziraj poslovne aplikacije"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Ponovo aktiviraj"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filter"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Nije uspelo: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Privatni prostor"</string>
diff --git a/res/values-be/strings.xml b/res/values-be/strings.xml
index 641509e..6d714bc 100644
--- a/res/values-be/strings.xml
+++ b/res/values-be/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Новае акно"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Захаваць спалучэнне праграм"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Дадзенае спалучэнне праграм не падтрымліваецца на гэтай прыладзе"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Стварэнне нататак"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Дадаць"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Дадаць віджэт \"<xliff:g id="WIDGET_NAME">%1$s</xliff:g>\""</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Націсніце, каб змяніць налады віджэта"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Змяніць налады віджэта"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Пошук праграм"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Адключаная адміністратарам"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Дазволіць паварот галоўнага экрана"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Пры павароце тэлефона"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Значкі апавяшчэнняў"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Уключана"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Выкл."</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"Усталёўваецца праграма \"<xliff:g id="NAME">%1$s</xliff:g>\", завершана <xliff:g id="PROGRESS">%2$s</xliff:g>"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"Ідзе спампоўка <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> чакае ўсталёўкі"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"Праграма \"<xliff:g id="NAME">%1$s</xliff:g>\" знаходзіцца ў архіве. Націсніце, каб спампаваць яе і аднавіць."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"Праграма \"<xliff:g id="NAME">%1$s</xliff:g>\" знаходзіцца ў архіве."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"спампаваць і аднавіць"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Неабходна абнавіць праграму"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Гэта версія праграмы састарэла. Абнавіце праграму ўручную, каб зноў карыстацца гэтым ярлыком, або выдаліце значок."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Абнавіць"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Зразумела"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Прыпыніць працоўныя праграмы"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Актываваць"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <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>
diff --git a/res/values-bg/strings.xml b/res/values-bg/strings.xml
index 3ce3c5f..191ef03 100644
--- a/res/values-bg/strings.xml
+++ b/res/values-bg/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Нов прозорец"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Запазване на двойката приложения"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Тази двойка приложения не се поддържа на устройството"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Водене на бележки"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Добавяне"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Добавяне на приспособлението „<xliff:g id="WIDGET_NAME">%1$s</xliff:g>“"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Докоснете, за да промените настройките на приспособлението"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Промяна на настройките на приспособлението"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Търсене в приложенията"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Деактивирано от администратора ви"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Разрешаване на завъртането на началния екран"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"При завъртане на телефона"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Точки за известия"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Вкл."</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Изкл."</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> се инсталира, <xliff:g id="PROGRESS">%2$s</xliff:g> завършено"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<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> изчаква инсталиране"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"Приложението <xliff:g id="NAME">%1$s</xliff:g> е архивирано. Докоснете за изтегляне и възстановяване."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"Приложението <xliff:g id="NAME">%1$s</xliff:g> е архивирано."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"изтегляне и възстановяване"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Изисква се актуализация на приложението"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Приложението за тази икона не е актуализирано. Можете да го актуализирате ръчно, за да активирате отново този пряк път, или да премахнете иконата."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Актуализиране"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Разбрах"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Поставяне на пауза на служебните приложения"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Отмяна на паузата"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <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>
diff --git a/res/values-bn/strings.xml b/res/values-bn/strings.xml
index 9b23590..43834af 100644
--- a/res/values-bn/strings.xml
+++ b/res/values-bn/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"নতুন উইন্ডো"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"অ্যাপ পেয়ার সেভ করুন"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"এই ডিভাইসে এই অ্যাপ পেয়ারটি কাজ করে না"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"নোট নেওয়া"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"যোগ করুন"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> উইজেট যোগ করুন"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"উইজেট সেটিংস পরিবর্তন করতে ট্যাপ করুন"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"উইজেট সেটিংস পরিবর্তন করুন"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"অ্যাপ খুঁজুন"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"আপনার প্রশাসক দ্বারা অক্ষম করা হয়েছে"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"হোম স্ক্রিন রোটেট করার অনুমতি দিন"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"যখন ফোনটি ঘোরানো হয়"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"বিজ্ঞপ্তি ডট"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"চালু করা আছে"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"বন্ধ"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> ইনস্টল করা হচ্ছে, <xliff:g id="PROGRESS">%2$s</xliff:g> সম্পূর্ণ হয়েছে"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<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> ইনস্টলের অপেক্ষায় রয়েছে"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> আর্কাইভ করা হয়েছে। ডাউনলোড করতে এবং ফিরিয়ে আনতে ট্যাপ করুন।"</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> আর্কাইভ করা হয়েছে।"</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"ডাউনলোড করুন ও ফিরিয়ে আনুন"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"অ্যাপটি আপডেট করা প্রয়োজন"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"এই আইকনের জন্য অ্যাপটি আপডেট করা নেই। এই শর্টকার্ট আবার চালু করতে, আপনি ম্যানুয়ালি আপডেট করতে বা সরিয়ে দিতে পারবেন।"</string>
     <string name="dialog_update" msgid="2178028071796141234">"আপডেট করুন"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"বুঝেছি"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"অফিসের অ্যাপ পজ করুন"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"আনপজ করুন"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <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>
diff --git a/res/values-bs/strings.xml b/res/values-bs/strings.xml
index 4a34da7..0beeafe 100644
--- a/res/values-bs/strings.xml
+++ b/res/values-bs/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Novi prozor"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Sačuvaj par aplikacija"</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">"Par aplikacija nije podržan na uređaju"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Pisanje bilješki"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Dodajte"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Dodavanje vidžeta <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Dodirnite da promijenite postavke vidžeta"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Promjena postavki vidžeta"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Pretražite aplikacije"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Onemogućio vaš administrator"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Dozvoli rotiranje početnog ekrana"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Kada se telefon zarotira"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Tačke za obavještenja"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Uključeno"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Isključeno"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"Instaliranje aplikacije <xliff:g id="NAME">%1$s</xliff:g>, završeno je <xliff:g id="PROGRESS">%2$s</xliff:g>"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> se preuzima, završeno <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> čeka da se instalira"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"Arhivirana je aplikacija <xliff:g id="NAME">%1$s</xliff:g>. Dodirnite da je preuzmete i vratite."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"Arhivirana je aplikacija <xliff:g id="NAME">%1$s</xliff:g>."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"preuzimanje i vraćanje"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Potrebno je ažurirati aplikaciju"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Aplikacija za ovu ikonu nije ažurirana. Možete je ažurirati ručno da ponovo omogućite ovu prečicu ili možete ukloniti ikonu."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Ažuriraj"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Razumijem"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Pauziraj poslovne aplikacije"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Ponovo pokreni"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filtrirajte"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Nije uspjelo: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Privatni prostor"</string>
diff --git a/res/values-ca/strings.xml b/res/values-ca/strings.xml
index c341ec7..94a7b41 100644
--- a/res/values-ca/strings.xml
+++ b/res/values-ca/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Finestra nova"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Desa la parella d\'aplicacions"</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">"Aquesta parella d\'aplicacions no s\'admet en aquest dispositiu"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Presa de notes"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Afegeix"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Afegeix el widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Toca per canviar la configuració del widget"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Canvia la configuració del widget"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Cerca aplicacions"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Desactivada per l\'administrador"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Permet la rotació de la pantalla d\'inici"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"En girar el telèfon"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Punts de notificació"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Activats"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Desactivats"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"S\'està instal·lant <xliff:g id="NAME">%1$s</xliff:g>; s\'ha completat un <xliff:g id="PROGRESS">%2$s</xliff:g>"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"S\'està baixant <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="PROGRESS">%2$s</xliff:g> completat"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"S\'està esperant per instal·lar <xliff:g id="NAME">%1$s</xliff:g>"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"L\'aplicació <xliff:g id="NAME">%1$s</xliff:g> està arxivada. Toca per baixar-la i restaurar-la."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"L\'aplicació <xliff:g id="NAME">%1$s</xliff:g> està arxivada."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"baixa i restaura"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Cal actualitzar l\'aplicació"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"L\'aplicació d\'aquesta icona no està actualitzada. Pots actualitzar-la manualment per tornar a activar aquesta drecera o pots suprimir la icona."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Actualitza"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Entesos"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Posa en pausa les aplicacions de treball"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Reactiva"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filtra"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Error: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Espai privat"</string>
diff --git a/res/values-cs/strings.xml b/res/values-cs/strings.xml
index d3512c9..93017bf 100644
--- a/res/values-cs/strings.xml
+++ b/res/values-cs/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Nové okno"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Uložit dvojici aplikací"</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">"Tento pár aplikací není na tomto zařízení podporován"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Psaní poznámek"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Přidat"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Přidat widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Klepnutím změníte nastavení widgetu"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Změnit nastavení widgetu"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Hledat v aplikacích"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Zakázáno administrátorem"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Povolit otáčení plochy"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Při otočení telefonu"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Puntíky s oznámením"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Zapnuto"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Vypnuto"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"Instalace aplikace <xliff:g id="NAME">%1$s</xliff:g>, dokončeno <xliff:g id="PROGRESS">%2$s</xliff:g>"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"Stahování aplikace <xliff:g id="NAME">%1$s</xliff:g> (dokončeno <xliff:g id="PROGRESS">%2$s</xliff:g>)"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"Instalace aplikace <xliff:g id="NAME">%1$s</xliff:g> čeká na zahájení"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"Aplikace <xliff:g id="NAME">%1$s</xliff:g> je archivována. Klepnutím ji můžete stáhnout a obnovit."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"Aplikace <xliff:g id="NAME">%1$s</xliff:g> je archivována."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"stáhnout a obnovit"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Je nutná aktualizace aplikace"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Aplikace pro tuto ikonu není nainstalována. Můžete ji ručně aktualizovat, aby zkratka znovu fungovala, případně můžete ikonu odstranit."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Aktualizovat"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"OK"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Pozastavit pracovní aplikace"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Zrušit pozastavení"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filtr"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Selhalo: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Soukromý prostor"</string>
diff --git a/res/values-da/strings.xml b/res/values-da/strings.xml
index 8aae860..c22d0dc 100644
--- a/res/values-da/strings.xml
+++ b/res/values-da/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Nyt vindue"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Gem appsammenknytning"</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">"Denne appsammenknytning understøttes ikke på enheden"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Notetagning"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Tilføj"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Tilføj <xliff:g id="WIDGET_NAME">%1$s</xliff:g>-widget"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Tryk for at ændre widgetindstillinger"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Skift widgetindstillinger"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Søg efter apps"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Deaktiveret af din administrator"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Tillad rotation af startskærmen"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Når telefonen roteres"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Notifikationsprikker"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Til"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Fra"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> installeres. <xliff:g id="PROGRESS">%2$s</xliff:g> fuldført"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> downloades. <xliff:g id="PROGRESS">%2$s</xliff:g> er gennemført"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> venter på at installere"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> er arkiveret Tryk for at downloade og gendanne."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> er arkiveret."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"download og gendan"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Appen skal opdateres"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Appen, der tilhører dette ikon, er ikke opdateret. Du kan opdatere appen manuelt for at genaktivere denne genvej, eller du kan fjerne ikonet."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Opdater"</string>
@@ -173,7 +185,7 @@
     <string name="action_deep_shortcut" msgid="2864038805849372848">"Genveje"</string>
     <string name="action_dismiss_notification" msgid="5909461085055959187">"Afvis"</string>
     <string name="accessibility_close" msgid="2277148124685870734">"Luk"</string>
-    <string name="all_apps_personal_tab" msgid="4190252696685155002">"Personlige"</string>
+    <string name="all_apps_personal_tab" msgid="4190252696685155002">"Personlig"</string>
     <string name="all_apps_work_tab" msgid="4884822796154055118">"Arbejde"</string>
     <string name="work_profile_toggle_label" msgid="3081029915775481146">"Arbejdsprofil"</string>
     <string name="work_profile_edu_work_apps" msgid="7895468576497746520">"Arbejdsapps har badges og kan ses af din it-administrator"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"OK"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Sæt arbejdsapps på pause"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Genoptag"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filter"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Mislykket: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Privat område"</string>
diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml
index 374f5a1..21ada28 100644
--- a/res/values-de/strings.xml
+++ b/res/values-de/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Neues Fenster"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"App-Paar speichern"</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">"Dieses App-Paar wird auf diesem Gerät nicht unterstützt"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Notizen"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Hinzufügen"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Widget „<xliff:g id="WIDGET_NAME">%1$s</xliff:g>“ hinzufügen"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Tippen, um die Widget-Einstellungen zu ändern"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Widget-Einstellungen ändern"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Apps finden"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Von deinem Administrator deaktiviert"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Drehen des Startbildschirms zulassen"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Beim Drehen des Smartphones"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"App-Benachrichtigungspunkte"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"An"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Aus"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> wird installiert, <xliff:g id="PROGRESS">%2$s</xliff:g> abgeschlossen"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> wird heruntergeladen, <xliff:g id="PROGRESS">%2$s</xliff:g> abgeschlossen"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"Warten auf Installation von <xliff:g id="NAME">%1$s</xliff:g>"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> ist archiviert. Tippe, um die App herunterzuladen und wiederherzustellen."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> ist archiviert."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"herunterladen und wiederherstellen"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"App-Update erforderlich"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Die App für dieses Symbol wurde noch nicht aktualisiert. Du kannst sie manuell aktualisieren, um die Verknüpfung wieder zu aktivieren, oder das Symbol entfernen."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Aktualisieren"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"OK"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Geschäftliche Apps pausieren"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Nicht mehr pausieren"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filter"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Fehler: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Vertrauliches Profil"</string>
diff --git a/res/values-el/strings.xml b/res/values-el/strings.xml
index cafe86e..1e18977 100644
--- a/res/values-el/strings.xml
+++ b/res/values-el/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Νέο παράθυρο"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Αποθήκευση ζεύγους εφαρμογών"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Αυτό το ζεύγος εφαρμογών δεν υποστηρίζεται σε αυτή τη συσκευή"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Δημιουργία σημειώσεων"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Προσθήκη"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Προσθήκη του γραφικού στοιχείου <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Πατήστε για αλλαγή των ρυθμίσεων του γραφικού στοιχείου"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Αλλαγή ρυθμίσεων γραφικού στοιχείου"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Αναζήτηση εφαρμογών"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Απενεργοποιήθηκε από τον διαχειριστή σας"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Να επιτρέπεται η περιστροφή της αρχικής οθόνης"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Όταν το τηλέφωνο περιστρέφεται"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Κουκκίδες ειδοποίησης"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Ενεργοποίηση"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Απενεργοποίηση"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"Έχει ολοκληρωθεί το <xliff:g id="PROGRESS">%2$s</xliff:g> της εγκατάστασης της εφαρμογής <xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"Λήψη <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> σε αναμονή για εγκατάσταση"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"Η εφαρμογή <xliff:g id="NAME">%1$s</xliff:g> είναι αρχειοθετημένη. Πατήστε για λήψη και επαναφορά."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"Η εφαρμογή <xliff:g id="NAME">%1$s</xliff:g> είναι αρχειοθετημένη."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"λήψη και επαναφορά"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Απαιτείται ενημέρωση της εφαρμογής"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Η εφαρμογή για αυτό το εικονίδιο δεν έχει ενημερωθεί. Μπορείτε να την ενημερώσετε μη αυτόματα για να ενεργοποιήσετε ξανά τη συγκεκριμένη συντόμευση ή να καταργήσετε το εικονίδιο."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Ενημέρωση"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Το κατάλαβα"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Παύση εφαρμογών εργασιών"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Αναίρεση παύσης"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <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>
diff --git a/res/values-en-rAU/strings.xml b/res/values-en-rAU/strings.xml
index 1b0722d..a2a4145 100644
--- a/res/values-en-rAU/strings.xml
+++ b/res/values-en-rAU/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"New window"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Save app pair"</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">"This app pair isn\'t supported on this device"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Note-taking"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Add"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Add <xliff:g id="WIDGET_NAME">%1$s</xliff:g> widget"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Tap to change widget settings"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Change widget settings"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Search apps"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Disabled by your admin"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Allow home screen rotation"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"When phone is rotated"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Notification dots"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"On"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Off"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> installing, <xliff:g id="PROGRESS">%2$s</xliff:g> complete"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> downloading, <xliff:g id="PROGRESS">%2$s</xliff:g> complete"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> waiting to install"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> is archived. Tap to download and restore."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> is archived."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"download and restore"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"App update required"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"The app for this icon isn\'t updated. You can update manually to re-enable this shortcut or remove the icon."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Update"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"OK"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Pause work apps"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Unpause"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filter"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Failed: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Private space"</string>
diff --git a/res/values-en-rCA/strings.xml b/res/values-en-rCA/strings.xml
index de41d2c..c600680 100644
--- a/res/values-en-rCA/strings.xml
+++ b/res/values-en-rCA/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"New Window"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Save app pair"</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">"This app pair isn\'t supported on this device"</string>
@@ -68,6 +69,9 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Note-taking"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Add"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Add <xliff:g id="WIDGET_NAME">%1$s</xliff:g> widget"</string>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"Show all"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"Show all widgets"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"Showing all widgets"</string>
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Tap to change widget settings"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Change widget settings"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Search apps"</string>
@@ -123,6 +127,8 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Disabled by your admin"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Allow home screen rotation"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"When phone is rotated"</string>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"Landscape mode"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"Set phone into landscape mode"</string>
     <string name="notification_dots_title" msgid="9062440428204120317">"Notification dots"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"On"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Off"</string>
@@ -141,7 +147,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> installing, <xliff:g id="PROGRESS">%2$s</xliff:g> complete"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> downloading, <xliff:g id="PROGRESS">%2$s</xliff:g> complete"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> waiting to install"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> is archived. Tap to download and restore."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> is archived."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"download and restore"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"App update required"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"The app for this icon isn\'t updated. You can update manually to re-enable this shortcut, or remove the icon."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Update"</string>
@@ -186,6 +193,7 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Got it"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Pause work apps"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Unpause"</string>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"Work apps schedule"</string>
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filter"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Failed: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Private space"</string>
diff --git a/res/values-en-rGB/strings.xml b/res/values-en-rGB/strings.xml
index 1b0722d..a2a4145 100644
--- a/res/values-en-rGB/strings.xml
+++ b/res/values-en-rGB/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"New window"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Save app pair"</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">"This app pair isn\'t supported on this device"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Note-taking"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Add"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Add <xliff:g id="WIDGET_NAME">%1$s</xliff:g> widget"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Tap to change widget settings"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Change widget settings"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Search apps"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Disabled by your admin"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Allow home screen rotation"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"When phone is rotated"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Notification dots"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"On"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Off"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> installing, <xliff:g id="PROGRESS">%2$s</xliff:g> complete"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> downloading, <xliff:g id="PROGRESS">%2$s</xliff:g> complete"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> waiting to install"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> is archived. Tap to download and restore."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> is archived."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"download and restore"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"App update required"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"The app for this icon isn\'t updated. You can update manually to re-enable this shortcut or remove the icon."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Update"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"OK"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Pause work apps"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Unpause"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filter"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Failed: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Private space"</string>
diff --git a/res/values-en-rIN/strings.xml b/res/values-en-rIN/strings.xml
index 1b0722d..a2a4145 100644
--- a/res/values-en-rIN/strings.xml
+++ b/res/values-en-rIN/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"New window"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Save app pair"</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">"This app pair isn\'t supported on this device"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Note-taking"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Add"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Add <xliff:g id="WIDGET_NAME">%1$s</xliff:g> widget"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Tap to change widget settings"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Change widget settings"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Search apps"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Disabled by your admin"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Allow home screen rotation"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"When phone is rotated"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Notification dots"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"On"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Off"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> installing, <xliff:g id="PROGRESS">%2$s</xliff:g> complete"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> downloading, <xliff:g id="PROGRESS">%2$s</xliff:g> complete"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> waiting to install"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> is archived. Tap to download and restore."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> is archived."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"download and restore"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"App update required"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"The app for this icon isn\'t updated. You can update manually to re-enable this shortcut or remove the icon."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Update"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"OK"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Pause work apps"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Unpause"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filter"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Failed: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Private space"</string>
diff --git a/res/values-en-rXC/strings.xml b/res/values-en-rXC/strings.xml
index a856340..769567c 100644
--- a/res/values-en-rXC/strings.xml
+++ b/res/values-en-rXC/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‏‏‎‎‏‎‏‏‏‏‏‏‎‏‎‏‎‏‎‏‏‏‎‏‏‏‏‎‎‏‎‎‎‏‎‏‏‏‎‏‏‏‏‎‏‎‏‎‎‎‏‎‎‏‎‏‎‏‎‏‏‎New Window‎‏‎‎‏‎"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‏‏‎‎‏‏‏‎‎‏‏‎‎‎‎‎‎‎‎‎‏‎‎‏‎‎‎‎‎‏‎‏‏‏‏‎‎‏‏‎‎‎‎‏‏‏‎‎‎‎‎‎‏‏‎‎‎‎‏‎‎‎‏‏‎Save app pair‎‏‎‎‏‎"</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">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‏‏‏‏‎‏‎‏‏‏‏‏‏‎‏‎‏‎‎‏‎‎‏‏‏‎‎‎‏‏‎‎‏‎‏‏‎‏‏‎‏‎‏‏‏‏‎‎‏‏‎‏‎‎‎‎‏‎‎‏‏‎‎‎‎‎‎‎This app pair isn\'t supported on this device‎‏‎‎‏‎"</string>
diff --git a/res/values-es-rUS/strings.xml b/res/values-es-rUS/strings.xml
index ba1b0af..44147c2 100644
--- a/res/values-es-rUS/strings.xml
+++ b/res/values-es-rUS/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Ventana nueva"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Guardar vinculación"</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">"No se admite esta vinculación de apps en este dispositivo"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Tomar notas"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Agregar"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Agregar widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Presiona para cambiar la configuración del widget"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Cambiar la configuración del widget"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Buscar apps"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"El administrador inhabilitó esta función"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Permitir la rotación de la pantalla principal"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Al girar el teléfono"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Puntos de notificación"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Activados"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Desactivados"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"Se está instalando <xliff:g id="NAME">%1$s</xliff:g>; <xliff:g id="PROGRESS">%2$s</xliff:g> completado"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"Se completó el <xliff:g id="PROGRESS">%2$s</xliff:g> de la descarga de <xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"Instalación de <xliff:g id="NAME">%1$s</xliff:g> en espera"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> está archivada. Presiona para descargar y restablecer."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> está archivada."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"descargar y restablecer"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Es necesario actualizar la app"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"No se actualizó la app de este ícono. Puedes actualizarla manualmente para rehabilitar el acceso directo, o bien quitar el ícono."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Actualizar"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Entendido"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Detener apps de trabajo"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Reanudar"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filtro"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Error: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Espacio privado"</string>
diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml
index ad12192..40e04d5 100644
--- a/res/values-es/strings.xml
+++ b/res/values-es/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Ventana nueva"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Guardar apps emparejadas"</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">"El dispositivo no admite esta aplicación emparejada"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Toma de notas"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Añadir"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Añadir widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Toca para cambiar los ajustes del widget"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Cambiar ajustes del widget"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Buscar aplicaciones"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Inhabilitado por el administrador"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Permitir rotación de la pantalla de inicio"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Al girar el teléfono"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Burbujas de notificación"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Activado"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Desactivadas"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"Instalando <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="PROGRESS">%2$s</xliff:g> completado"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"Descargando <xliff:g id="NAME">%1$s</xliff:g> (<xliff:g id="PROGRESS">%2$s</xliff:g> completado)"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"Esperando para instalar <xliff:g id="NAME">%1$s</xliff:g>"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> está archivada. Toca para descargar y restaurar."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> está archivada."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"descargar y restaurar"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Debes actualizar la aplicación"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"La aplicación de este icono no está actualizada. Puedes actualizarla manualmente para volver a habilitar este acceso directo o puedes eliminar el icono."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Actualizar"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Entendido"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Pausar aplicaciones de trabajo"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Reanudar"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filtro"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Se ha producido un error: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Espacio privado"</string>
diff --git a/res/values-et/strings.xml b/res/values-et/strings.xml
index 96d0b2c..de62de1 100644
--- a/res/values-et/strings.xml
+++ b/res/values-et/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Uus aken"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Salvesta rakendusepaar"</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">"See rakendusepaar ei ole selles seadmes toetatud"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Märkmete tegemine"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Lisa"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Lisa vidin <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Puudutage vidina seadete muutmiseks"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Vidina seadete muutmine"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Otsige rakendusi"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Keelas administraator"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Luba avakuva pööramine"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Kui telefoni pööratakse"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Märguandetäpid"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Sees"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Väljas"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"Üksust <xliff:g id="NAME">%1$s</xliff:g> installitakse, <xliff:g id="PROGRESS">%2$s</xliff:g> on valmis"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"Rakenduse <xliff:g id="NAME">%1$s</xliff:g> allalaadimine, <xliff:g id="PROGRESS">%2$s</xliff:g> on valmis"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> on installimise ootel"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> on arhiivitud. Puudutage allalaadimiseks ja taastamiseks."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> on arhiivitud."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"laadi alla ja taasta"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Rakendust tuleb värskendada"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Selle ikooni rakendust pole värskendatud. Otsetee uuesti lubamiseks võite rakendust käsitsi värskendada või ikooni eemaldada."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Värskenda"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Selge"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Peata töörakendused"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Lõpeta peatamine"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filter"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Nurjus: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Privaatne ruum"</string>
diff --git a/res/values-eu/strings.xml b/res/values-eu/strings.xml
index bc9b8c1..27510b3 100644
--- a/res/values-eu/strings.xml
+++ b/res/values-eu/strings.xml
@@ -31,9 +31,10 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Leiho berria"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Gorde aplikazio parea"</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">"Aplikazio pare hori ez da onartzen gailu honetan"</string>
+    <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Aplikazio pare hori ez da gailu honekin bateragarria"</string>
     <string name="app_pair_needs_unfold" msgid="4588897528143807002">"Zabaldu gailua aplikazio pare hau erabiltzeko"</string>
     <string name="app_pair_not_available" msgid="3556767440808032031">"Aplikazio parea ez dago erabilgarri"</string>
     <string name="long_press_widget_to_add" msgid="3587712543577675817">"Eduki sakatuta widget bat mugitzeko."</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Oharrak idazteko"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Gehitu"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Gehitu <xliff:g id="WIDGET_NAME">%1$s</xliff:g> widgeta"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Sakatu hau widgeten ezarpenak aldatzeko"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Aldatu widgeten ezarpenak"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Bilatu aplikazioetan"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Administratzaileak desgaitu du"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Eman orri nagusia biratzeko baimena"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Telefonoa biratzean"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Jakinarazpen-biribiltxoak"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Aktibatuta"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Desaktibatuta"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> instalatzen, <xliff:g id="PROGRESS">%2$s</xliff:g> osatuta"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> deskargatzen, <xliff:g id="PROGRESS">%2$s</xliff:g> osatuta"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> instalatzeko zain"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> artxibatuta dago. Sakatu deskargatzeko eta leheneratzeko."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> artxibatuta dago."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"deskargatu eta leheneratu"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Aplikazioa eguneratu egin behar da"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Ikonoaren aplikazioa ez dago eguneratuta. Lasterbidea berriro gaitzeko, eskuz egunera dezakezu aplikazioa. Bestela, kendu ikonoa."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Eguneratu"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Ados"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Pausatu laneko aplikazioak"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Aktibatu berriro"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Iragazi"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Huts egin du: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Eremu pribatua"</string>
diff --git a/res/values-fa/strings.xml b/res/values-fa/strings.xml
index c167194..8a21dc7 100644
--- a/res/values-fa/strings.xml
+++ b/res/values-fa/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"پنجره جدید"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"ذخیره جفت برنامه"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"از این جفت برنامه در این دستگاه پشتیبانی نمی‌شود"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"یادداشت‌برداری"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"افزودن"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"افزودن ابزاره <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"برای تغییر تنظیمات ابزاره، تک‌ضرب بزنید"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"تغییر تنظیمات ابزاره"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"جستجوی برنامه‌ها"</string>
@@ -101,7 +108,7 @@
     <string name="permdesc_write_settings" msgid="726859348127868466">"به برنامه اجازه می‌دهد تنظیمات و میان‌برهای صفحه اصلی را تغییر دهد."</string>
     <string name="gadget_error_text" msgid="740356548025791839">"ابزاره را نمی‌توان بار کرد"</string>
     <string name="gadget_setup_text" msgid="8348374825537681407">"تنظیمات ابزاره"</string>
-    <string name="gadget_complete_setup_text" msgid="309040266978007925">"برای تکمیل راه‌اندازی تک‌ضرب بزنید"</string>
+    <string name="gadget_complete_setup_text" msgid="309040266978007925">"برای تمام کردن راه‌اندازی تک‌ضرب بزنید"</string>
     <string name="uninstall_system_app_text" msgid="4172046090762920660">"این برنامه سیستمی است و حذف نصب نمی‌شود."</string>
     <string name="folder_hint_text" msgid="5174843001373488816">"ویرایش نام"</string>
     <string name="disabled_app_label" msgid="6673129024321402780">"<xliff:g id="APP_NAME">%1$s</xliff:g> غیرفعال شد"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"توسط سرپرست سیستم غیرفعال شده است"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"مجاز کردن چرخش صفحه اصلی"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"وقتی تلفن چرخانده می‌شود"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"نقطه‌های اعلان"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"روشن"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"خاموش"</string>
@@ -130,7 +141,7 @@
     <string name="msg_missing_notification_access" msgid="281113995110910548">"برای نمایش «نقطه‌های اعلان»، اعلان‌های برنامه را برای <xliff:g id="NAME">%1$s</xliff:g> روشن کنید"</string>
     <string name="title_change_settings" msgid="1376365968844349552">"تغییر تنظیمات"</string>
     <string name="notification_dots_service_title" msgid="4284221181793592871">"نمایش نقطه‌های اعلان"</string>
-    <string name="developer_options_title" msgid="700788437593726194">"گزینه‌های برنامه‌نویس"</string>
+    <string name="developer_options_title" msgid="700788437593726194">"گزینه‌های توسعه‌دهندگان"</string>
     <string name="auto_add_shortcuts_label" msgid="4926805029653694105">"افزودن نماد برنامه‌ها به صفحه اصلی"</string>
     <string name="auto_add_shortcuts_description" msgid="7117251166066978730">"برای برنامه‌های جدید"</string>
     <string name="package_state_unknown" msgid="7592128424511031410">"نامشخص"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> درحال نصب است، <xliff:g id="PROGRESS">%2$s</xliff:g> تکمیل شده است"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"درحال بارگیری <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> درانتظار نصب"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> بایگانی شده است. برای بارگیری و بازیابی تک‌ضرب بزنید."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"‫<xliff:g id="NAME">%1$s</xliff:g> بایگانی شده است."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"بارگیری و بازیابی کردن"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"برنامه باید به‌روز شود"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"برنامه برای این نماد به‌روز نشده است. می‌توانید آن را به‌صورت دستی به‌روز کنید تا میان‌بر دوباره فعال شود، یا نماد را بردارید."</string>
     <string name="dialog_update" msgid="2178028071796141234">"به‌روزرسانی"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"متوجه‌ام"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"توقف موقت برنامه‌های کاری"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"ازسرگیری"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <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>
diff --git a/res/values-fi/strings.xml b/res/values-fi/strings.xml
index 007d077..c778a61 100644
--- a/res/values-fi/strings.xml
+++ b/res/values-fi/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Uusi ikkuna"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Tallenna sovelluspari"</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">"Sovellusparia ei tueta tällä laitteella"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Muistiinpanojen tekeminen"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Lisää"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Lisää widget: <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Napauta, niin voit muuttaa widgetin asetuksia"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Muuta widgetin asetuksia"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Hae sovelluksia"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Järjestelmänvalvoja on poistanut toiminnon käytöstä."</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Salli aloitusnäytön kiertäminen"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Kun puhelinta kierretään"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Pistemerkit"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Päällä"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Ei päällä"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> asennetaan, <xliff:g id="PROGRESS">%2$s</xliff:g> valmis"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> latautuu, valmiina <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> odottaa asennusta"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> on arkistoitu. Lataa ja palauta napauttamalla."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> on arkistoitu."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"lataa ja palauta"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Sovelluspäivitys vaaditaan"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Kuvakkeen sovellusta ei ole päivitetty. Voit ottaa pikakuvakkeen uudelleen käyttöön päivittämällä sovelluksen tai poistaa kuvakkeen."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Päivitä"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"OK"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Keskeytä työsovellusten käyttö"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Jatka"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Suodatin"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Epäonnistui: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Yksityinen tila"</string>
diff --git a/res/values-fr-rCA/strings.xml b/res/values-fr-rCA/strings.xml
index c443505..1f5e264 100644
--- a/res/values-fr-rCA/strings.xml
+++ b/res/values-fr-rCA/strings.xml
@@ -31,6 +31,7 @@
     <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="new_window_option_taskbar" msgid="6448780542727767211">"Nouvelle fenêtre"</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\'applis n\'est pas prise en charge sur cet appareil"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Prise de note"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Ajouter"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Ajoutez le widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <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 applis"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Cette fonction est désactivée par votre administrateur"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Autoriser la rotation de l\'écran d\'accueil"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Lorsque vous faites pivoter le téléphone"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Pastilles de notification"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Activé"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Désactivé"</string>
@@ -141,7 +152,8 @@
     <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="app_archived_title" msgid="4548283110222420708">"L\'appli <xliff:g id="NAME">%1$s</xliff:g> est archivée."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"télécharger et restaurer"</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>
@@ -186,12 +198,14 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"OK"</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>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <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>
     <string name="private_space_label" msgid="2359721649407947001">"Espace privé"</string>
     <string name="private_space_secondary_label" msgid="9203933341714508907">"Touchez pour configurer ou ouvrir"</string>
     <string name="ps_container_title" msgid="4391796149519594205">"Privé"</string>
-    <string name="ps_container_settings" msgid="6059734123353320479">"Paramètres de l\'Espace privé"</string>
+    <string name="ps_container_settings" msgid="6059734123353320479">"Paramètres de l\'espace privé"</string>
     <string name="ps_container_unlock_button_content_description" msgid="9181551784092204234">"Privé, déverrouillé."</string>
     <string name="ps_container_lock_button_content_description" msgid="5961993384382649530">"Privé, verrouillé."</string>
     <string name="ps_container_lock_title" msgid="2640257399982364682">"Verrouiller"</string>
diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml
index 4f5d111..a5b9a32 100644
--- a/res/values-fr/strings.xml
+++ b/res/values-fr/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Nouvelle fenêtre"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Enregistrer une 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>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Prise de notes"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Ajouter"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Ajoutez un widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Appuyez 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>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Désactivé par votre administrateur"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Autoriser la rotation de l\'écran d\'accueil"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Lorsque vous faites pivoter le téléphone"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Pastilles de notification"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Activées"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Désactivées"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"Installation de <xliff:g id="NAME">%1$s</xliff:g>… (<xliff:g id="PROGRESS">%2$s</xliff:g> terminés)"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> en cours de téléchargement, <xliff:g id="PROGRESS">%2$s</xliff:g> effectué(s)"</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\'application <xliff:g id="NAME">%1$s</xliff:g> est archivée. Appuyez pour la télécharger et la restaurer."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"L\'application <xliff:g id="NAME">%1$s</xliff:g> est archivée."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"télécharger et restaurer"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Mise à jour de l\'appli requise"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"L\'appli correspondant à cette icône n\'est pas mise à jour. Vous pouvez la mettre à jour manuellement pour réactiver le raccourci ou supprimer l\'icône."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Modifier"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"OK"</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>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filtre"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Échec : <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Espace privé"</string>
diff --git a/res/values-gl/strings.xml b/res/values-gl/strings.xml
index ff7c029..ca2861a 100644
--- a/res/values-gl/strings.xml
+++ b/res/values-gl/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Ventá nova"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Gardar parella de apps"</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">"O dispositivo non admite este emparellamento de aplicacións"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Toma de notas"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Engadir"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Engadir o widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Toca para cambiar a configuración do widget"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Cambiar configuración do widget"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Buscar aplicacións"</string>
@@ -119,10 +126,14 @@
     <string name="app_pair_name_format" msgid="8134106404716224054">"Emparellamento de aplicacións: <xliff:g id="APP1">%1$s</xliff:g> e <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"Estilo e fondo de pantalla"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"Editar pantalla de inicio"</string>
-    <string name="settings_button_text" msgid="8873672322605444408">"Axustes de Inicio"</string>
+    <string name="settings_button_text" msgid="8873672322605444408">"Configuración da pantalla de inicio"</string>
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Función desactivada polo administrador"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Permitir xirar a pantalla de inicio"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Ao xirar o teléfono"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Puntos de notificacións"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Opción activada"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Desactivados"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"Instalando <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="PROGRESS">%2$s</xliff:g> completado"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"Descargando <xliff:g id="NAME">%1$s</xliff:g> (<xliff:g id="PROGRESS">%2$s</xliff:g> completado)"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"Esperando para instalar <xliff:g id="NAME">%1$s</xliff:g>"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> está no arquivo. Toca para descargar e restaurar."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> está arquivada."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"descargar e restaurar"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"É necesario actualizar a aplicación"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"A aplicación á que corresponde esta icona non está actualizada. Podes actualizala manualmente para activar de novo este atallo, ou ben quitar a icona."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Actualizar"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Entendido"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Pór en pausa aplicacións do traballo"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Volver activar"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filtra"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Erro: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Espazo privado"</string>
diff --git a/res/values-gu/strings.xml b/res/values-gu/strings.xml
index 872faef..59e3134 100644
--- a/res/values-gu/strings.xml
+++ b/res/values-gu/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"નવી વિન્ડો"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"ઍપની જોડી સાચવો"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"આ ડિવાઇસ પર, આ ઍપની જોડીને સપોર્ટ આપવામાં આવતો નથી"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"નોંધ લેવી"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"ઉમેરો"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> વિજેટ ઉમેરો"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"વિજેટના સેટિંગ બદલવા માટે ટૅપ કરો"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"વિજેટના સેટિંગ બદલો"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"ઍપ શોધો"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"તમારા વ્યવસ્થાપક દ્વારા અક્ષમ કરેલ"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"હોમ સ્ક્રીનને ફેરવવાની મંજૂરી આપો"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"જ્યારે ફોન ફેરવવામાં આવે ત્યારે"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"નોટિફિકેશન માટેના ચિહ્નો"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"ચાલુ છે"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"બંધ છે"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> ઇન્સ્ટૉલ કરી રહ્યાં છીએ, <xliff:g id="PROGRESS">%2$s</xliff:g> પૂર્ણ થયું"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<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>, ઇન્સ્ટૉલ થવાની રાહ જોઈ રહ્યું છે"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g>ને આર્કાઇવ કર્યું છે. ડાઉનલોડ અને રિસ્ટોર કરવા માટે ટૅપ કરો."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g>ને આર્કાઇવ કર્યું છે."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"ડાઉનલોડ અને રિસ્ટોર કરો"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"ઍપને અપડેટ કરવી જરૂરી છે"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"આ આઇકન માટે ઍપ અપડેટ કરવામાં આવી નથી. તમે આ શૉર્ટકટ ફરી ચાલુ કરવા અથવા આઇકન કાઢી નાખવા માટે ઍપને મેન્યુઅલી અપડેટ કરી શકો છો."</string>
     <string name="dialog_update" msgid="2178028071796141234">"અપડેટ કરો"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"સમજાઈ ગયું"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"ઑફિસની ઍપ થોભાવો"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"ફરી ચાલુ કરો"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <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>
diff --git a/res/values-hi/strings.xml b/res/values-hi/strings.xml
index a44b874..c26cafb 100644
--- a/res/values-hi/strings.xml
+++ b/res/values-hi/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"नई विंडो"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"ऐप पेयर सेव करें"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"साथ में इस्तेमाल किए जा सकने वाले ये ऐप्लिकेशन, इस डिवाइस पर काम नहीं कर सकते"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"नोट बनाने से जुड़े विजेट"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"जोड़ें"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> विजेट जोड़ें"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"विजेट की सेटिंग में बदलाव करने के लिए टैप करें"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"विजेट की सेटिंग में बदलाव करें"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"ऐप्लिकेशन खोजें"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"आपके एडमिन ने बंद किया हुआ है"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"होम स्क्रीन घुमाने की अनुमति दें"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"फ़ोन घुुमाए जाने पर"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"सूचनाएं बताने वाला डॉट"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"चालू है"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"चालू"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> इंस्टॉल किया जा रहा है, <xliff:g id="PROGRESS">%2$s</xliff:g> पूरा हो गया"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<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> के इंस्टॉल होने की प्रतीक्षा की जा रही है"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> को संग्रहित किया गया. ऐप्लिकेशन को वापस लाने और डाउनलोड करने के लिए टैप करें."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> को संग्रहित किया गया."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"डाउनलोड करें और वापस लाएं"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"ऐप्लिकेशन को अपडेट करना ज़रूरी है"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"इस आइकॉन का ऐप्लिकेशन अपडेट नहीं है. इस शॉर्टकट को फिर से चालू करने या आइकॉन को हटाने के लिए, ऐप्लिकेशन को मैन्युअल रूप से अपडेट किया जा सकता है."</string>
     <string name="dialog_update" msgid="2178028071796141234">"अपडेट करें"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"ठीक है"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"वर्क ऐप्लिकेशन रोकें"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"चालू करें"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <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>
diff --git a/res/values-hr/strings.xml b/res/values-hr/strings.xml
index f62384c..4a29979 100644
--- a/res/values-hr/strings.xml
+++ b/res/values-hr/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Novi prozor"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Spremi par aplikacija"</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">"Taj par aplikacija nije podržan na ovom uređaju"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Pisanje bilježaka"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Dodaj"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Dodaj widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Dodirnite da biste promijenili postavke widgeta"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Promijenite postavke widgeta"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Pretraži aplikacije"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Onemogućio administrator"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Dopusti zakretanje početnog zaslona"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Kada se telefon zakrene"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Točke obavijesti"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Uključeno"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Isključeno"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"Instaliranje aplikacije <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="PROGRESS">%2$s</xliff:g> dovršeno"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"Preuzimanje aplikacije <xliff:g id="NAME">%1$s</xliff:g>, dovršeno <xliff:g id="PROGRESS">%2$s</xliff:g>"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"Čekanje na instaliranje aplikacije <xliff:g id="NAME">%1$s</xliff:g>"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"Aplikacija <xliff:g id="NAME">%1$s</xliff:g> je arhivirana. Dodirnite da biste je preuzeli i vratili."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"Aplikacija <xliff:g id="NAME">%1$s</xliff:g> je arhivirana."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"preuzmi i vrati"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Aplikacija se treba ažurirati"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Aplikacija ove ikone nije ažurirana. Možete ručno ažurirati da biste ponovo omogućili ovaj prečac ili uklonite ikonu."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Ažuriraj"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Shvaćam"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Pauziraj poslovne aplikacije"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Ponovno pokreni"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filtrirajte"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Nije uspjelo: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Privatni prostor"</string>
diff --git a/res/values-hu/strings.xml b/res/values-hu/strings.xml
index 6bc8b70..aa1167d 100644
--- a/res/values-hu/strings.xml
+++ b/res/values-hu/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Új ablak"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Alkalmazáspár mentése"</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">"Ezt az alkalmazáspárt nem támogatja az eszköz"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Jegyzetelés"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Hozzáadás"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> modul hozzáadása"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Ide koppintva módosíthatja a modulbeállításokat"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"A modulbeállítások módosítása"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Alkalmazások keresése"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"A rendszergazda letiltotta"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"A kezdőképernyő elforgatásának engedélyezése"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"A telefon elforgatásakor"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Értesítési pöttyök"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Be"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Ki"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"Folyamatban van a(z) <xliff:g id="NAME">%1$s</xliff:g> telepítése, <xliff:g id="PROGRESS">%2$s</xliff:g> kész"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"A(z) <xliff:g id="NAME">%1$s</xliff:g> letöltése, <xliff:g id="PROGRESS">%2$s</xliff:g> kész"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"A(z) <xliff:g id="NAME">%1$s</xliff:g> telepítésre vár"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> archiválva. Koppintson a letöltéshez és a visszaállításhoz."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> archiválva."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"letöltés és visszaállítás"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Alkalmazásfrissítés szükséges"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Az ikonhoz tartozó alkalmazás nincs frissítve. A parancsikon újbóli engedélyezéséhez frissítse az alkalmazást, vagy távolítsa ez az ikont."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Frissítés"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Értem"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Munkahelyi alkalmazások szüneteltetése"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Folytatás"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Szűrő"</string>
     <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>
diff --git a/res/values-hy/strings.xml b/res/values-hy/strings.xml
index 69b320d..692984e 100644
--- a/res/values-hy/strings.xml
+++ b/res/values-hy/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Նոր պատուհան"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Պահել հավելվ. զույգը"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Հավելվածների զույգը չի աջակցվում այս սարքում"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Նշումների ստեղծում"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Ավելացնել"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Ավելացնել <xliff:g id="WIDGET_NAME">%1$s</xliff:g> վիջեթը"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Հպեք՝ վիջեթի կարգավորումները փոփոխելու համար"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Փոխել վիջեթի կարգավորումները"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Որոնել հավելվածներ"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Անջատվել է ձեր ադմինիստրատորի կողմից"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Թույլ տալ հիմնական էկրանի պտտումը"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Հեռախոսը պտտելու դեպքում"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Ծանուցումների կետիկներ"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Միացված է"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Անջատված է"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> հավելվածը տեղադրվում է, կատարված է <xliff:g id="PROGRESS">%2$s</xliff:g>-ը"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<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>-ի տեղադրման սպասում"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> հավելվածն արխիվացված է։ Հպեք՝ ներբեռնելու և վերականգնելու համար։"</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> հավելվածն արխիվացված է։"</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"ներբեռնել և վերականգնել"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Պահանջվում է թարմացնել հավելվածը"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Հավելվածը հնացել է։ Թարմացրեք այն ձեռքով, որպեսզի շարունակեք օգտագործել դյուրանցումը, կամ հեռացրեք հավելվածի պատկերակը։"</string>
     <string name="dialog_update" msgid="2178028071796141234">"Թարմացնել"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Եղավ"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Դադարեցնել աշխատանքային հավելվածները"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Վերսկսել"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <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>
diff --git a/res/values-in/strings.xml b/res/values-in/strings.xml
index 58a429f..1870055 100644
--- a/res/values-in/strings.xml
+++ b/res/values-in/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Jendela Baru"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Simpan pasangan aplikasi"</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">"Pasangan aplikasi ini tidak didukung di perangkat ini"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Pembuatan catatan"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Tambahkan"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Tambahkan widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Ketuk untuk mengubah setelan widget"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Ubah setelan widget"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Telusuri aplikasi"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Dinonaktifkan oleh admin"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Izinkan layar utama diputar"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Saat ponsel diputar"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Titik notifikasi"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Aktif"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Nonaktif"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> sedang diinstal, <xliff:g id="PROGRESS">%2$s</xliff:g> selesai"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> sedang didownload, <xliff:g id="PROGRESS">%2$s</xliff:g> selesai"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> menunggu dipasang"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> diarsipkan. Ketuk untuk mendownload dan memulihkan."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> diarsipkan."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"download dan pulihkan"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Aplikasi perlu diupdate"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Aplikasi untuk ikon ini belum diupdate. Anda dapat mengupdate secara manual untuk mengaktifkan kembali pintasan ini, atau hapus ikon."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Update"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Oke"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Jeda aplikasi kerja"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Aktifkan lagi"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filter"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Gagal: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Ruang privasi"</string>
diff --git a/res/values-is/strings.xml b/res/values-is/strings.xml
index 95bd21f..393589a 100644
--- a/res/values-is/strings.xml
+++ b/res/values-is/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Nýr gluggi"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Vista forritapar"</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">"Þetta forritapar er ekki stutt í þessu tæki"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Glósugerð"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Bæta við"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Bæta græjunni <xliff:g id="WIDGET_NAME">%1$s</xliff:g> við"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Ýttu til að breyta græjustillingum"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Breyta græjustillingum"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Leita í forritum"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Gert óvirkt af kerfisstjóra"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Leyfa snúning á heimaskjá"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Þegar símanum er snúið"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Tilkynningapunktar"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Kveikt"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Slökkt"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"Setur upp <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="PROGRESS">%2$s</xliff:g> lokið"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> í niðurhali, <xliff:g id="PROGRESS">%2$s</xliff:g> lokið"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> bíður uppsetningar"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> er í geymslu. Ýttu til að sækja og endurheimta."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> er í geymslu."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"sækja og endurheimta"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Uppfæra þarf forritið"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Forritið fyrir þetta tákn er ekki uppfært. Þú getur uppfært það handvirkt til að kveikja aftur á þessari flýtileið eða fjarlægt táknið."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Uppfæra"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Ég skil"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Setja vinnuforrit í bið"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Ljúka hléi"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Sía"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Mistókst: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Leynirými"</string>
diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml
index 3c01cd4..2a14445 100644
--- a/res/values-it/strings.xml
+++ b/res/values-it/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Nuova finestra"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Salva coppia di app"</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">"Questa coppia di app non è supportata su questo dispositivo"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Aggiunta di note"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Aggiungi"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Aggiungi widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Tocca per modificare le impostazioni del widget"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Modifica le impostazioni del widget"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Cerca nelle app"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Disattivata dall\'amministratore"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Consenti rotazione della schermata Home"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Con il telefono ruotato"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Indicatori di notifica"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"On"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Off"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"Installazione di <xliff:g id="NAME">%1$s</xliff:g>, completamento: <xliff:g id="PROGRESS">%2$s</xliff:g>"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"Download di <xliff:g id="NAME">%1$s</xliff:g> in corso, <xliff:g id="PROGRESS">%2$s</xliff:g> completato"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> in attesa di installazione"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"App <xliff:g id="NAME">%1$s</xliff:g> archiviata. Tocca per scaricare e ripristinare."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"App <xliff:g id="NAME">%1$s</xliff:g> archiviata."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"download e ripristino"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"È necessario aggiornare l\'app"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"L\'app relativa a questa icona non è aggiornata. Puoi eseguire manualmente l\'aggiornamento per riattivare questa scorciatoia oppure rimuovere l\'icona."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Aggiorna"</string>
@@ -186,12 +198,14 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"OK"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Metti in pausa le app di lavoro"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Riattiva"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filtra"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Operazione non riuscita: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Spazio privato"</string>
     <string name="private_space_secondary_label" msgid="9203933341714508907">"Tocca per configurare o aprire"</string>
     <string name="ps_container_title" msgid="4391796149519594205">"Privato"</string>
-    <string name="ps_container_settings" msgid="6059734123353320479">"Impostazioni dello Spazio privato"</string>
+    <string name="ps_container_settings" msgid="6059734123353320479">"Impostazioni dello spazio privato"</string>
     <string name="ps_container_unlock_button_content_description" msgid="9181551784092204234">"Privato, sbloccato."</string>
     <string name="ps_container_lock_button_content_description" msgid="5961993384382649530">"Privato, bloccato."</string>
     <string name="ps_container_lock_title" msgid="2640257399982364682">"Blocca"</string>
diff --git a/res/values-iw/strings.xml b/res/values-iw/strings.xml
index f198166..724742a 100644
--- a/res/values-iw/strings.xml
+++ b/res/values-iw/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"חלון חדש"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"שמירת צמד אפליקציות"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"צמד האפליקציות הזה לא נתמך במכשיר הזה"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"כתיבת הערות"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"הוספה"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"הוספת הווידג\'ט <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"אפשר לשנות את הגדרות הווידג\'ט בהקשה"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"שינוי הגדרות הווידג\'ט"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"חיפוש אפליקציות"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"הושבת על ידי מנהל המערכת שלך"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"סיבוב מסך הבית"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"כאשר מסובבים את הטלפון"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"סימני ההתראות"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"מופעל"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"כבוי"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> בתהליך התקנה, <xliff:g id="PROGRESS">%2$s</xliff:g> הושלמו"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"הורדת <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>"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"אפליקציית <xliff:g id="NAME">%1$s</xliff:g> הועברה לארכיון. אפשר להקיש כדי להוריד ולשחזר אותה."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"אפליקציית <xliff:g id="NAME">%1$s</xliff:g> הועברה לארכיון."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"הורדה ושחזור"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"נדרש עדכון לאפליקציה"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"האפליקציה של הסמל הזה לא מעודכנת. אפשר לעדכן אותה ידנית כדי להפעיל מחדש את קיצור הדרך הזה, או להסיר את הסמל."</string>
     <string name="dialog_update" msgid="2178028071796141234">"עדכון"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"הבנתי"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"השהיית האפליקציות לעבודה"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"ביטול ההשהיה"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <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>
diff --git a/res/values-ja/strings.xml b/res/values-ja/strings.xml
index d2f9a97..29dbf26 100644
--- a/res/values-ja/strings.xml
+++ b/res/values-ja/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"新しいウィンドウ"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"アプリのペア設定を保存"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"このデバイスは、このアプリのペア設定に対応していません"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"メモ"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"追加"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g>ウィジェットを追加"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"タップしてウィジェットの設定を変更する"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"ウィジェットの設定を変更します"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"アプリを検索"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"管理者により無効にされています"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"ホーム画面の回転を許可"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"スマートフォンの向きに合わせます"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"通知ドット"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"ON"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"OFF"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> をインストールしています: <xliff:g id="PROGRESS">%2$s</xliff:g> 完了"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<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>のインストール待ち"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g>はアーカイブ済みです。ダウンロードして復元するには、タップしてください。"</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> はアーカイブ済みです。"</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"ダウンロードして復元"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"アプリの更新が必要"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"このアイコンのアプリは更新されていません。手動で更新して、このショートカットを再度有効にできます。また、アイコンを削除することもできます。"</string>
     <string name="dialog_update" msgid="2178028071796141234">"更新"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"OK"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"仕事用アプリを一時停止"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"停止解除"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <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>
diff --git a/res/values-ka/strings.xml b/res/values-ka/strings.xml
index e67cc41..876ab11 100644
--- a/res/values-ka/strings.xml
+++ b/res/values-ka/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"ახალი ფანჯარა"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"აპთა წყვილის შენახვა"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"ამ მოწყობილობაზე აღნიშნული აპთა წყვილი არ არის მხარდაჭერილი"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"ჩანიშვნა"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"დამატება"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> ვიჯეტის დამატება"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"შეეხეთ ვიჯეტის პარამეტრების შესაცვლელად"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"ვიჯეტის პარამეტრების შეცვლა"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"აპების ძიება"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"გათიშულია თქვენი ადმინისტრატორის მიერ"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"მთავარი ეკრანის შეტრიალების დაშვება"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"ტელეფონის შეტრიალებისას"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"შეტყობინების ნიშნულები"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"ჩართულია"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"გამორთულია"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"ინსტალირდება <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="PROGRESS">%2$s</xliff:g> დასრულებულია"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"მიმდინარეობს <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> ელოდება ინსტალაციას"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> დაარქივებულია. შეეხეთ გადმოსაწერად და აღსადგენად."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> დაარქივებულია."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"ჩამოტვირთვა და აღდგენა"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"საჭიროა აპის განახლება"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"ამ ხატულის აპი განახლებული არ არის. შეგიძლიათ, ხელით განაახლოთ ამ მალსახმობის ხელახლა გასააქტიურებლად, ან ამოშალოთ ხატულა."</string>
     <string name="dialog_update" msgid="2178028071796141234">"განახლება"</string>
@@ -186,11 +198,13 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"გასაგებია"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"სამსახურის აპების დაპაუზება"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"პაუზის გაუქმება"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <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_secondary_label" msgid="9203933341714508907">"დასაყენებლად ან გასახსნელად შეეხეთ"</string>
-    <string name="ps_container_title" msgid="4391796149519594205">"პირადი"</string>
+    <string name="ps_container_title" msgid="4391796149519594205">"კერძო"</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>
diff --git a/res/values-kk/strings.xml b/res/values-kk/strings.xml
index d5ccae5..bc1c380 100644
--- a/res/values-kk/strings.xml
+++ b/res/values-kk/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Жаңа терезе"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Қолданбаларды жұптау әрекетін сақтау"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Бұл құрылғы қолданбаларды жұптау функциясын қолдамайды."</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Ескертпе жазу"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Қосу"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Виджет (<xliff:g id="WIDGET_NAME">%1$s</xliff:g>) қосу"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Виджет параметрлерін өзгерту үшін түртіңіз."</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Виджет параметрлерін өзгерту"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Қолданбаларды іздеу"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Әкімші өшірді"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Негізгі экранды бұруға рұқсат ету"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Телефон бұрылғанда"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Хабарландыру белгілері"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Қосулы"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Өшірулі"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> орнатылуда, <xliff:g id="PROGRESS">%2$s</xliff:g> аяқталды"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<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> орнату күтілуде"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> мұрағатталды. Жүктеп алу және қалпына келтіру үшін түртіңіз."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> мұрағатталды."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"жүктеп алу және қалпына келтіру"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Қолданбаны жаңарту қажет"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Осы белгіше үшін қолданба жаңартылмаған. Оны қолмен жаңартып, осы таңбашаны қайта іске қоса аласыз немесе белгішені өшіріп тастаңыз."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Жаңарту"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Түсінікті"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Жұмыс қолданбаларын кідірту"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Қайта қосу"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <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>
diff --git a/res/values-km/strings.xml b/res/values-km/strings.xml
index ebd68f7..813b5a2 100644
--- a/res/values-km/strings.xml
+++ b/res/values-km/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"វិនដូ​ថ្មី"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"រក្សាទុកគូកម្មវិធី"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"មិនអាចប្រើគូកម្មវិធីនេះនៅលើឧបករណ៍នេះបានទេ"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"ការកត់ត្រា"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"បញ្ចូល"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"បញ្ចូលធាតុ​ក្រាហ្វិក <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"ចុចដើម្បីប្ដូរការកំណត់ធាតុ​ក្រាហ្វិក"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"ប្ដូរការកំណត់ធាតុ​ក្រាហ្វិក"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"ស្វែងរក​កម្មវិធី"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"បានបិទដំណើរការដោយអ្នកគ្រប់គ្រងរបស់អ្នក"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"អនុញ្ញាតការបងិ្វលអេក្រង់ដើម"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"នៅពេលដែលបង្វិលទូរសព្ទ"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"ស្លាកជូនដំណឹង"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"បើក"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"បិទ"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"កំពុង​ដំឡើង <xliff:g id="NAME">%1$s</xliff:g>, បាន​បញ្ចប់ <xliff:g id="PROGRESS">%2$s</xliff:g>"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"កំពុងដោនឡូត <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> កំពុងរង់ចាំការដំឡើង"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> ត្រូវបានទុក​ក្នុង​បណ្ណសារ។ សូមចុចដើម្បីទាញយក និងស្ដារ។"</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> ត្រូវបានទុក​ក្នុង​បណ្ណសារ។"</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"ទាញយក និងស្ដារ"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"តម្រូវឱ្យមាន​កំណែកម្មវិធីថ្មី"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"កម្មវិធីសម្រាប់​រូបតំណាងនេះ​មិនត្រូវបានដំឡើងកំណែ​ទេ។ អ្នកអាច​ដំឡើងកំណែ​ដោយផ្ទាល់ ដើម្បីបើក​ផ្លូវកាត់នេះឡើងវិញ ឬលុបរូបតំណាងនេះ។"</string>
     <string name="dialog_update" msgid="2178028071796141234">"ដំឡើងកំណែ"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"យល់ហើយ"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"ផ្អាក​កម្មវិធី​ការងារ"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"ឈប់ផ្អាក"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <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>
diff --git a/res/values-kn/strings.xml b/res/values-kn/strings.xml
index ab84833..b4bc3a1 100644
--- a/res/values-kn/strings.xml
+++ b/res/values-kn/strings.xml
@@ -21,7 +21,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name" msgid="649227358658669779">"Launcher3"</string>
     <string name="work_folder_name" msgid="3753320833950115786">"ಕೆಲಸ"</string>
-    <string name="activity_not_found" msgid="8071924732094499514">"ಅಪ್ಲಿಕೇಶನ್‌ ಅನ್ನು ಸ್ಥಾಪಿಸಲಾಗಿಲ್ಲ"</string>
+    <string name="activity_not_found" msgid="8071924732094499514">"ಆ್ಯಪ್‌ ಅನ್ನು ಸ್ಥಾಪಿಸಲಾಗಿಲ್ಲ"</string>
     <string name="activity_not_available" msgid="7456344436509528827">"ಅಪ್ಲಿಕೇಶನ್ ಲಭ್ಯವಿಲ್ಲ"</string>
     <string name="safemode_shortcut_error" msgid="9160126848219158407">"ಡೌನ್‌ಲೋಡ್ ಮಾಡಲಾದ ಅಪ್ಲಿಕೇಶನ್ ಅನ್ನು ಸುರಕ್ಷಿತ ಮೋಡ್‌ನಲ್ಲಿ ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಲಾಗಿದೆ"</string>
     <string name="safemode_widget_error" msgid="4863470563535682004">"ಸುರಕ್ಷಿತ ಮೋಡ್‌ನಲ್ಲಿ ವಿಜೆಟ್‌ಗಳನ್ನು ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಲಾಗಿದೆ"</string>
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"ಹೊಸ ವಿಂಡೋ"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"ಆ್ಯಪ್ ಪೇರ್ ಸೇವ್ ಮಾಡಿ"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"ಈ ಆ್ಯಪ್ ಜೋಡಿಯು ಈ ಸಾಧನದಲ್ಲಿ ಬೆಂಬಲಿತವಾಗಿಲ್ಲ"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"ಟಿಪ್ಪಣಿ ತೆಗೆದುಕೊಳ್ಳುವುದು"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"ಸೇರಿಸಿ"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> ವಿಜೆಟ್ ಸೇರಿಸಿ"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"ವಿಜೆಟ್ ಸೆಟ್ಟಿಂಗ್‌ಗಳನ್ನು ಬದಲಾಯಿಸಲು ಟ್ಯಾಪ್ ಮಾಡಿ"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"ವಿಜೆಟ್ ಸೆಟ್ಟಿಂಗ್‌ಗಳನ್ನು ಬದಲಾಯಿಸಿ"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"ಆ್ಯಪ್‍ಗಳನ್ನು ಹುಡುಕಿ"</string>
@@ -102,7 +109,7 @@
     <string name="gadget_error_text" msgid="740356548025791839">"ವಿಜೆಟ್ ಅನ್ನು ಲೋಡ್ ಮಾಡಲು ಸಾಧ್ಯವಿಲ್ಲ"</string>
     <string name="gadget_setup_text" msgid="8348374825537681407">"ವಿಜೆಟ್ ಸೆಟ್ಟಿಂಗ್‌ಗಳು"</string>
     <string name="gadget_complete_setup_text" msgid="309040266978007925">"ಸೆಟಪ್ ಪೂರ್ಣಗೊಳಿಸಲು ಟ್ಯಾಪ್ ಮಾಡಿ"</string>
-    <string name="uninstall_system_app_text" msgid="4172046090762920660">"ಇದೊಂದು ಅಪ್ಲಿಕೇಶನ್ ಆಗಿದೆ ಮತ್ತು ಅಸ್ಥಾಪಿಸಲು ಸಾಧ್ಯವಿಲ್ಲ."</string>
+    <string name="uninstall_system_app_text" msgid="4172046090762920660">"ಇದೊಂದು ಆ್ಯಪ್‌ ಆಗಿದೆ ಮತ್ತು ಅಸ್ಥಾಪಿಸಲು ಸಾಧ್ಯವಿಲ್ಲ."</string>
     <string name="folder_hint_text" msgid="5174843001373488816">"ಹೆಸರನ್ನು ಎಡಿಟ್ ಮಾಡಿ"</string>
     <string name="disabled_app_label" msgid="6673129024321402780">"<xliff:g id="APP_NAME">%1$s</xliff:g> ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಲಾಗಿದೆ"</string>
     <string name="dotted_app_label" msgid="1865617679843363410">"{count,plural, =1{{app_name} ಆ್ಯಪ್ # ಅಧಿಸೂಚನೆಯನ್ನು ಹೊಂದಿದೆ}one{{app_name} ಆ್ಯಪ್ # ಅಧಿಸೂಚನೆಗಳನ್ನು ಹೊಂದಿದೆ}other{{app_name} ಆ್ಯಪ್ # ಅಧಿಸೂಚನೆಗಳನ್ನು ಹೊಂದಿದೆ}}"</string>
@@ -119,10 +126,14 @@
     <string name="app_pair_name_format" msgid="8134106404716224054">"ಆ್ಯಪ್ ಜೋಡಿ: <xliff:g id="APP1">%1$s</xliff:g> ಮತ್ತು <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="styles_wallpaper_button_text" msgid="8216961355289236794">"ವಾಲ್‌ಪೇಪರ್ ಮತ್ತು ಶೈಲಿ"</string>
     <string name="edit_home_screen" msgid="8947858375782098427">"ಹೋಮ್ ಸ್ಕ್ರೀನ್ ಅನ್ನು ಎಡಿಟ್ ಮಾಡಿ"</string>
-    <string name="settings_button_text" msgid="8873672322605444408">"ಮುಖಪುಟ ಸೆಟ್ಟಿಂಗ್‌ಗಳು"</string>
+    <string name="settings_button_text" msgid="8873672322605444408">"ಹೋಮ್ ಸೆಟ್ಟಿಂಗ್‌ಗಳು"</string>
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"ನಿಮ್ಮ ನಿರ್ವಾಹಕರು ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಿದ್ದಾರೆ"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"ಹೋಮ್ ಸ್ಕ್ರೀನ್ ತಿರುಗುವಿಕೆಯನ್ನು ಅನುಮತಿಸಿ"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"ಫೋನ್‌ ತಿರುಗಿಸಿದಾಗ"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"ನೋಟಿಫಿಕೇಶನ್ ಡಾಟ್‌ಗಳು"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"ಆನ್ ಆಗಿದೆ"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"ಆಫ್ ಆಗಿದೆ"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> ಇನ್‌ಸ್ಟಾಲ್ ಮಾಡಲಾಗುತ್ತಿದೆ, <xliff:g id="PROGRESS">%2$s</xliff:g> ಪೂರ್ಣಗೊಂಡಿದೆ"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<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> ಸ್ಥಾಪಿಸಲು ಕಾಯಲಾಗುತ್ತಿದೆ"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> ಅನ್ನು ಆರ್ಕೈವ್ ಮಾಡಲಾಗಿದೆ. ಡೌನ್‌ಲೋಡ್ ಮಾಡಲು ಮತ್ತು ಮರುಸ್ಥಾಪಿಸಲು ಟ್ಯಾಪ್ ಮಾಡಿ."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> ಅನ್ನು ಆರ್ಕೈವ್ ಮಾಡಲಾಗಿದೆ."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"ಡೌನ್‌ಲೋಡ್ ಮಾಡಿ ಮತ್ತು ಮರುಸ್ಥಾಪಿಸಿ"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"ಆ್ಯಪ್ ಅಪ್‌ಡೇಟ್ ಅಗತ್ಯವಿದೆ"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"ಈ ಐಕಾನ್‌ಗಾಗಿ ಆ್ಯಪ್ ಅನ್ನು ಅಪ್‌ಡೇಟ್ ಮಾಡಲಾಗಿಲ್ಲ. ಈ ಶಾರ್ಟ್‌ಕಟ್ ಅನ್ನು ಮರು-ಸಕ್ರಿಯಗೊಳಿಸಲು ನೀವು ಹಸ್ತಚಾಲಿತವಾಗಿ ಅಪ್‌ಡೇಟ್ ಮಾಡಬಹುದು ಅಥವಾ ಐಕಾನ್ ಅನ್ನು ತೆಗೆದುಹಾಕಬಹುದು."</string>
     <string name="dialog_update" msgid="2178028071796141234">"ಅಪ್‌ಡೇಟ್ ಮಾಡಿ"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"ಅರ್ಥವಾಯಿತು"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"ಕೆಲಸಕ್ಕೆ ಸಂಬಂಧಿಸಿದ ಆ್ಯಪ್‌ಗಳನ್ನು ವಿರಾಮಗೊಳಿಸಿ"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"ವಿರಾಮವನ್ನು ರದ್ದುಗೊಳಿಸಿ"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <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>
diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml
index 318cd00..4baba23 100644
--- a/res/values-ko/strings.xml
+++ b/res/values-ko/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"새 창"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"앱 페어링 저장"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"이 앱 페어링은 이 기기에서 지원되지 않습니다"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"메모"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"추가"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> 위젯 추가"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"탭하여 위젯 설정 변경"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"위젯 설정 변경"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"앱 검색"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"관리자가 사용 중지함"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"홈 화면 회전 허용"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"휴대전화 회전 시"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"알림 표시 점"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"사용"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"사용 안함"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> 설치 중, <xliff:g id="PROGRESS">%2$s</xliff:g> 완료"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<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> 설치 대기 중"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> 앱이 보관처리되었습니다. 탭하여 다운로드하고 복원하세요"</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> 앱이 보관처리되었습니다"</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"다운로드 및 복원"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"앱 업데이트 필요"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"바로가기 아이콘의 앱이 업데이트되지 않았습니다. 직접 업데이트하여 앱 바로가기를 다시 사용할 수 있도록 하거나 아이콘을 삭제하세요."</string>
     <string name="dialog_update" msgid="2178028071796141234">"업데이트"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"확인"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"직장 앱 일시중지"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"일시중지 해제"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <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>
diff --git a/res/values-ky/strings.xml b/res/values-ky/strings.xml
index 856e2b2..83b0fa8 100644
--- a/res/values-ky/strings.xml
+++ b/res/values-ky/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Жаңы терезе"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Колдонмолорду сактап коюу"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Бул эки колдонмону бул түзмөктө бир маалда пайдаланууга болбойт"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Эскертме жазуу"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Кошуу"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> виджетин кошуу"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Виджеттин параметрлерин өзгөртүү үчүн таптап коюңуз"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Виджеттин параметрлерин өзгөртүү"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Колдонмолорду издөө"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Администраторуңуз өчүрүп койгон"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Башкы экранды бурууга уруксат берүү"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Телефон бурулганда"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Билдирмелер белгилери"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Күйүк"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Өчүк"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> орнотулууда, <xliff:g id="PROGRESS">%2$s</xliff:g> аткарылды"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<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> орнотулушу күтүлүүдө"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> архивделди. Жүктөп алуу жана калыбына келтирүү үчүн таптаңыз."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> архивделди."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"жүктөп алуу жана калыбына келтирүү"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Колдонмону жаңыртыңыз"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Бул сүрөтчөнүн колдонмосу жаңыртылган эмес. Ыкчам баскычты кайра иштетүү үчүн аны кол менен жаңыртып же сүрөтчөнү өчүрүп койсоңуз болот."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Жаңыртуу"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Түшүндүм"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Жумуш колдонмолорун тындыруу"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Улантуу"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <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>
diff --git a/res/values-lo/strings.xml b/res/values-lo/strings.xml
index 3d1a6c9..767c574 100644
--- a/res/values-lo/strings.xml
+++ b/res/values-lo/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"ໜ້າຈໍໃໝ່"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"ບັນທຶກຈັບຄູ່ແອັບ"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"ການຈັບຄູ່ແອັບນີ້ບໍ່ຮອງຮັບຢູ່ອຸປະກອນນີ້"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"ການຈົດບັນທຶກ"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"ເພີ່ມ"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"ເພີ່ມວິດເຈັດ <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"ແຕະເພື່ອປ່ຽນການຕັ້ງຄ່າວິດເຈັດ"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"ປ່ຽນການຕັ້ງຄ່າວິດເຈັດ"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"ຊອກຫາແອັບ"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"ຖືກປິດການນຳໃຊ້ໂດຍຜູ້ເບິ່ງແຍງລະບົບຂອງທ່ານ"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"ອະນຸຍາດໃຫ້ໝຸນໜ້າຈໍຢູ່ໂຮມສະກຣີນໄດ້"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"ເມື່ອໝຸນໂທລະສັບ"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"ຈຸດການແຈ້ງເຕືອນ"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"ເປີດ"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"ປິດ"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"ກຳລັງຕິດຕັ້ງ <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="PROGRESS">%2$s</xliff:g> ສຳເລັດແລ້ວ"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<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> ກຳ​ລັງ​ລໍ​ຖ້າ​ຕິດ​ຕັ້ງ"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> ຖືກເກັບໄວ້ໃນແຟ້ມ. ແຕະເພື່ອດາວໂຫຼດ ແລະ ກູ້ຄືນ."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> ຖືກເກັບໄວ້ໃນແຟ້ມ."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"ດາວໂຫຼດ ແລະ ກູ້ຄືນ"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"ຈຳເປັນຕ້ອງອັບເດດແອັບ"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"ບໍ່ໄດ້ອັບເດດແອັບສຳລັບໄອຄອນນີ້. ທ່ານສາມາດອັບເດດເອງໄດ້ເພື່ອເປີດການນຳໃຊ້ທາງລັດນີ້ຄືນໃໝ່ ຫຼື ລຶບໄອຄອນດັ່ງກ່າວອອກ."</string>
     <string name="dialog_update" msgid="2178028071796141234">"ອັບເດດ"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"ເຂົ້າໃຈແລ້ວ"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"ຢຸດແອັບບ່ອນເຮັດວຽກຊົ່ວຄາວ"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"ຍົກເລີກການຢຸດຊົ່ວຄາວ"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <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>
diff --git a/res/values-lt/strings.xml b/res/values-lt/strings.xml
index 4c9bd9b..08b603d 100644
--- a/res/values-lt/strings.xml
+++ b/res/values-lt/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Naujas langas"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Išsaugoti programų porą"</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">"Ši programų pora šiame įrenginyje nepalaikoma"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Užrašų kūrimas"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Pridėti"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Pridėti valdiklį: <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Palieskite, kad pakeistumėte valdiklio nustatymus"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Pakeisti valdiklio nustatymus"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Paieškos programos"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Išjungė administratorius"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Leisti pasukti pagrindinį ekraną"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Kai telefonas pasukamas"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Pranešimų taškai"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Įjungta"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Išjungta"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"Įdiegiama: „<xliff:g id="NAME">%1$s</xliff:g>“; baigta: <xliff:g id="PROGRESS">%2$s</xliff:g>"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"Atsisiunčiama programa „<xliff:g id="NAME">%1$s</xliff:g>“, <xliff:g id="PROGRESS">%2$s</xliff:g> baigta"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"Laukiama, kol bus įdiegta programa „<xliff:g id="NAME">%1$s</xliff:g>“"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"Programa „<xliff:g id="NAME">%1$s</xliff:g>“ suarchyvuota. Palieskite, jei norite atsisiųsti ir atkurti."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"Programa „<xliff:g id="NAME">%1$s</xliff:g>“ suarchyvuota."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"atsisiųsti ir atkurti"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Būtina atnaujinti programą"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Šios piktogramos programa neatnaujinta. Galite patys atnaujinti, kad iš naujo įgalintumėte šį spartųjį klavišą, arba pašalinkite piktogramą."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Atnaujinti"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Supratau"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Pristabdyti darbo programas"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Atšaukti pristabdymą"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filtruoti"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Nepavyko: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Privati erdvė"</string>
diff --git a/res/values-lv/strings.xml b/res/values-lv/strings.xml
index 0a82705..e02abe0 100644
--- a/res/values-lv/strings.xml
+++ b/res/values-lv/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Jauns logs"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Saglabāt lietotņu pāri"</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">"Šis lietotņu pāris netiek atbalstīts šajā ierīcē"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Piezīmju pierakstīšana"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Pievienot"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Pievienot logrīku <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Pieskarieties, lai mainītu logrīka iestatījumus."</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Mainīt logrīka iestatījumus"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Meklēt lietotnes"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Atspējojis administrators"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Atļaut sākuma ekrāna pagriešanu"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Pagriežot tālruni"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Paziņojumu punkti"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Ieslēgti"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Izslēgts"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"Notiek lietotnes “<xliff:g id="NAME">%1$s</xliff:g>” instalēšana. Norise: <xliff:g id="PROGRESS">%2$s</xliff:g>."</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"Lietotnes <xliff:g id="NAME">%1$s</xliff:g> lejupielāde (<xliff:g id="PROGRESS">%2$s</xliff:g> pabeigti)"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"Notiek <xliff:g id="NAME">%1$s</xliff:g> instalēšana"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"Lietotne <xliff:g id="NAME">%1$s</xliff:g> ir arhivēta; lai lejupielādētu un atjaunotu, pieskarieties"</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"Lietotne <xliff:g id="NAME">%1$s</xliff:g> ir arhivēta."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"lejupielādēt un atjaunot"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Lietotne ir jāatjaunina"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Šai ikonai paredzētā lietotne nav atjaunināta. Varat to atjaunināt manuāli, lai atkārtoti iespējotu šo saīsni, vai noņemt ikonu."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Atjaunināt"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Labi"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Pārtraukt darba lietotņu darbību"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Atsākt"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filtrs"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Neizdevās: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Privātā telpa"</string>
diff --git a/res/values-mk/strings.xml b/res/values-mk/strings.xml
index 887ca82..e524d82 100644
--- a/res/values-mk/strings.xml
+++ b/res/values-mk/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Нов прозорец"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Зачувај го парот апликации"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Паров апликации не е поддржан на уредов"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Фаќање белешки"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Додај"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Додај го виџетот <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Допрете за да ги промените поставките за виџетот"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Промени ги поставките за виџетот"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Пребарувајте апликации"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Оневозможено од администраторот"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Дозволи ротирање на почетниот екран"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Кога телефонот се ротира"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Точки за известување"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Вклучено"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Исклучено"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> се инсталира, <xliff:g id="PROGRESS">%2$s</xliff:g> завршено"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"Се презема <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> чека да се инсталира"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"Апликацијата <xliff:g id="NAME">%1$s</xliff:g> е архивирана. Допрете за да преземете и вратите."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"Апликацијата <xliff:g id="NAME">%1$s</xliff:g> е архивирана."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"преземете и вратете"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Потребно е ажурирање на апликацијата"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Апликацијата за оваа икона не е ажурирана. Може да ажурирате рачно за да повторно се овозможи кратенкава или отстранете ја иконата."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Ажурирај"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Сфатив"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Паузирај ги работните апликации"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Прекини ја паузата"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <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>
diff --git a/res/values-ml/strings.xml b/res/values-ml/strings.xml
index dda5679..e273e14 100644
--- a/res/values-ml/strings.xml
+++ b/res/values-ml/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"പുതിയ വിന്‍ഡോ"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"ആപ്പ് ജോടി സംരക്ഷിക്കുക"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"ഈ ഉപകരണത്തിൽ ഈ ആപ്പ് ജോടിക്ക് പിന്തുണയില്ല"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"കുറിപ്പ് രേഖപ്പെടുത്തൽ"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"ചേർക്കുക"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> വിജറ്റ് ചേർക്കുക"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"വിജറ്റ് ക്രമീകരണം മാറ്റാൻ ടാപ്പ് ചെയ്യുക"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"വിജറ്റ് ക്രമീകരണം മാറ്റുക"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"ആപ്പുകൾ തിരയുക"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"അഡ്മിൻ പ്രവർത്തനരഹിതമാക്കിയിരിക്കുന്നു"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"ഹോം സ്ക്രീൻ റൊട്ടേഷൻ അനുവദിക്കുക"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"ഫോൺ തിരിച്ച നിലയിലായിരിക്കുമ്പോൾ"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"അറിയിപ്പ് ഡോട്ടുകൾ"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"ഓണാണ്"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"ഓഫാണ്"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> ഇൻസ്‌റ്റാൾ ചെയ്യുന്നു, <xliff:g id="PROGRESS">%2$s</xliff:g> പൂർത്തിയായി"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<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> കാക്കുന്നു"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> ആർക്കൈവ് ചെയ്തു. ഡൗൺലോഡ് ചെയ്യാനും പുനഃസ്ഥാപിക്കാനും ടാപ്പ് ചെയ്യുക."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> ആർക്കൈവ് ചെയ്തു."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"ഡൗൺലോഡ് ചെയ്ത് പുനഃസ്ഥാപിക്കുക"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"ആപ്പ് അപ്‌ഡേറ്റ് ചെയ്യേണ്ടതുണ്ട്"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"ഈ ഐക്കണിനുള്ള ആപ്പ് അപ്‌ഡേറ്റ് ചെയ്തിട്ടില്ല. ഈ കുറുക്കുവഴി വീണ്ടും പ്രവർത്തനക്ഷമമാക്കാൻ നിങ്ങൾക്ക് നേരിട്ട് അപ്‌ഡേറ്റ് ചെയ്യാം അല്ലെങ്കിൽ ഐക്കൺ നീക്കം ചെയ്യാം."</string>
     <string name="dialog_update" msgid="2178028071796141234">"അപ്ഡേറ്റ് ചെയ്യുക"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"മനസ്സിലായി"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"ഔദ്യോഗിക ആപ്പുകൾ താൽക്കാലികമായി നിർത്തുക"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"താൽക്കാലികമായി നിർത്തിയത് മാറ്റുക"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <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>
diff --git a/res/values-mn/strings.xml b/res/values-mn/strings.xml
index 49d71c2..7e5ed73 100644
--- a/res/values-mn/strings.xml
+++ b/res/values-mn/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Шинэ цонх"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Апп хослуулалтыг хадгалах"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Энэ апп хослуулалтыг уг төхөөрөмж дээр дэмждэггүй"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Тэмдэглэл хөтлөх"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Нэмэх"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> виджетийг нэмэх"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Жижиг хэрэгслийн тохиргоог өөрчлөхийн тулд товшино уу"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Жижиг хэрэгслийн тохиргоог өөрчлөх"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Апп хайх"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Таны админ идэвхгүй болгосон"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Үндсэн нүүрийг эргүүлэхийг зөвшөөрөх"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Утсыг эргүүлсэн үед"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Мэдэгдлийн цэг"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Асаалттай"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Унтраалттай"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g>-г суулгаж байна. <xliff:g id="PROGRESS">%2$s</xliff:g> дууссан"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<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> нь суулгахыг хүлээж байна"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g>-г архивласан. Татаж, сэргээхийн тулд товшино уу."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g>-г архивласан."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"татах, сэргээх"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Аппын шинэчлэлт шаардлагатай"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Энэ дүрс тэмдгийн аппыг шинэчлээгүй. Та энэ товчлолыг дахин идэвхжүүлэх эсвэл дүрсийг хасахын тулд гараар шинэчлэх боломжтой."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Шинэчлэх"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Ойлголоо"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Ажлын аппуудыг түр зогсоох"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Түр зогсоохоо болих"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <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>
diff --git a/res/values-mr/strings.xml b/res/values-mr/strings.xml
index fdf864a..b5d4f71 100644
--- a/res/values-mr/strings.xml
+++ b/res/values-mr/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"नवीन विंडो"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"ॲपची जोडी सेव्ह करा"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"या ॲपची जोडीला या डिव्हाइसवर सपोर्ट नाही"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"टिपा घेणे"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"जोडा"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> विजेट जोडा"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"विजेट सेटिंग्ज बदलण्यासाठी टॅप करा"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"विजेट सेटिंग्ज बदला"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"अ‍ॅप्स शोधा"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"आपल्या प्रशासकाने अक्षम केले"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"होम स्क्रीन फिरवण्‍याची अनुमती द्या"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"फोन फिरवला जातो तेव्हा"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"नोटिफिकेशन डॉट"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"सुरू"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"बंद"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> इंस्टॉल करत आहे, <xliff:g id="PROGRESS">%2$s</xliff:g> पूर्ण झाले"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<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> इंस्टॉल करण्याची प्रतिक्षा करत आहे"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> संग्रहित केले आहे. डाउनलोड करून रिस्टोअर करण्यासाठी टॅप करा."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> संग्रहित केले आहे."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"डाउनलोड करून रिस्टोअर करा"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"अ‍ॅप अपडेट करणे आवश्‍यक आहे"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"या आयकनसाठी अ‍ॅप अपडेट केलेले नाही. हा शॉटकर्ट पुन्हा सुरू करण्यासाठी तुम्ही मॅन्युअली अपडेट करू शकता किंवा आयकन काढून टाका."</string>
     <string name="dialog_update" msgid="2178028071796141234">"अपडेट करा"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"समजले"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"कार्य ॲप्स थांबवा"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"अनपॉझ करा"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <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>
diff --git a/res/values-ms/strings.xml b/res/values-ms/strings.xml
index b86f657..a48d518 100644
--- a/res/values-ms/strings.xml
+++ b/res/values-ms/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Tetingkap Baharu"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Simpan gandingan apl"</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">"Gandingan apl ini tidak disokong pada peranti ini"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Pengambilan nota"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Tambah"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Tambahkan widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Ketik untuk menukar tetapan widget"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Tukar tetapan widget"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Cari apl"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Dilumpuhkan oleh pentadbir anda"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Benarkan putaran skrin utama"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Apabila telefon diputar"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Titik pemberitahuan"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Hidup"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Mati"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> dipasang, <xliff:g id="PROGRESS">%2$s</xliff:g> selesai"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> memuat turun, <xliff:g id="PROGRESS">%2$s</xliff:g> selesai"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> menunggu untuk dipasang"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> diarkibkan. Ketik untuk memuat turun dan memulihkan apl tersebut."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> diarkibkan."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"muat turun dan pulihkan"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Kemas kini apl diperlukan"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Apl untuk ikon ini tidak dikemas kini. Anda boleh mengemas kini secara manual untuk mendayakan semula pintasan atau mengalih keluar ikon."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Kemas kini"</string>
@@ -186,9 +198,11 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"OK"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Jeda apl kerja"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Nyahjeda"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Tapis"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Gagal: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
-    <string name="private_space_label" msgid="2359721649407947001">"Ruang privasi"</string>
+    <string name="private_space_label" msgid="2359721649407947001">"Ruang persendirian"</string>
     <string name="private_space_secondary_label" msgid="9203933341714508907">"Ketik untuk menyediakan atau membuka"</string>
     <string name="ps_container_title" msgid="4391796149519594205">"Persendirian"</string>
     <string name="ps_container_settings" msgid="6059734123353320479">"Tetapan Ruang Peribadi"</string>
diff --git a/res/values-my/strings.xml b/res/values-my/strings.xml
index 7e8fd14..f892a1b 100644
--- a/res/values-my/strings.xml
+++ b/res/values-my/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"ဝင်းဒိုးအသစ်"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"အက်ပ်တွဲချိတ်ခြင်း သိမ်းရန်"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"ဤအက်ပ်တွဲချိတ်ခြင်းကို ဤစက်တွင် ပံ့ပိုးမထားပါ"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"မှတ်စုလိုက်ခြင်း"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"ထည့်ရန်"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> ဝိဂျက်ထည့်ရန်"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"ဝိဂျက် ဆက်တင်များကို ပြောင်းရန် တို့ပါ"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"ဝိဂျက် ဆက်တင်များကို ပြောင်းပါ"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"ရှာဖွေမှု အက်ပ်များ"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"သင့်စီမံခန့်ခွဲသူက ပိတ်လိုက်ပါသည်"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"ပင်မစာမျက်နှာလှည့်ခြင်းကို ခွင့်ပြုခြင်း"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"ဖုန်းကိုလှည့်ထားစဉ်"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"သတိပေးချက် အစက်များ"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"ဖွင့်"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"ပိတ်"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> ကို ထည့်သွင်းနေသည်၊ <xliff:g id="PROGRESS">%2$s</xliff:g> ပြီးပါပြီ"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<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> ကိုထည့်သွင်းရန်စောင့်နေသည်"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> ကို သိမ်းထားသည်။ ဒေါင်းလုဒ်လုပ်ပြီး ပြန်ယူရန် တို့ပါ။"</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> ကို သိမ်းထားသည်။"</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"ဒေါင်းလုဒ်လုပ်ပြီး ပြန်ယူရန်"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"အက်ပ်ကို အပ်ဒိတ်လုပ်ရန် လိုအပ်သည်"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"ဤသင်္ကေတအတွက် အက်ပ်ကို အပ်ဒိတ်လုပ်မထားပါ။ ဤဖြတ်လမ်းလင့်ခ်ကို ပြန်ဖွင့်ရန် ကိုယ်တိုင်အပ်ဒိတ်လုပ်နိုင်သည် (သို့) သင်္ကေတကို ဖယ်ရှားနိုင်သည်။"</string>
     <string name="dialog_update" msgid="2178028071796141234">"အပ်ဒိတ်လုပ်ရန်"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"နားလည်ပြီ"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"အလုပ်သုံးအက်ပ်များကို ခဏရပ်ရန်"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"ပြန်ဖွင့်ရန်"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <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>
diff --git a/res/values-nb/strings.xml b/res/values-nb/strings.xml
index e2be4ff..b9b69a0 100644
--- a/res/values-nb/strings.xml
+++ b/res/values-nb/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Nytt vindu"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Lagre app-paret"</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">"Denne apptilkoblingen støttes ikke på denne enheten"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Notatskriving"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Legg til"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Legg til <xliff:g id="WIDGET_NAME">%1$s</xliff:g>-modulen"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Trykk for å endre modulinnstillinger"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Endre modulinnstillinger"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Søk etter apper"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Administratoren har slått av funksjonen"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Tillat at startskjermen roterer"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Når telefonen roteres"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Varselsprikker"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"På"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Av"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> installerer, <xliff:g id="PROGRESS">%2$s</xliff:g> er fullført"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"Laster ned <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="PROGRESS">%2$s</xliff:g> er fullført"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"Venter på å installere <xliff:g id="NAME">%1$s</xliff:g>"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> er arkivert. Trykk for å laste ned og gjenopprette."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> er arkivert."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"last ned og gjenopprett"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Appen må oppdateres"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Appen for dette ikonet er ikke oppdatert. Du kan oppdatere manuelt for å aktivere denne snarveien igjen, eller du kan fjerne ikonet."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Oppdater"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Greit"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Sett jobbapper på pause"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Gjenoppta"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filter"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Mislyktes: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Privat område"</string>
diff --git a/res/values-ne/strings.xml b/res/values-ne/strings.xml
index fa2e59b..6f93761 100644
--- a/res/values-ne/strings.xml
+++ b/res/values-ne/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"नयाँ विन्डो"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"एपको पेयर सेभ गर्नुहोस्"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"यस डिभाइसमा यो एप पेयर प्रयोग गर्न मिल्दैन"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"नोट लेख्ने कार्य"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"हाल्नुहोस्"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> विजेट हाल्नुहोस्"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"विजेटका सेटिङ बदल्न ट्याप गर्नुहोस्"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"विजेटका सेटिङ बदल्नुहोस्"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"एपहरू खोज्नुहोस्"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"तपाईँको प्रशासकद्वारा असक्षम गरिएको"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"होम स्क्रिन रोटेट हुन दिइयोस्"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"फोन घुमाउँदा"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"नोटिफिकेसन डट"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"सक्रिय"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"निष्क्रिय"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> इन्स्टल गरिँदै छ, <xliff:g id="PROGRESS">%2$s</xliff:g> पूरा भयो"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<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> स्थापना गर्न प्रतीक्षा गर्दै"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> अभिलेखमा राखिएको छ। डाउनलोड गरी रिस्टोर गर्न ट्याप गर्नुहोस्।"</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> अभिलेखमा राखिएको छ।"</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"डाउनलोड र रिस्टोर गर्नुहोस्"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"एप अपडेट गरिनु पर्छ"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"यो आइकनले जनाउने एप अपडेट गरिएको छैन। तपाईं यो सर्टकट फेरि अन गर्न म्यानुअल रूपमा अपडेट गर्न सक्नुहुन्छ वा आइकन नै हटाउनुहोस्।"</string>
     <string name="dialog_update" msgid="2178028071796141234">"अपडेट गर्नुहोस्"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"बुझेँ"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"कामसम्बन्धी एपहरू पज गर्नुहोस्"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"सुचारु गर्नुहोस्"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <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>
diff --git a/res/values-night-v31/colors.xml b/res/values-night-v31/colors.xml
index 0f630e5..d9f9769 100644
--- a/res/values-night-v31/colors.xml
+++ b/res/values-night-v31/colors.xml
@@ -53,10 +53,5 @@
     <color name="widget_picker_add_button_text_color_dark">
         @android:color/system_accent1_800</color>
 
-    <color name="work_fab_bg_color">
-        @android:color/system_accent1_200</color>
-    <color name="work_fab_icon_color">
-        @android:color/system_accent1_900</color>
-
     <color name="material_color_on_surface">@android:color/system_neutral1_100</color>
 </resources>
\ No newline at end of file
diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml
index 9271b96..a38971f 100644
--- a/res/values-nl/strings.xml
+++ b/res/values-nl/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Nieuw venster"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"App-paar opslaan"</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">"Dit app-paar wordt niet ondersteund op dit apparaat"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Aantekeningen maken"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Toevoegen"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g> toevoegen"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Tik om de widgetinstellingen te wijzigen"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Widgetinstellingen wijzigen"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Apps zoeken"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Uitgezet door je beheerder"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Draaien van startscherm toestaan"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Als de telefoon gedraaid is"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Meldingsstipjes"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Aan"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Uit"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> installeren, <xliff:g id="PROGRESS">%2$s</xliff:g> voltooid"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> wordt gedownload, <xliff:g id="PROGRESS">%2$s</xliff:g> voltooid"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> wacht op installatie"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> is gearchiveerd. Tik om te downloaden en te herstellen."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> is gearchiveerd."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"downloaden en herstellen"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"App-update vereist"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"De app voor dit icoon is niet geüpdatet. Je kunt handmatig updaten om deze snelkoppeling weer aan te zetten of het icoon verwijderen."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Updaten"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"OK"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Werk-apps pauzeren"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Hervatten"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filteren"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Mislukt: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Privéruimte"</string>
diff --git a/res/values-or/strings.xml b/res/values-or/strings.xml
index 98d52de..a20e0fc 100644
--- a/res/values-or/strings.xml
+++ b/res/values-or/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"ନୂଆ ୱିଣ୍ଡୋ"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"ଆପ ପେୟାର ସେଭ କରନ୍ତୁ"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"ଏହି ଆପ ପେୟାର ଏ ଡିଭାଇସରେ ସମର୍ଥିତ ନୁହେଁ"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"ନୋଟ-ଟେକିଂ"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"ଯୋଗ କରନ୍ତୁ"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> ୱିଜେଟ ଯୋଗ କରନ୍ତୁ"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"ୱିଜେଟ ସେଟିଂସ ପରିବର୍ତ୍ତନ କରିବାକୁ ଟାପ କରନ୍ତୁ"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"ୱିଜେଟ ସେଟିଂସ ପରିବର୍ତ୍ତନ କରନ୍ତୁ"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"ଆପ ସର୍ଚ୍ଚ କରନ୍ତୁ"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"ଆପଣଙ୍କ ଆଡମିନଙ୍କ ଦ୍ୱାରା ଅକ୍ଷମ କରାଯାଇଛି"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"ହୋମ ସ୍କ୍ରିନ ରୋଟେସନକୁ ଅନୁମତି ଦିଅନ୍ତୁ"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"ଯେତେବେଳେ ଫୋନକୁ ରୋଟେଟ କରାଯାଇଥାଏ"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"ବିଜ୍ଞପ୍ତି ଡଟ୍ସ"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"ଚାଲୁ"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"ବନ୍ଦ କରନ୍ତୁ"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> ଇନଷ୍ଟଲ୍ କରାଯାଉଛି, <xliff:g id="PROGRESS">%2$s</xliff:g> ସମ୍ପୂର୍ଣ୍ଣ ହୋଇଛି"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<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> ଇନଷ୍ଟଲ୍‌ ହେବାକୁ ଅପେକ୍ଷା କରିଛି"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g>କୁ ଆର୍କାଇଭ କରାଯାଇଛି। ଡାଉନଲୋଡ ଏବଂ ରିଷ୍ଟୋର କରିବା ପାଇଁ ଟାପ କରନ୍ତୁ।"</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g>କୁ ଆର୍କାଇଭ କରାଯାଇଛି।"</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"ଡାଉନଲୋଡ ଏବଂ ରିଷ୍ଟୋର କରନ୍ତୁ"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"ଆପକୁ ଅପଡେଟ କରିବା ଆବଶ୍ୟକ"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"ଏହି ଆଇକନ ପାଇଁ ଆପକୁ ଅପଡେଟ କରାଯାଇନାହିଁ। ଏହି ସର୍ଟକଟକୁ ପୁଣି-ସକ୍ଷମ କରିବା ପାଇଁ ଆପଣ ମାନୁଆଲୀ ଅପଡେଟ କରିପାରିବେ କିମ୍ବା ଆଇକନଟିକୁ କାଢ଼ି ଦେଇପାରିବେ।"</string>
     <string name="dialog_update" msgid="2178028071796141234">"ଅପଡେଟ କରନ୍ତୁ"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"ବୁଝିଗଲି"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"ୱାର୍କ ଆପ୍ସ ବିରତ କରନ୍ତୁ"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"ପୁଣି ଚାଲୁ କରନ୍ତୁ"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <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>
diff --git a/res/values-pa/strings.xml b/res/values-pa/strings.xml
index 782979e..65187bf 100644
--- a/res/values-pa/strings.xml
+++ b/res/values-pa/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"ਨਵੀਂ ਵਿੰਡੋ"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"ਐਪ ਜੋੜਾਬੱਧ ਰੱਖਿਅਤ ਕਰੋ"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"ਇਸ ਐਪ ਜੋੜਾਬੱਧ ਦਾ ਇਸ ਡੀਵਾਈਸ \'ਤੇ ਸਮਰਥਨ ਨਹੀਂ ਕੀਤਾ ਜਾਂਦਾ"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"ਨੋਟ ਬਣਾਉਣਾ"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"ਸ਼ਾਮਲ ਕਰੋ"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> ਵਿਜੇਟ ਸ਼ਾਮਲ ਕਰੋ"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"ਵਿਜੇਟ ਸੈਟਿੰਗਾਂ ਨੂੰ ਬਦਲਣ ਲਈ ਟੈਪ ਕਰੋ"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"ਵਿਜੇਟ ਸੈਟਿੰਗਾਂ ਬਦਲੋ"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"ਐਪਾਂ ਖੋਜੋ"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"ਤੁਹਾਡੇ ਪ੍ਰਸ਼ਾਸਕ ਦੁਆਰਾ ਅਯੋਗ ਬਣਾਈ ਗਈ"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"ਹੋਮ ਸਕ੍ਰੀਨ ਨੂੰ ਘੁਮਾਉਣ ਦੀ ਆਗਿਆ ਦਿਓ"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"ਜਦੋਂ ਫ਼ੋਨ ਘੁਮਾਇਆ ਜਾਂਦਾ ਹੈ"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"ਸੂਚਨਾ ਬਿੰਦੂ"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"ਚਾਲੂ"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"ਬੰਦ"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> ਨੂੰ ਸਥਾਪਤ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ, <xliff:g id="PROGRESS">%2$s</xliff:g> ਪੂਰਾ ਹੋਇਆ"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<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> ਸਥਾਪਤ ਕਰਨ ਦੀ ਉਡੀਕ ਕਰ ਰਿਹਾ ਹੈ"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> ਪੁਰਾਲੇਖਬੱਧ ਹੈ। ਡਾਊਨਲੋਡ ਅਤੇ ਮੁੜ-ਬਹਾਲ ਕਰਨ ਲਈ ਟੈਪ ਕਰੋ।"</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> ਪੁਰਾਲੇਖਬੱਧ ਹੈ।"</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"ਡਾਊਨਲੋਡ ਕਰ ਕੇ ਮੁੜ-ਬਹਾਲ ਕਰੋ"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"ਐਪ ਨੂੰ ਅੱਪਡੇਟ ਕਰਨ ਦੀ ਲੋੜ ਹੈ"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"ਇਸ ਪ੍ਰਤੀਕ ਲਈ ਐਪ ਨੂੰ ਅੱਪਡੇਟ ਨਹੀਂ ਕੀਤਾ ਗਿਆ ਹੈ। ਇਸ ਸ਼ਾਰਟਕੱਟ ਨੂੰ ਮੁੜ-ਚਾਲੂ ਕਰਨ ਜਾਂ ਪ੍ਰਤੀਕ ਨੂੰ ਹਟਾਉਣ ਲਈ ਤੁਸੀਂ ਹੱਥੀਂ ਅੱਪਡੇਟ ਕਰ ਸਕਦੇ ਹੋ।"</string>
     <string name="dialog_update" msgid="2178028071796141234">"ਅੱਪਡੇਟ ਕਰੋ"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"ਸਮਝ ਲਿਆ"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"ਕੰਮ ਸੰਬੰਧੀ ਐਪਾਂ ਰੋਕੋ"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"ਰੋਕ ਹਟਾਓ"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <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>
diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml
index 71e569c..861be1e 100644
--- a/res/values-pl/strings.xml
+++ b/res/values-pl/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Nowe okno"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Zapisz parę aplikacji"</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">"Ta para aplikacji nie jest obsługiwana na tym urządzeniu"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Notatki"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Dodaj"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Dodaj widżet <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Kliknij, aby zmienić ustawienia widżetu"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Zmień ustawienia widżetu"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Wyszukaj aplikacje"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Funkcja wyłączona przez administratora"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Zezwalaj na obrót ekranu głównego"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Po obróceniu telefonu"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Kropki powiadomień"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Włączone"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Wyłączone"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"Instaluję aplikację <xliff:g id="NAME">%1$s</xliff:g>, postęp: <xliff:g id="PROGRESS">%2$s</xliff:g>"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"Pobieranie elementu <xliff:g id="NAME">%1$s</xliff:g>, ukończono: <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> oczekuje na instalację"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"Aplikacja <xliff:g id="NAME">%1$s</xliff:g> jest zarchiwizowana. Kliknij, aby ją pobrać i przywrócić."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"Aplikacja <xliff:g id="NAME">%1$s</xliff:g> jest zarchiwizowana."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"pobierz i przywróć"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Wymagana aktualizacja aplikacji"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Aplikacja z tą ikoną nie jest aktualizowana. Możesz zaktualizować ją ręcznie, aby ponownie uruchomić ten skrót, lub usunąć ikonę."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Aktualizuj"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"OK"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Wstrzymaj aplikacje służbowe"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Cofnij wstrzymywanie"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filtruj"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Niepowodzenie: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Przestrzeń prywatna"</string>
diff --git a/res/values-pt-rPT/strings.xml b/res/values-pt-rPT/strings.xml
index 1c44d9b..04bff1f 100644
--- a/res/values-pt-rPT/strings.xml
+++ b/res/values-pt-rPT/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Nova janela"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Guardar par de apps"</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">"Este par de apps não é suportado neste dispositivo"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Tomar notas"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Adicionar"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Adicione o widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Toque para alterar as definições do widget"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Alterar definições do widget"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Pesquisar apps"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Desativada pelo gestor"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Permitir rotação do ecrã principal"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Quando o telemóvel é rodado"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Pontos de notificação"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Ativados"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Desativados"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"A instalar <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="PROGRESS">%2$s</xliff:g> concluído"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"A transferir o <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="PROGRESS">%2$s</xliff:g> concluído"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"A aguardar a instalação do <xliff:g id="NAME">%1$s</xliff:g>"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"A app <xliff:g id="NAME">%1$s</xliff:g> está arquivada. Toque para transferir e restaurar."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"A app <xliff:g id="NAME">%1$s</xliff:g> está arquivada."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"transferir e restaurar"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Atualização da app necessária"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"A app deste ícone não está atualizada. Pode atualizar manualmente para reativar este atalho ou remover o ícone."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Atualizar"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"OK"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Pausar apps de trabalho"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Retomar"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filtrar"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Falhou: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Espaço privado"</string>
diff --git a/res/values-pt/strings.xml b/res/values-pt/strings.xml
index 3f44591..20cc9fb 100644
--- a/res/values-pt/strings.xml
+++ b/res/values-pt/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Nova janela"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Salvar par de apps"</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">"Este Par de apps não está disponível no dispositivo"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Anotações"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Adicionar"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Adicionar o widget <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Toque para mudar as configurações do widget"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Mudar as configurações do widget"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Pesquisar apps"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Desativado pelo administrador"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Permitir a rotação da tela inicial"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Quando o smartphone for girado"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Pontos de notificação"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Ativados"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Desativado"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"Instalando <xliff:g id="NAME">%1$s</xliff:g>. <xliff:g id="PROGRESS">%2$s</xliff:g> concluído"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"Fazendo download de <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="PROGRESS">%2$s</xliff:g> concluído"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"Aguardando instalação de <xliff:g id="NAME">%1$s</xliff:g>"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"O app <xliff:g id="NAME">%1$s</xliff:g> está arquivado. Toque para baixar e restaurar."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"O app <xliff:g id="NAME">%1$s</xliff:g> está arquivado."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"baixar e restaurar"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Atualização obrigatória do app"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"O app desse ícone não está atualizado. Você pode remover o ícone ou atualizar o app manualmente para reativar esse atalho."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Atualizar"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Ok"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Pausar apps de trabalho"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Ativar"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filtrar"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Falha: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Espaço privado"</string>
diff --git a/res/values-ro/strings.xml b/res/values-ro/strings.xml
index b37d93b..4226920 100644
--- a/res/values-ro/strings.xml
+++ b/res/values-ro/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Fereastră nouă"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Salvează perechea de aplicații"</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">"Perechea de aplicații nu este acceptată pe acest dispozitiv"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Luare de notițe"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Adaugă"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Adaugă widgetul <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Atinge ca să schimbi setările pentru widgeturi"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Modifică setările pentru widgeturi"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Caută aplicații"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Dezactivată de administrator"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Permite rotirea ecranului de pornire"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Când telefonul este rotit"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Puncte de notificare"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Activate"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Dezactivate"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> se instalează, <xliff:g id="PROGRESS">%2$s</xliff:g> finalizat"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> se descarcă (finalizat <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> așteaptă instalarea"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> s-a arhivat. Atinge pentru a descărca și restabili."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> s-a arhivat."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"descarcă și restabilește"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Este necesară actualizarea aplicației"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Aplicația pentru această pictogramă nu este actualizată. Poți să actualizezi manual ca să reactivezi comanda rapidă sau să elimini pictograma."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Actualizează"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"OK"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Întrerupe aplicațiile pentru lucru"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Anulează întreruperea"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filtru"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Eșuare: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Spațiu privat"</string>
diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml
index 321bf37..2b827ef 100644
--- a/res/values-ru/strings.xml
+++ b/res/values-ru/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Новое окно"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Сохранить приложения"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Одновременно использовать эти два приложения на устройстве нельзя."</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Создание заметок"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Добавить"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Добавить виджет \"<xliff:g id="WIDGET_NAME">%1$s</xliff:g>\""</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Нажмите, чтобы изменить настройки виджета"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Изменить настройки виджета"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Поиск приложений"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Функция отключена администратором"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Разрешить поворачивать главный экран"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"При повороте телефона"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Значки уведомлений"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Включены"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Отключены"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"Установка приложения \"<xliff:g id="NAME">%1$s</xliff:g>\" (выполнено <xliff:g id="PROGRESS">%2$s</xliff:g>)"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"Скачивается \"<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>\""</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"Приложение \"<xliff:g id="NAME">%1$s</xliff:g>\" находится в архиве. Нажмите, чтобы скачать его и восстановить"</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"Приложение \"<xliff:g id="NAME">%1$s</xliff:g>\" находится в архиве."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"скачать и восстановить"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Обновите приложение"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Эта версия приложения устарела. Обновите его вручную, чтобы снова пользоваться ярлыком, или удалите значок."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Обновить"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"ОК"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Приостановить рабочие приложения"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Возобновить"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <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>
diff --git a/res/values-si/strings.xml b/res/values-si/strings.xml
index 91c818a..646443e 100644
--- a/res/values-si/strings.xml
+++ b/res/values-si/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"නව කවුළුව"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"යෙදුම් යුගල සුරකින්න"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"මෙම යෙදුම් යුගලය මෙම උපාංගයෙහි සහාය නොදක්වයි"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"සටහන් කර ගැනීම"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"එක් කරන්න"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> විජට්ටුව එක් කරන්න"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"විජට් සැකසීම් වෙනස් කිරීමට තට්ටු කරන්න"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"විජට් සැකසීම් වෙනස් කරන්න"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"යෙදුම් සොයන්න"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"ඔබගේ පරිපාලක විසින් අබල කරන ලදී"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"මුල් තිරය කරකැවීමට ඉඩ දෙන්න"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"දුරකථනය කරකවන විට"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"දැනුම්දීම් තිත්"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"ක්‍රියාත්මකයි"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"ක්‍රියාවිරහිතයි"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> ස්ථාපනය කරමින්, <xliff:g id="PROGRESS">%2$s</xliff:g> සම්පූර්ණයි"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<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> ස්ථාපනය කිරීමට බලා සිටිමින්"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> සංරක්‍ෂිතයි. බා ගෙන ප්‍රතිසාධන කිරීමට තට්ටු කරන්න."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> ලේඛනාරක්ෂණය කර ඇත."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"බාගත කර ප්‍රතිසාධනය කරන්න"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"යෙදුම් යාවත්කාලීනයක් අවශ්‍යයි"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"මෙම නිරූපකය සඳහා යෙදුම යාවත්කාලීන කර නැත. ඔබට මෙම කෙටි මඟ යළි සබල කිරීමට හෝ නිරූපකය ඉවත් කිරීමට හස්තීයව යාවත්කාලීන කළ හැකිය."</string>
     <string name="dialog_update" msgid="2178028071796141234">"යාවත්කාලීන කරන්න"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"තේරුණා"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"කාර්යාල යෙදුම් විරාම කරන්න"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"විරාම නොකරන්න"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <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>
diff --git a/res/values-sk/strings.xml b/res/values-sk/strings.xml
index eaae7c0..f9245cd 100644
--- a/res/values-sk/strings.xml
+++ b/res/values-sk/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Nové okno"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Uložiť pár aplikácií"</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">"Tento pár aplikácií nie je v tomto zariadení podporovaný"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Zapisovanie poznámok"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Pridať"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Pridať miniaplikáciu <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Klepnutím zmeňte nastavenia miniaplikácie"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Zmena nastavení miniaplikácie"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Hľadať aplikácie"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Zakázané vaším správcom"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Povoliť otáčanie plochy"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Pri otočení telefónu"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Bodky upozornení"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Zapnuté"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Vypnuté"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"Inštaluje sa <xliff:g id="NAME">%1$s</xliff:g>. Dokončené: <xliff:g id="PROGRESS">%2$s</xliff:g>."</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"Sťahuje sa aplikácia <xliff:g id="NAME">%1$s</xliff:g>. Stiahnuté: <xliff:g id="PROGRESS">%2$s</xliff:g>"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"Aplikácia <xliff:g id="NAME">%1$s</xliff:g> čaká na inštaláciu"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"Aplikácia <xliff:g id="NAME">%1$s</xliff:g> je archivovaná. Klepnutím ju stiahnite a obnovte."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"Aplikácia <xliff:g id="NAME">%1$s</xliff:g> je archivovaná."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"stiahnuť a obnoviť"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Vyžaduje sa aktualizácia aplikácie"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Aplikácia, ktorú zastupuje táto ikona, nie je aktualizovaná. Môžete ju ručne aktualizovať, aby odkaz znova fungoval, prípadne môžete ikonu odstrániť."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Aktualizovať"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Dobre"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Pozastaviť pracovné aplikácie"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Zrušiť pozastavenie"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filtrujte"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Zlyhalo: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Súkromný priestor"</string>
diff --git a/res/values-sl/strings.xml b/res/values-sl/strings.xml
index dccf2f1..e8d8702 100644
--- a/res/values-sl/strings.xml
+++ b/res/values-sl/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Novo okno"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Shrani par aplikacij"</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">"Ta par aplikacij ni podprt v tej napravi"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Ustvarjanje zapiskov"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Dodaj"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Dodajanje pripomočka »<xliff:g id="WIDGET_NAME">%1$s</xliff:g>«"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Dotaknite se, če želite spremeniti nastavitve pripomočka."</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Spreminjanje nastavitev pripomočka"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Iskanje programov"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Onemogočil skrbnik."</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Dovoli sukanje začetnega zaslona"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Ko se telefon zasuka"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Obvestilne pike"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Vklopljeno"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Izklopljeno"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> se namešča, dokončano: <xliff:g id="PROGRESS">%2$s</xliff:g>"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"Prenašanje aplikacije <xliff:g id="NAME">%1$s</xliff:g>; preneseno <xliff:g id="PROGRESS">%2$s</xliff:g>"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"Aplikacija <xliff:g id="NAME">%1$s</xliff:g> čaka na namestitev"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"Aplikacija <xliff:g id="NAME">%1$s</xliff:g> je arhivirana. Dotaknite se za prenos in obnovitev."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"Aplikacija <xliff:g id="NAME">%1$s</xliff:g> je arhivirana."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"prenos in obnovitev"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Zahtevana je posodobitev aplikacije"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Aplikacija za to ikono ni posodobljena. Lahko jo ročno posodobite, da znova omogočite to bližnjico, ali pa odstranite ikono."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Posodobi"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"V redu"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Začasno zaustavi delovne aplikacije"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Znova aktiviraj"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filtriranje"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Ni uspelo: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Zasebni prostor"</string>
diff --git a/res/values-sq/strings.xml b/res/values-sq/strings.xml
index 84484e8..1e5420a 100644
--- a/res/values-sq/strings.xml
+++ b/res/values-sq/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Dritare e re"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Ruaj çiftin e aplikacioneve"</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">"Ky çift aplikacionesh nuk mbështetet në këtë pajisje"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Mbajtja e shënimeve"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Shto"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Shto miniaplikacionin <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Trokit për të ndryshuar cilësimet e miniaplikacionit"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Ndrysho cilësimet e miniaplikacionit"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Kërko për aplikacione"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Çaktivizuar nga administratori"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Lejo rrotullimin e ekranit bazë"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Kur telefoni rrotullohet"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Pikat e njoftimeve"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Aktiv"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Joaktiv"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> po instalohet, <xliff:g id="PROGRESS">%2$s</xliff:g> i përfunduar"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> po shkarkohet, <xliff:g id="PROGRESS">%2$s</xliff:g> të përfunduara"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> po pret të instalohet"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> është arkivuar. Trokit për ta shkarkuar dhe restauruar."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> është arkivuar."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"shkarko dhe restauro"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Kërkohet përditësimi i aplikacionit"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Aplikacioni për këtë ikonë nuk është përditësuar. Mund ta përditësosh manualisht për të riaktivizuar këtë shkurtore ose hiq ikonën."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Përditëso"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"E kuptova"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Vendos në pauzë aplikacionet e punës"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Hiq nga pauza"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filtro"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Dështoi: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Hapësira private"</string>
diff --git a/res/values-sr/strings.xml b/res/values-sr/strings.xml
index 64383f2..c5b7bd0 100644
--- a/res/values-sr/strings.xml
+++ b/res/values-sr/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Нови прозор"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Сачувај пар апликација"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Овај пар апликација није подржан на овом уређају"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Прављење бележака"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Додај"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Додајте виџет <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Додирните да бисте променили подешавања виџета"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Промените подешавања виџета"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Претражите апликације"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Администратор је онемогућио"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Дозволи ротацију почетног екрана"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Када се телефон ротира"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Тачке за обавештења"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Укључено"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Искључено"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> се инсталира, <xliff:g id="PROGRESS">%2$s</xliff:g> готово"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<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> чека на инсталирање"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"Апликација <xliff:g id="NAME">%1$s</xliff:g> је архивирана. Додирните да бисте је преузели и вратили."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"Апликација <xliff:g id="NAME">%1$s</xliff:g> је архивирана."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"преузмите и вратите"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Треба да ажурирате апликацију"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Апликација за ову икону није ажурирана. Можете да је ручно ажурирате да бисте поново омогућили ову пречицу или уклоните икону."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Ажурирај"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Важи"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Паузирај пословне апликације"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Поново активирај"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <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>
diff --git a/res/values-sv/strings.xml b/res/values-sv/strings.xml
index 47aed86..9ebf366 100644
--- a/res/values-sv/strings.xml
+++ b/res/values-sv/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Nytt fönster"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Spara app-par"</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">"De här apparna som ska användas tillsammans stöds inte på den här enheten"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Anteckna"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Lägg till"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Lägg till widgeten <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Tryck för att ändra inställningarna för widgeten"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Ändra inställningarna för widgeten"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Sök efter appar"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Inaktiverat av administratören"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Tillåt rotering av startskärmen"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"När telefonen vrids"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Aviseringsprickar"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"På"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Av"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> installeras. <xliff:g id="PROGRESS">%2$s</xliff:g> har slutförts"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> laddas ned, <xliff:g id="PROGRESS">%2$s</xliff:g> klart"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> väntar på installation"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> har arkiverats. Tryck för att ladda ner och återställa."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> har arkiverats."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"ladda ned och återställ"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Du måste uppdatera appen"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Appen för den här ikonen har inte uppdaterats. Du kan uppdatera den manuellt för att återaktivera genvägen eller ta bort ikonen."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Uppdatera"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"OK"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Pausa jobbappar"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Återuppta"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filter"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Misslyckades: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Privat rum"</string>
diff --git a/res/values-sw/strings.xml b/res/values-sw/strings.xml
index 5eadd1d..a7725f3 100644
--- a/res/values-sw/strings.xml
+++ b/res/values-sw/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Dirisha Jipya"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Hifadhi jozi ya programu"</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">"Jozi hii ya programu haitumiki kwenye kifaa hiki"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Kuandika madokezo"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Weka"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Weka wijeti ya <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Gusa ili ubadilishe mipangilio ya wijeti"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Badilisha mipangilio ya wijeti"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Tafuta programu"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Imezimwa na msimamizi wako"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Ruhusu kipengele cha kuzungusha skrini ya kwanza"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Simu inapozungushwa"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Vitone vya arifa"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Imewashwa"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Imezimwa"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"Inasakinisha <xliff:g id="NAME">%1$s</xliff:g>, imekamilika <xliff:g id="PROGRESS">%2$s</xliff:g>"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> inapakuliwa, <xliff:g id="PROGRESS">%2$s</xliff:g> imekamilika"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> inasubiri kusakinisha"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> imewekwa kwenye kumbukumbu. Gusa ili upakue na urejeshe."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> imewekwa kwenye kumbukumbu."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"pakua na urejeshe"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Unahitaji kusasisha programu"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Programu ya aikoni hii haijasasishwa. Unaweza kusasisha mwenyewe ili uruhusu upya njia hii ya mkato au uondoe aikoni."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Sasisha"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Nimeelewa"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Simamisha programu za kazini"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Acha kusimamisha"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Kichujio"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Hitilafu: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Nafasi ya faragha"</string>
diff --git a/res/values-ta/strings.xml b/res/values-ta/strings.xml
index d84485a..295367d 100644
--- a/res/values-ta/strings.xml
+++ b/res/values-ta/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"புதிய சாளரம்"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"ஆப்ஸ் ஜோடியைச் சேமி"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"இந்தச் சாதனத்தில் இந்த ஆப்ஸ் ஜோடி ஆதரிக்கப்படவில்லை"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"குறிப்பெடுத்தல்"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"சேர்"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> விட்ஜெட்டைச் சேர்க்கும்"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"விட்ஜெட் அமைப்புகளை மாற்றத் தட்டவும்"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"விட்ஜெட் அமைப்புகளை மாற்றும்"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"ஆப்ஸில் தேடுக"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"உங்கள் நிர்வாகி முடக்கியுள்ளார்"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"முகப்புத் திரை சுழற்சியை அனுமதித்தல்"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"மொபைலைச் சுழற்றும் போது"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"அறிவிப்புப் புள்ளிகள்"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"ஆன்"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"ஆஃப்"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> நிறுவப்படுகிறது, <xliff:g id="PROGRESS">%2$s</xliff:g> முடிந்தது"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<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>ஐ நிறுவுவதற்காகக் காத்திருக்கிறது"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> காப்பிடப்பட்டுள்ளது. அதைப் பதிவிறக்கி மீட்டெடுக்க தட்டுங்கள்."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> காப்பிடப்பட்டுள்ளது."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"பதிவிறக்கி மீட்டெடுக்கும்"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"ஆப்ஸைப் புதுப்பியுங்கள்"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"இந்த ஐகானுக்கான ஆப்ஸ் புதுப்பிக்கப்படவில்லை. இந்த ஷார்ட்கட்டை மீண்டும் இயக்கவோ ஐகானை அகற்றவோ நீங்களாகவே புதுப்பிக்கலாம்."</string>
     <string name="dialog_update" msgid="2178028071796141234">"புதுப்பி"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"சரி"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"பணி ஆப்ஸை இடைநிறுத்து"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"மீண்டும் இயக்கு"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <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>
diff --git a/res/values-te/strings.xml b/res/values-te/strings.xml
index 8487e96..5859665 100644
--- a/res/values-te/strings.xml
+++ b/res/values-te/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"కొత్త విండో"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"యాప్ పెయిర్‌ను సేవ్ చేయండి"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"ఈ పరికరంలో ఈ యాప్ పెయిర్ సపోర్ట్ చేయదు"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"నోట్-టేకింగ్"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"జోడించండి"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> విడ్జెట్‌ను జోడించండి"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"విడ్జెట్ సెట్టింగ్‌లను మార్చడానికి ట్యాప్ చేయండి"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"విడ్జెట్ సెట్టింగ్‌లను మార్చండి"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"యాప్‌ల కోసం సెర్చ్ చేయండి"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"మీ నిర్వాహకులు నిలిపివేసారు"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"మొదటి స్క్రీన్ రొటేషన్‌ను అనుమతించండి"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"ఫోన్‌‌ను తిప్పినప్పుడు"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"నోటిఫికేషన్ డాట్‌లు"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"ఆన్"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"ఆఫ్"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g>‌ను ఇన్‌స్టాల్ చేయడం, <xliff:g id="PROGRESS">%2$s</xliff:g> పూర్తయింది"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<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> ఇన్‌స్టాల్ కావడానికి వేచి ఉంది"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> ఆర్కైవ్ చేయబడింది. డౌన్‌లోడ్ చేయడానికి, రీస్టోర్ చేయడానికి ట్యాప్ చేయండి."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> ఆర్కైవ్ చేయబడింది."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"డౌన్‌లోడ్ చేసి, రీస్టోర్ చేయండి"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"యాప్‌ను అప్‌డేట్ చేయడం అవసరం"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"ఈ చిహ్నం కోసం యాప్ అప్‌డేట్ చేయబడలేదు. మీరు ఈ షార్ట్‌కట్‌ను మళ్లీ ఎనేబుల్ చేయడానికి మాన్యువల్‌గా అప్‌డేట్ చేయవచ్చు లేదా చిహ్నాన్ని తీసివేయవచ్చు."</string>
     <string name="dialog_update" msgid="2178028071796141234">"అప్‌డేట్ చేయండి"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"అర్థమైంది"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"వర్క్ యాప్‌లను పాజ్ చేయండి"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"పాజ్ నుండి తీసివేయండి"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <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>
diff --git a/res/values-th/strings.xml b/res/values-th/strings.xml
index 71f4d15..fd2ba21 100644
--- a/res/values-th/strings.xml
+++ b/res/values-th/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"หน้าต่างใหม่"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"บันทึกคู่แอป"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"ไม่รองรับคู่แอปนี้ในอุปกรณ์เครื่องนี้"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"การจดบันทึก"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"เพิ่ม"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"เพิ่มวิดเจ็ต <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"แตะเพื่อเปลี่ยนการตั้งค่าวิดเจ็ต"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"เปลี่ยนการตั้งค่าวิดเจ็ต"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"ค้นหาแอป"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"ปิดใช้โดยผู้ดูแลระบบ"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"อนุญาตให้หมุนหน้าจอหลัก"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"เมื่อหมุนโทรศัพท์"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"เครื่องหมายจุดแสดงการแจ้งเตือน"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"เปิด"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"ปิด"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"กำลังติดตั้ง <xliff:g id="NAME">%1$s</xliff:g> เสร็จแล้ว <xliff:g id="PROGRESS">%2$s</xliff:g>"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"กำลังดาวน์โหลด <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> กำลังรอติดตั้ง"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"เก็บถาวร <xliff:g id="NAME">%1$s</xliff:g> แล้ว แตะเพื่อดาวน์โหลดและกู้คืน"</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"เก็บถาวร <xliff:g id="NAME">%1$s</xliff:g> แล้ว"</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"ดาวน์โหลดและกู้คืน"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"ต้องอัปเดตแอป"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"แอปสำหรับไอคอนนี้ยังไม่ได้อัปเดต คุณอัปเดตด้วยตนเองได้โดยเปิดใช้ทางลัดนี้อีกครั้งหรือนำไอคอนออก"</string>
     <string name="dialog_update" msgid="2178028071796141234">"อัปเดต"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"รับทราบ"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"หยุดแอปงานชั่วคราว"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"ยกเลิกการหยุดชั่วคราว"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <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>
diff --git a/res/values-tl/strings.xml b/res/values-tl/strings.xml
index 7cf6a44..69f88e1 100644
--- a/res/values-tl/strings.xml
+++ b/res/values-tl/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Bagong Window"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"I-save ang app pair"</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">"Hindi sinusuportahan sa device na ito ang pares ng app na ito"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Pagtatala"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Idagdag"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Idagdag ang widget na <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"I-tap para baguhin ang mga setting ng widget"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Baguhin ang mga setting ng widget"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Maghanap ng mga app"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Na-disable ng iyong admin"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Payagan ang pag-rotate ng home screen"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Kailan maro-rotate ang telepono"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Mga notification dot"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Naka-on"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Naka-off"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"Ini-install ang <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="PROGRESS">%2$s</xliff:g> kumpleto"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"Dina-download na ang <xliff:g id="NAME">%1$s</xliff:g>, tapos na ang <xliff:g id="PROGRESS">%2$s</xliff:g>"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"Hinihintay nang mag-install ang <xliff:g id="NAME">%1$s</xliff:g>"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"Naka-archive ang <xliff:g id="NAME">%1$s</xliff:g>. I-tap para i-download at i-restore."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"Naka-archive ang <xliff:g id="NAME">%1$s</xliff:g>."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"i-download at i-restore"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Kinakailangang i-update ang app"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Hindi updated ang app para sa icon na ito. Puwede kang manual na mag-update para ma-enable ulit ang shortcut na ito, o alisin ang icon."</string>
     <string name="dialog_update" msgid="2178028071796141234">"I-update"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"OK"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"I-pause ang mga app para sa trabaho"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"I-unpause"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filter"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Hindi nagawa: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Pribadong space"</string>
diff --git a/res/values-tr/strings.xml b/res/values-tr/strings.xml
index d55181c..e61b670 100644
--- a/res/values-tr/strings.xml
+++ b/res/values-tr/strings.xml
@@ -31,6 +31,7 @@
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Bölünmüş ekran"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"%1$s uygulama bilgileri"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"%1$s ile ilgili kullanım ayarları"</string>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Yeni Pencere"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Uygulama çiftini kaydedin"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Bu uygulama çifti bu cihazda desteklenmiyor"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Not alma"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Ekle"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> widget\'ı ekle"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Widget ayarlarını değiştirmek için dokunun"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Widget ayarlarını değiştir"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Uygulamalarda ara"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Yöneticiniz tarafından devre dışı bırakıldı"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Ana ekranı döndürmeye izin ver"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Telefon döndürüldüğünde"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Bildirim noktaları"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Açık"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Kapalı"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> yükleniyor, <xliff:g id="PROGRESS">%2$s</xliff:g> tamamlandı"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> indiriliyor, <xliff:g id="PROGRESS">%2$s</xliff:g> tamamlandı"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> uygulaması yüklenmek için bekliyor"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> arşivlendi. İndirip geri yüklemek için dokunun."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> arşivlendi."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"indir ve geri yükle"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Uygulama güncellemesi gerekli"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Bu simgenin uygulaması güncellenmemiş. Simgeyi kaldırabilir ya da uygulamayı manuel olarak güncelleyerek bu kısayolu yeniden etkinleştirebilirsiniz."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Güncelle"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Anladım"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"İş uygulamalarını duraklat"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Devam ettir"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filtre"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Başarısız: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Gizli alan"</string>
diff --git a/res/values-uk/strings.xml b/res/values-uk/strings.xml
index b5c1c70..fd293e3 100644
--- a/res/values-uk/strings.xml
+++ b/res/values-uk/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Нове вікно"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Зберегти пару додатків"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"Ці два додатки не можна одночасно використовувати на цьому пристрої"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Створення нотаток"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Додати"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Додати віджет \"<xliff:g id="WIDGET_NAME">%1$s</xliff:g>\""</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Натисніть, щоб змінити налаштування віджета"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Змінити налаштування віджета"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Пошук додатків"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Вимкнув адміністратор"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Дозволити обертання головного екрана"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Коли телефон обертається"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Значки сповіщень"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Увімкнено"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Вимкнено"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> встановлюється, виконано <xliff:g id="PROGRESS">%2$s</xliff:g>"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<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> очікує на завантаження"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"Додаток <xliff:g id="NAME">%1$s</xliff:g> заархівовано. Натисніть, щоб завантажити й відновити."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"Додаток <xliff:g id="NAME">%1$s</xliff:g> заархівовано."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"завантажити й відновити"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Потрібно оновити додаток"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Додаток для цього значка не оновлено. Ви можете оновити його вручну, щоб знову ввімкнути цю швидку команду, або можете вилучити значок."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Оновити"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Зрозуміло"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Призупинити робочі додатки"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Відновити"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <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>
diff --git a/res/values-ur/strings.xml b/res/values-ur/strings.xml
index 6fa76dd..911a2d5 100644
--- a/res/values-ur/strings.xml
+++ b/res/values-ur/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"نئی ونڈو"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"ایپس کے جوڑے کو محفوظ کریں"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"ایپس کا یہ جوڑا اس آلے پر تعاون یافتہ نہیں ہے"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"نوٹ لکھنا"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"شامل کریں"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> ویجیٹ شامل کریں"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"ویجیٹ ترتیبات تبدیل کرنے کے لیے تھپتھپائیں"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"ویجیٹ ترتیبات تبدیل کریں"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"ایپس تلاش کریں"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"آپ کے منتظم کی طرف سے غیر فعال کر دیا گیا"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"ہوم اسکرین گھمانے کی اجازت دیں"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"جب فون گھمایا جاتا ہے"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"اطلاعاتی ڈاٹس"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"آن ہے"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"آف ہے"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> انسٹال کی جا رہی ہے، <xliff:g id="PROGRESS">%2$s</xliff:g> مکمل ہو گئی"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<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> انسٹال ہونے کا انتظار کر رہی ہے"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> کو آرکائیو کر لیا گیا ہے۔ ڈاؤن لوڈ اور بحال کرنے کیلئے تھپتھپائیں۔"</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"‫<xliff:g id="NAME">%1$s</xliff:g> کو آرکائیو کر لیا گیا ہے۔"</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"ڈاؤن لوڈ کریں اور بحال کریں"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"ایپ کی اپ ڈیٹ درکار ہے"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"اس آئیکن کیلئے ایپ کو اپ ڈیٹ نہیں کیا گیا ہے۔ آپ اس شارٹ کٹ کو دوبارہ فعال کرنے کے لیے دستی طور پر اپ ڈیٹ کر سکتے ہیں، یا آئیکن کو ہٹا سکتے ہیں۔"</string>
     <string name="dialog_update" msgid="2178028071796141234">"اپ ڈیٹ کریں"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"سمجھ آ گئی"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"ورک ایپس موقوف کریں"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"چلائیں"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <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>
diff --git a/res/values-uz/strings.xml b/res/values-uz/strings.xml
index 21a8145..aacb35f 100644
--- a/res/values-uz/strings.xml
+++ b/res/values-uz/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Yangi oyna"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Ilova juftini saqlash"</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">"Bu ilova jufti ushbu qurilmada ishlamaydi"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Qayd olish"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Chiqarish"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> vidjetini chiqarish"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Vidjet sozlamalarini oʻzgartirish uchun bosing"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Vidjet sozlamalarini oʻzgartirish"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Ilovalarni qidirish"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Administrator tomonidan o‘chirilgan"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Bosh ekranni burishga ruxsat"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Telefon burilganda"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Bildirishnoma belgilari"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Yoniq"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Oʻchiq"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> oʻrnatlmoqda, <xliff:g id="PROGRESS">%2$s</xliff:g> yakunlandi"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> yuklab olinmoqda, <xliff:g id="PROGRESS">%2$s</xliff:g> bajarildi"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> ilovasi o‘rnatilishi kutilmoqda"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> arxivlangan. Yuklab olish va tiklash uchun bosing."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> arxivlangan."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"yuklab olish va tiklash"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Ilovani yangilash zarur"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Bu belgi uchun ilova yangilanmagan. Ushbu yorliqni qayta yoqish uchun oddiy usulda yangilashingiz yoki belgini olib tashlashingiz mumkin."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Yangilash"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"OK"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Ishga oid ilovalarni pauza qilish"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Pauzadan chiqarish"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Saralash"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Xato: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Shaxsiy xona"</string>
diff --git a/res/values-v31/colors.xml b/res/values-v31/colors.xml
index a5cdfc7..d74e308 100644
--- a/res/values-v31/colors.xml
+++ b/res/values-v31/colors.xml
@@ -104,11 +104,6 @@
     <color name="widget_picker_add_button_text_color_light">
         @android:color/system_accent1_0</color>
 
-    <color name="work_fab_bg_color">
-        @android:color/system_accent1_200</color>
-    <color name="work_fab_icon_color">
-        @android:color/system_accent1_900</color>
-
     <color name="overview_foreground_scrim_color">@android:color/system_neutral1_1000</color>
 
     <color name="material_color_on_surface">@android:color/system_neutral1_900</color>
diff --git a/res/values-vi/strings.xml b/res/values-vi/strings.xml
index b0bac73..cea93da 100644
--- a/res/values-vi/strings.xml
+++ b/res/values-vi/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Cửa sổ mới"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Lưu cặp ứng dụng"</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">"Cặp ứng dụng này không hoạt động được trên thiết bị này"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Ghi chú"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Thêm"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Thêm tiện ích <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Nhấn để thay đổi chế độ cài đặt tiện ích"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Thay đổi chế độ cài đặt tiện ích"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Tìm kiếm ứng dụng"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Bị tắt bởi quản trị viên của bạn"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Cho phép xoay màn hình chính"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Khi xoay điện thoại"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Dấu chấm thông báo"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Đang bật"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Tắt"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"Đang cài đặt <xliff:g id="NAME">%1$s</xliff:g>, hoàn tất <xliff:g id="PROGRESS">%2$s</xliff:g>"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"Đang tải xuống <xliff:g id="NAME">%1$s</xliff:g>, <xliff:g id="PROGRESS">%2$s</xliff:g> hoàn tất"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"Đang chờ cài đặt <xliff:g id="NAME">%1$s</xliff:g>"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"<xliff:g id="NAME">%1$s</xliff:g> đã được lưu trữ. Hãy nhấn để tải xuống và khôi phục."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"<xliff:g id="NAME">%1$s</xliff:g> đã được lưu trữ."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"tải xuống và khôi phục"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Cần cập nhật ứng dụng"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"Ứng dụng cho biểu tượng này chưa được cập nhật. Bạn có thể cập nhật theo cách thủ công để bật lại phím tắt này hoặc xóa biểu tượng."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Cập nhật"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Tôi hiểu"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Tạm dừng các ứng dụng công việc"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Bỏ tạm dừng"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Bộ lọc"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Không thực hiện được thao tác: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Không gian riêng tư"</string>
diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml
index 112b945..9efd6c3 100644
--- a/res/values-zh-rCN/strings.xml
+++ b/res/values-zh-rCN/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"新窗口"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"保存应用组合"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"在该设备上无法使用此应用对"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"记事"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"添加"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"添加“<xliff:g id="WIDGET_NAME">%1$s</xliff:g>”微件"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"点按即可更改微件设置"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"更改微件设置"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"搜索应用"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"已被您的管理员停用"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"允许旋转主屏幕"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"手机旋转时"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"通知圆点"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"已开启"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"已关闭"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"正在安装<xliff:g id="NAME">%1$s</xliff:g>,已完成 <xliff:g id="PROGRESS">%2$s</xliff:g>"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"正在下载<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>正在等待安装"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"已归档“<xliff:g id="NAME">%1$s</xliff:g>”。点按即可进行下载并恢复。"</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"已归档“<xliff:g id="NAME">%1$s</xliff:g>”。"</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"下载并恢复"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"需要更新应用"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"此图标对应的应用未更新。您可以手动更新以重新启用该快捷方式,或者移除此图标。"</string>
     <string name="dialog_update" msgid="2178028071796141234">"更新"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"知道了"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"暂停工作应用"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"取消暂停"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <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>
diff --git a/res/values-zh-rHK/strings.xml b/res/values-zh-rHK/strings.xml
index e63093e..c944240 100644
--- a/res/values-zh-rHK/strings.xml
+++ b/res/values-zh-rHK/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"新視窗"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"儲存應用程式配對"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"此裝置不支援此應用程式配對"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"做筆記"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"新增"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"加<xliff:g id="WIDGET_NAME">%1$s</xliff:g>小工具"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"輕按即可變更小工具設定"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"變更小工具設定"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"搜尋應用程式"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"已由你的管理員停用"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"允許旋轉主畫面"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"隨手機旋轉"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"通知圓點"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"開啟"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"關閉"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"正在安裝「<xliff:g id="NAME">%1$s</xliff:g>」(已完成 <xliff:g id="PROGRESS">%2$s</xliff:g>)"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"正在下載 <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>"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"「<xliff:g id="NAME">%1$s</xliff:g>」已封存。輕按即可下載並還原。"</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"「<xliff:g id="NAME">%1$s</xliff:g>」已封存。"</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"下載及還原"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"必須更新應用程式"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"你尚未更新這個圖示代表的應用程式。你可以手動更新以重新啟用此快速鍵,或者移除圖示。"</string>
     <string name="dialog_update" msgid="2178028071796141234">"更新"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"知道了"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"暫停工作應用程式"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"取消暫停"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <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>
diff --git a/res/values-zh-rTW/strings.xml b/res/values-zh-rTW/strings.xml
index 25f9703..476c0e5 100644
--- a/res/values-zh-rTW/strings.xml
+++ b/res/values-zh-rTW/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"新視窗"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"儲存應用程式配對"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"這部裝置不支援這組應用程式配對"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"做筆記"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"新增"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"新增「<xliff:g id="WIDGET_NAME">%1$s</xliff:g>」小工具"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"輕觸即可變更小工具設定"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"變更小工具設定"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"搜尋應用程式"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"已由你的管理員停用"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"允許旋轉主畫面"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"當手機旋轉時"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"通知圓點"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"開啟"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"關閉"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"正在安裝「<xliff:g id="NAME">%1$s</xliff:g>」(已完成 <xliff:g id="PROGRESS">%2$s</xliff:g>)"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"正在下載「<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>」"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"「<xliff:g id="NAME">%1$s</xliff:g>」已封存。輕觸即可下載並還原。"</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"「<xliff:g id="NAME">%1$s</xliff:g>」已封存。"</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"下載及還原"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"必須更新應用程式"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"這個圖示代表的應用程式未更新。手動更新即可重新啟用這個捷徑,你也可以移除圖示。"</string>
     <string name="dialog_update" msgid="2178028071796141234">"更新"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"我知道了"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"暫停工作應用程式"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"取消暫停"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <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>
diff --git a/res/values-zu/strings.xml b/res/values-zu/strings.xml
index ec1f941..623454b 100644
--- a/res/values-zu/strings.xml
+++ b/res/values-zu/strings.xml
@@ -31,6 +31,7 @@
     <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>
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"Iwindi Elisha"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Londoloza i-app ebhangqiwe"</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">"Lokhu kubhanqwa kwe-app akusekelwa kule divayisi"</string>
@@ -68,6 +69,12 @@
     <string name="widget_category_note_taking" msgid="3469689394504266039">"Ukuthatha amanothi"</string>
     <string name="widget_add_button_label" msgid="2761267068711937179">"Engeza"</string>
     <string name="widget_add_button_content_description" msgid="1810530016360039643">"Engeza iwijethi ye-<xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+    <!-- no translation found for widgets_list_expand_button_label (7912016136574932622) -->
+    <skip />
+    <!-- no translation found for widgets_list_expand_button_content_description (4600513860973450888) -->
+    <skip />
+    <!-- no translation found for widgets_list_expanded (7374857868788557730) -->
+    <skip />
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Thepha ukuze ushintshe amasethingi ewijethi"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Shintsha amasethingi ewijethi"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Sesha izinhlelo zokusebenza"</string>
@@ -123,6 +130,10 @@
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"Kukhutshazwe umlawuli wakho"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"Vumela ukuzungezisa kwesikrini sasekhaya"</string>
     <string name="allow_rotation_desc" msgid="8662546029078692509">"Uma ifoni iphendukiswa"</string>
+    <!-- no translation found for landscape_mode_title (5138814555934843926) -->
+    <skip />
+    <!-- no translation found for landscape_mode_desc (7372569859592816793) -->
+    <skip />
     <string name="notification_dots_title" msgid="9062440428204120317">"Amacashazi esaziso"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"Vuliwe"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"Valiwe"</string>
@@ -141,7 +152,8 @@
     <string name="app_installing_title" msgid="5864044122733792085">"I-<xliff:g id="NAME">%1$s</xliff:g> iyafakwa, seyiqede <xliff:g id="PROGRESS">%2$s</xliff:g>"</string>
     <string name="app_downloading_title" msgid="8336702962104482644">"I-<xliff:g id="NAME">%1$s</xliff:g> iyalandwa, <xliff:g id="PROGRESS">%2$s</xliff:g> kuqediwe"</string>
     <string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> ilinde ukufakwa"</string>
-    <string name="app_archived_title" msgid="7717956158562544081">"Okuthi <xliff:g id="NAME">%1$s</xliff:g> kufakwe kungobo yomlando. Thepha ukuze udawunilode futhi ubuyisele."</string>
+    <string name="app_archived_title" msgid="4548283110222420708">"Okuthi <xliff:g id="NAME">%1$s</xliff:g> kufakwe kungobo yomlando."</string>
+    <string name="app_unarchiving_action" msgid="5736107006413929484">"dawuniloda uphinde ubuyisele"</string>
     <string name="dialog_update_title" msgid="114234265740994042">"Kudingeka isibuyekezo se-app"</string>
     <string name="dialog_update_message" msgid="4176784553982226114">"I-app yalesi sithonjana ibuyekeziwe. Ungabuyekeza mathupha ukuze uphinde unike amandla lesi sinqamuleli, noma ususe isithonjana."</string>
     <string name="dialog_update" msgid="2178028071796141234">"Vuselela"</string>
@@ -186,6 +198,8 @@
     <string name="work_apps_paused_edu_accept" msgid="6377476824357318532">"Ngiyezwa"</string>
     <string name="work_apps_pause_btn_text" msgid="4669288269140620646">"Misa ama-app omsebenzi"</string>
     <string name="work_apps_enable_btn_text" msgid="1736198302467317371">"Susa ukumisa"</string>
+    <!-- no translation found for work_scheduler_button_content_description (917340740986764967) -->
+    <skip />
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Hlunga"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Yehlulekile: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Isikhala esiyimfihlo"</string>
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index 57c9bc7..4ccdd53 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -126,6 +126,8 @@
     <attr name="widgetPickerCollapseHandleColor" format="color"/>
     <attr name="widgetPickerAddButtonBackgroundColor" format="color"/>
     <attr name="widgetPickerAddButtonTextColor" format="color"/>
+    <attr name="widgetPickerExpandButtonBackgroundColor" format="color"/>
+    <attr name="widgetPickerExpandButtonTextColor" format="color"/>
     <attr name="widgetCellTitleColor" format="color" />
     <attr name="widgetCellSubtitleColor" format="color" />
 
@@ -208,8 +210,18 @@
         <attr name="layout_sticky" format="boolean" />
     </declare-styleable>
 
+    <declare-styleable name="NumRows">
+        <attr name="minDeviceWidthPx" format="float"/>
+        <attr name="minDeviceHeightPx" format="float"/>
+        <attr name="numRowsNew" format="integer"/>
+        <attr name="dbFile" />
+        <attr name="defaultLayoutId"/>
+        <attr name="demoModeLayoutId"/>
+    </declare-styleable>
+
     <declare-styleable name="GridDisplayOption">
         <attr name="name" format="string" />
+        <attr name="title" />
 
         <attr name="numRows" format="integer" />
         <attr name="numColumns" format="integer" />
@@ -260,6 +272,9 @@
              defaults to @dimen/taskbar_button_margin_default -->
         <attr name="inlineNavButtonsEndSpacing" format="reference" />
 
+        <!-- Grid flips row and column count when rotating the device -->
+        <attr name="isDualGrid" format="boolean" />
+
         <attr name="dbFile" format="string" />
         <attr name="defaultLayoutId" format="reference" />
         <attr name="demoModeLayoutId" format="reference" />
@@ -294,8 +309,13 @@
         <!-- File that contains the specs for all apps icon and text size.
         Needs FeatureFlags.ENABLE_RESPONSIVE_WORKSPACE enabled -->
         <attr name="allAppsCellSpecsId" format="reference" />
+        <attr name="rowCountSpecsId" format="reference" />
         <!-- defaults to allAppsCellSpecsId, if not specified -->
         <attr name="allAppsCellSpecsTwoPanelId" format="reference" />
+        <!-- defaults to false, if not specified -->
+        <attr name="isFixedLandscape" format="boolean" />
+        <!-- defaults to false, if not specified -->
+        <attr name="isOldGrid" format="boolean" />
 
         <!-- By default all categories are enabled -->
         <attr name="deviceCategory" format="integer">
diff --git a/res/values/colors.xml b/res/values/colors.xml
index 3f8bede..4549b86 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -90,6 +90,7 @@
     <color name="drop_target_hover_button_color_dark">#0842A0</color>
 
     <color name="taskbar_running_app_indicator_color">#000000</color>
+    <color name="taskbar_minimized_app_indicator_color">#000000</color>
 
     <color name="preload_icon_accent_color_light">#00668B</color>
     <color name="preload_icon_background_color_light">#B5CAD7</color>
@@ -97,8 +98,6 @@
     <color name="preload_icon_background_color_dark">#40484D</color>
 
     <color name="work_turn_on_stroke">?android:attr/colorAccent</color>
-    <color name="work_fab_bg_color">#A8C7FA</color>
-    <color name="work_fab_icon_color">#041E49</color>
 
     <color name="widget_picker_primary_surface_color_light">#EFEDED</color>
     <color name="widget_picker_secondary_surface_color_light">#FAF9F8</color>
@@ -118,6 +117,12 @@
     <color name="widget_picker_collapse_handle_color_light">#C4C7C5</color>
     <color name="widget_picker_add_button_background_color_light">#0B57D0</color>
     <color name="widget_picker_add_button_text_color_light">#0B57D0</color>
+    <color name="widget_picker_expand_button_background_color_light">
+        @color/widget_picker_secondary_surface_color_light
+    </color>
+    <color name="widget_picker_expand_button_text_color_light">
+        @color/widget_picker_header_app_title_color_light
+    </color>
     <color name="widget_cell_title_color_light">@color/system_on_surface_light</color>
     <color name="widget_cell_subtitle_color_light">@color/system_on_surface_variant_light</color>
 
@@ -139,6 +144,12 @@
     <color name="widget_picker_collapse_handle_color_dark">#444746</color>
     <color name="widget_picker_add_button_background_color_dark">#062E6F</color>
     <color name="widget_picker_add_button_text_color_dark">#FFFFFF</color>
+    <color name="widget_picker_expand_button_background_color_dark">
+        @color/widget_picker_secondary_surface_color_dark
+    </color>
+    <color name="widget_picker_expand_button_text_color_dark">
+        @color/widget_picker_header_app_title_color_dark
+    </color>
     <color name="widget_cell_title_color_dark">@color/system_on_surface_dark</color>
     <color name="widget_cell_subtitle_color_dark">@color/system_on_surface_variant_dark</color>
 
diff --git a/res/values/config.xml b/res/values/config.xml
index 701e64a..f6f3c95 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -76,16 +76,16 @@
     <string name="taskbar_view_callbacks_factory_class" translatable="false"></string>
     <string name="launcher_restore_event_logger_class" translatable="false"></string>
     <string name="taskbar_edu_tooltip_controller_class" translatable="false"></string>
-    <string name="contextual_edu_manager_class" translatable="false"></string>
     <!--  Used for determining category of a widget presented in widget recommendations. -->
     <string name="widget_recommendation_category_provider_class" translatable="false"></string>
-    <string name="api_wrapper_class" translatable="false"></string>
 
     <!-- Default packages -->
     <string name="wallpaper_picker_package" translatable="false"></string>
     <string name="local_colors_extraction_class" translatable="false"></string>
     <string name="search_session_manager_class" translatable="false"></string>
-    <string name="plugin_manager_wrapper_class" translatable="false"></string>
+
+    <!-- Filters for widgets displayed in the widget picker  -->
+    <string name="widgets_filter_data_provider_class" translatable="false"></string>
 
     <!-- Scalable Grid configuration -->
     <!-- This is a float because it is converted to dp later in DeviceProfile -->
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index f8c075f..61d99d7 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -126,6 +126,10 @@
     <dimen name="all_apps_work_profile_tab_footer_bottom_padding">20dp</dimen>
     <dimen name="all_apps_tabs_button_horizontal_padding">4dp</dimen>
     <dimen name="all_apps_tabs_vertical_padding">6dp</dimen>
+    <dimen name="all_apps_tabs_vertical_padding_focus">1dp</dimen>
+    <dimen name="all_apps_tabs_focus_width">5dp</dimen>
+    <dimen name="all_apps_tabs_focus_border">3dp</dimen>
+    <dimen name="all_apps_tabs_focus_padding">2dp</dimen>
     <dimen name="all_apps_tabs_margin_top">8dp</dimen>
     <dimen name="all_apps_divider_height">2dp</dimen>
     <dimen name="all_apps_divider_width">128dp</dimen>
@@ -151,16 +155,21 @@
     <!-- Floating action button inside work tab to toggle work profile -->
     <dimen name="work_fab_height">56dp</dimen>
     <dimen name="work_fab_radius">16dp</dimen>
+    <dimen name="work_fab_elevation">6dp</dimen>
     <dimen name="work_fab_icon_size">24dp</dimen>
-    <dimen name="work_fab_icon_end_margin">12dp</dimen>
-    <dimen name="work_fab_text_end_margin">16dp</dimen>
+    <dimen name="work_fab_icon_vertical_margin">16dp</dimen>
+    <dimen name="work_fab_icon_start_margin_expanded">4dp</dimen>
+    <dimen name="work_fab_text_start_margin">8dp</dimen>
+    <dimen name="work_fab_text_end_margin">10dp</dimen>
     <dimen name="work_card_padding_horizontal">10dp</dimen>
     <dimen name="work_fab_width">214dp</dimen>
     <dimen name="work_card_button_height">52dp</dimen>
     <dimen name="work_fab_margin">16dp</dimen>
     <dimen name="work_fab_margin_bottom">20dp</dimen>
-    <dimen name="work_mode_fab_background_start_padding">16dp</dimen>
-    <dimen name="work_mode_fab_background_end_padding">4dp</dimen>
+    <dimen name="work_mode_fab_background_horizontal_padding">16dp</dimen>
+    <dimen name="work_scheduler_background_padding">16dp</dimen>
+    <dimen name="work_scheduler_bottom_margin">8dp</dimen>
+    <dimen name="work_scheduler_size">56dp</dimen>
     <dimen name="work_profile_footer_padding">20dp</dimen>
     <dimen name="work_edu_card_margin">16dp</dimen>
     <dimen name="work_edu_card_radius">16dp</dimen>
@@ -220,8 +229,18 @@
     <!-- Bottom margin for the search and recommended widgets container with work profile -->
     <dimen name="search_and_recommended_widgets_container_small_bottom_margin">10dp</dimen>
 
+    <dimen name="widget_header_focus_ring_width">3dp</dimen>
+    <dimen name="widget_focus_ring_corner_radius">28dp</dimen>
+    <dimen name="widget_header_background_border">5dp</dimen>
+
     <dimen name="widget_list_top_bottom_corner_radius">28dp</dimen>
     <dimen name="widget_list_content_corner_radius">4dp</dimen>
+    <!-- Button that expands the widget apps list in the widget picker. -->
+    <dimen name="widgets_list_expand_button_drawable_padding">8dp</dimen>
+    <dimen name="widgets_list_expand_button_start_padding">16dp</dimen>
+    <dimen name="widgets_list_expand_button_end_padding">24dp</dimen>
+    <dimen name="widgets_list_expand_button_vertical_padding">16dp</dimen>
+    <dimen name="widgets_list_expand_button_top_margin">14dp</dimen>
 
     <dimen name="widget_list_header_view_vertical_padding">20dp</dimen>
     <dimen name="widget_list_entry_spacing">2dp</dimen>
@@ -422,9 +441,7 @@
     <dimen name="taskbar_running_app_indicator_height">0dp</dimen>
     <dimen name="taskbar_running_app_indicator_width">0dp</dimen>
     <dimen name="taskbar_running_app_indicator_top_margin">0dp</dimen>
-    <dimen name="taskbar_minimized_app_indicator_height">0dp</dimen>
     <dimen name="taskbar_minimized_app_indicator_width">0dp</dimen>
-    <dimen name="taskbar_minimized_app_indicator_top_margin">0dp</dimen>
 
     <!-- Transient taskbar (placeholders to compile in Launcher3 without Quickstep) -->
     <dimen name="transient_taskbar_padding">0dp</dimen>
diff --git a/res/values/id.xml b/res/values/id.xml
index 28496b5..67692d8 100644
--- a/res/values/id.xml
+++ b/res/values/id.xml
@@ -19,6 +19,7 @@
     <item type="id" name="view_type_widgets_space" />
     <item type="id" name="view_type_widgets_list" />
     <item type="id" name="view_type_widgets_header" />
+    <item type="id" name="view_type_widgets_list_expand" />
 
     <!-- Accessibility actions -->
     <item type="id" name="action_remove" />
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 9d06021..123e2b8 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -157,6 +157,12 @@
     <!-- Accessibility content description for the button that adds a widget to the home screen. The
          placeholder text is the widget name. [CHAR_LIMIT=none] -->
     <string name="widget_add_button_content_description">Add <xliff:g id="widget_name" example="Calendar month view">%1$s</xliff:g> widget</string>
+    <!-- Text on the button that enables users to expand the widgets list to see all widget apps besides the default ones displayed. [CHAR_LIMIT=15] -->
+    <string name="widgets_list_expand_button_label">Show all</string>
+    <!-- Accessibility content description for the button that enables users to expand the widgets list to see all widget apps besides the default ones displayed. [CHAR_LIMIT=none]  -->
+    <string name="widgets_list_expand_button_content_description">Show all widgets</string>
+    <!-- Accessibility announcement to indicate to the users that widgets list is now expanded -->
+    <string name="widgets_list_expanded">Showing all widgets</string>
 
     <!-- Text on an educational tip on widget informing users that they can change widget settings.
          [CHAR_LIMIT=NONE] -->
@@ -311,6 +317,12 @@
     <string name="allow_rotation_title">Allow home screen rotation</string>
     <!-- Text explaining when the home screen will get rotated. [CHAR LIMIT=100] -->
     <string name="allow_rotation_desc">When phone is rotated</string>
+
+    <!-- Title for Landscape Mode setting. [CHAR LIMIT=50] -->
+    <string name="landscape_mode_title">Landscape mode</string>
+    <!--  [CHAR LIMIT=100] -->
+    <string name="landscape_mode_desc">Set phone into landscape mode</string>
+
     <!-- Title for Notification dots setting. Tapping this will link to the system Notifications settings screen where the user can turn off notification dots globally. [CHAR LIMIT=50] -->
     <string name="notification_dots_title">Notification dots</string>
     <!-- Text to indicate that the system notification dots setting is on [CHAR LIMIT=100] -->
@@ -355,8 +367,9 @@
     <!-- Title for an app whose download has been started. -->
     <string name="app_waiting_download_title"><xliff:g id="name" example="Messenger">%1$s</xliff:g> waiting to install</string>
     <!-- Title for an app which is archived. -->
-    <string name="app_archived_title"><xliff:g id="name" example="Messenger">%1$s</xliff:g> is archived. Tap to download and restore.</string>
-
+    <string name="app_archived_title"><xliff:g id="name" example="Messenger">%1$s</xliff:g> is archived.</string>
+    <!-- Accessibility Action for an app which is archived. -->
+    <string name="app_unarchiving_action">download and restore</string>
 
     <!-- Title shown on the alert dialog prompting the user to update the application in market
      in order to re-enable the disabled shortcuts -->
@@ -465,6 +478,8 @@
     <string name="work_profile_edu_accept">Got it</string>
     <!-- Info icon unicode for alpha scroller when work edu card is present -->
     <string name="work_profile_edu_section" translatable="false">\u24D8</string>
+    <!-- Intent for work profiler scheduler -->
+    <string name="work_profile_scheduler_intent" translatable="false"/>
 
     <!--- heading shown when user opens work apps tab while work apps are paused -->
     <string name="work_apps_paused_title">Work apps are paused</string>
@@ -483,6 +498,8 @@
     <string name="work_apps_pause_btn_text">Pause work apps</string>
     <!-- button string shown enable work profile -->
     <string name="work_apps_enable_btn_text">Unpause</string>
+    <!-- Label for the work profile scheduler button in the work profile screen. [CHAR_LIMIT=40] -->
+    <string name="work_scheduler_button_content_description">Work apps schedule</string>
 
     <!-- A hint shown in launcher settings develop options filter box -->
     <string name="developer_options_filter_hint">Filter</string>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 728c523..6d3579b 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -275,6 +275,12 @@
             @color/widget_picker_add_button_background_color_light</item>
         <item name="widgetPickerAddButtonTextColor">
             @color/widget_picker_add_button_text_color_light</item>
+        <item name="widgetPickerExpandButtonBackgroundColor">
+            @color/widget_picker_expand_button_background_color_light
+        </item>
+        <item name="widgetPickerExpandButtonTextColor">
+            @color/widget_picker_expand_button_text_color_light
+        </item>
         <item name="widgetCellTitleColor">
             @color/widget_cell_title_color_light</item>
         <item name="widgetCellSubtitleColor">
@@ -316,6 +322,12 @@
             @color/widget_picker_add_button_background_color_dark</item>
         <item name="widgetPickerAddButtonTextColor">
             @color/widget_picker_add_button_text_color_dark</item>
+        <item name="widgetPickerExpandButtonBackgroundColor">
+            @color/widget_picker_expand_button_background_color_dark
+        </item>
+        <item name="widgetPickerExpandButtonTextColor">
+            @color/widget_picker_expand_button_text_color_dark
+        </item>
         <item name="widgetCellTitleColor">
             @color/widget_cell_title_color_dark</item>
         <item name="widgetCellSubtitleColor">
diff --git a/res/xml/backupscheme.xml b/res/xml/backupscheme.xml
index 0f0dde2..34b80b1 100644
--- a/res/xml/backupscheme.xml
+++ b/res/xml/backupscheme.xml
@@ -2,11 +2,15 @@
 <full-backup-content xmlns:android="http://schemas.android.com/apk/res/android">
 
     <include domain="database" path="launcher.db" />
+    <include domain="database" path="launcher_5_by_8.db" />
     <include domain="database" path="launcher_6_by_5.db" />
+    <include domain="database" path="launcher_5_by_6.db" />
+    <include domain="database" path="launcher_4_by_6.db" />
     <include domain="database" path="launcher_4_by_5.db" />
     <include domain="database" path="launcher_4_by_4.db" />
     <include domain="database" path="launcher_3_by_3.db" />
     <include domain="database" path="launcher_2_by_2.db" />
+    <include domain="database" path="launcher_7_by_3.db" />
     <include domain="sharedpref" path="com.android.launcher3.prefs.xml" />
     <include domain="file" path="downgrade_schema.json" />
 
diff --git a/res/xml/default_workspace_5x8.xml b/res/xml/default_workspace_5x8.xml
new file mode 100644
index 0000000..b078cfd
--- /dev/null
+++ b/res/xml/default_workspace_5x8.xml
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 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.
+-->
+
+<favorites xmlns:launcher="http://schemas.android.com/apk/res-auto/com.android.launcher3">
+
+    <!-- Hotseat (We use the screen as the position of the item in the hotseat) -->
+    <!-- Mail Calendar Gallery Store Internet Camera -->
+    <resolve
+        launcher:container="-101"
+        launcher:screen="0"
+        launcher:x="0"
+        launcher:y="0" >
+        <favorite launcher:uri="#Intent;action=android.intent.action.MAIN;category=android.intent.category.APP_EMAIL;end" />
+        <favorite launcher:uri="mailto:" />
+    </resolve>
+
+    <resolve
+        launcher:container="-101"
+        launcher:screen="1"
+        launcher:x="1"
+        launcher:y="0" >
+        <favorite launcher:uri="#Intent;action=android.intent.action.MAIN;category=android.intent.category.APP_CALENDAR;end" />
+    </resolve>
+
+    <resolve
+        launcher:container="-101"
+        launcher:screen="2"
+        launcher:x="2"
+        launcher:y="0" >
+        <favorite launcher:uri="#Intent;action=android.intent.action.MAIN;category=android.intent.category.APP_GALLERY;end" />
+        <favorite launcher:uri="#Intent;type=images/*;end" />
+    </resolve>
+
+    <resolve
+        launcher:container="-101"
+        launcher:screen="3"
+        launcher:x="3"
+        launcher:y="0" >
+        <favorite launcher:uri="#Intent;action=android.intent.action.MAIN;category=android.intent.category.APP_MARKET;end" />
+        <favorite launcher:uri="market://details?id=com.android.launcher" />
+    </resolve>
+
+    <resolve
+        launcher:container="-101"
+        launcher:screen="4"
+        launcher:x="4"
+        launcher:y="0" >
+        <favorite
+            launcher:uri="#Intent;action=android.intent.action.MAIN;category=android.intent.category.APP_BROWSER;end" />
+        <favorite launcher:uri="http://www.example.com/" />
+    </resolve>
+
+    <!-- Resolve camera intent if GoogleCamera is not available e.g. on emulator -->
+    <resolve
+        launcher:container="-101"
+        launcher:screen="5"
+        launcher:x="5"
+        launcher:y="0" >
+        <favorite launcher:uri="#Intent;action=android.media.action.STILL_IMAGE_CAMERA;end" />
+        <favorite launcher:uri="#Intent;action=android.intent.action.CAMERA_BUTTON;end" />
+    </resolve>
+
+</favorites>
diff --git a/res/xml/paddings_5x8.xml b/res/xml/paddings_5x8.xml
new file mode 100644
index 0000000..afa70c5
--- /dev/null
+++ b/res/xml/paddings_5x8.xml
@@ -0,0 +1,45 @@
+<?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.
+  -->
+
+<device-paddings xmlns:launcher="http://schemas.android.com/apk/res-auto" >
+
+    <device-padding
+        launcher:maxEmptySpace="100dp">
+        <workspaceTopPadding
+            launcher:a="0.31"
+            launcher:b="0"/>
+        <workspaceBottomPadding
+            launcher:a="0.69"
+            launcher:b="0"/>
+        <hotseatBottomPadding
+            launcher:a="0"
+            launcher:b="0"/>
+    </device-padding>
+
+    <device-padding
+        launcher:maxEmptySpace="9999dp">
+        <workspaceTopPadding
+            launcher:a="0.48"
+            launcher:b="0"/>
+        <workspaceBottomPadding
+            launcher:a="0.52"
+            launcher:b="0"/>
+        <hotseatBottomPadding
+            launcher:a="0"
+            launcher:b="0"/>
+    </device-padding>
+</device-paddings>
\ No newline at end of file
diff --git a/src/com/android/launcher3/Alarm.java b/src/com/android/launcher3/Alarm.java
index fb8088c..e516ad0 100644
--- a/src/com/android/launcher3/Alarm.java
+++ b/src/com/android/launcher3/Alarm.java
@@ -20,6 +20,8 @@
 import android.os.Looper;
 import android.os.SystemClock;
 
+import androidx.annotation.VisibleForTesting;
+
 public class Alarm implements Runnable{
     // if we reach this time and the alarm hasn't been cancelled, call the listener
     private long mAlarmTriggerTime;
@@ -96,4 +98,13 @@
     public long getLastSetTimeout() {
         return mLastSetTimeout;
     }
+
+    /** Simulates the alarm firing for tests. */
+    @VisibleForTesting
+    public void finishAlarm() {
+        if (!mAlarmPending) return;
+        mAlarmPending = false;
+        mHandler.removeCallbacks(this);
+        mAlarmListener.onAlarm(this);
+    }
 }
diff --git a/src/com/android/launcher3/BaseActivity.java b/src/com/android/launcher3/BaseActivity.java
index fec94fe..2e75261 100644
--- a/src/com/android/launcher3/BaseActivity.java
+++ b/src/com/android/launcher3/BaseActivity.java
@@ -188,7 +188,7 @@
 
     public SystemUiController getSystemUiController() {
         if (mSystemUiController == null) {
-            mSystemUiController = new SystemUiController(getWindow());
+            mSystemUiController = new SystemUiController(getWindow().getDecorView());
         }
         return mSystemUiController;
     }
diff --git a/src/com/android/launcher3/BaseDraggingActivity.java b/src/com/android/launcher3/BaseDraggingActivity.java
index 177b28c..50e78ac 100644
--- a/src/com/android/launcher3/BaseDraggingActivity.java
+++ b/src/com/android/launcher3/BaseDraggingActivity.java
@@ -80,7 +80,7 @@
         updateTheme();
     }
 
-    private void updateTheme() {
+    protected void updateTheme() {
         if (mThemeRes != Themes.getActivityThemeRes(this)) {
             recreate();
         }
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index 8121e53..ef5c88a 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -20,6 +20,7 @@
 import static android.graphics.fonts.FontStyle.FONT_WEIGHT_NORMAL;
 import static android.text.Layout.Alignment.ALIGN_NORMAL;
 
+import static com.android.launcher3.Flags.enableContrastTiles;
 import static com.android.launcher3.Flags.enableCursorHoverStates;
 import static com.android.launcher3.graphics.PreloadIconDrawable.newPendingIcon;
 import static com.android.launcher3.icons.BitmapInfo.FLAG_NO_BADGE;
@@ -39,6 +40,7 @@
 import android.graphics.Color;
 import android.graphics.Paint;
 import android.graphics.Rect;
+import android.graphics.RectF;
 import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.Drawable;
 import android.icu.text.MessageFormat;
@@ -52,18 +54,19 @@
 import android.util.AttributeSet;
 import android.util.Log;
 import android.util.Property;
-import android.util.Size;
 import android.util.TypedValue;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewDebug;
+import android.view.accessibility.AccessibilityNodeInfo;
 import android.widget.TextView;
 
 import androidx.annotation.DrawableRes;
 import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
 import androidx.annotation.VisibleForTesting;
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
 
 import com.android.launcher3.accessibility.BaseAccessibilityDelegate;
 import com.android.launcher3.dot.DotInfo;
@@ -187,20 +190,22 @@
     @ViewDebug.ExportedProperty(category = "launcher")
     private DotInfo mDotInfo;
     private DotRenderer mDotRenderer;
-    private Locale mCurrentLocale;
+    private String mCurrentLanguage;
     @ViewDebug.ExportedProperty(category = "launcher", deepExport = true)
     protected DotRenderer.DrawParams mDotParams;
     private Animator mDotScaleAnim;
     private boolean mForceHideDot;
 
     // These fields, related to showing running apps, are only used for Taskbar.
-    private final Size mRunningAppIndicatorSize;
+    private final int mRunningAppIndicatorWidth;
+    private final int mMinimizedAppIndicatorWidth;
+    private final int mRunningAppIndicatorHeight;
     private final int mRunningAppIndicatorTopMargin;
-    private final Size mMinimizedAppIndicatorSize;
-    private final int mMinimizedAppIndicatorTopMargin;
     private final Paint mRunningAppIndicatorPaint;
     private final Rect mRunningAppIconBounds = new Rect();
     private RunningAppState mRunningAppState;
+    private final int mRunningAppIndicatorColor;
+    private final int mMinimizedAppIndicatorColor;
 
     /**
      * Various options for the running state of an app.
@@ -220,7 +225,7 @@
 
     private CancellableTask mIconLoadRequest;
 
-    private boolean mEnableIconUpdateAnimation = false;
+    private boolean mHighResUpdateInProgress = false;
 
     public BubbleTextView(Context context) {
         this(context, null, 0);
@@ -277,28 +282,27 @@
                 defaultIconSize);
         a.recycle();
 
-        mRunningAppIndicatorSize = new Size(
-                getResources().getDimensionPixelSize(R.dimen.taskbar_running_app_indicator_width),
-                getResources().getDimensionPixelSize(R.dimen.taskbar_running_app_indicator_height));
-        mMinimizedAppIndicatorSize = new Size(
-                getResources().getDimensionPixelSize(R.dimen.taskbar_minimized_app_indicator_width),
-                getResources().getDimensionPixelSize(
-                        R.dimen.taskbar_minimized_app_indicator_height));
+        mRunningAppIndicatorWidth =
+                getResources().getDimensionPixelSize(R.dimen.taskbar_running_app_indicator_width);
+        mMinimizedAppIndicatorWidth =
+                getResources().getDimensionPixelSize(R.dimen.taskbar_minimized_app_indicator_width);
+        mRunningAppIndicatorHeight =
+                getResources().getDimensionPixelSize(R.dimen.taskbar_running_app_indicator_height);
         mRunningAppIndicatorTopMargin =
                 getResources().getDimensionPixelSize(
                         R.dimen.taskbar_running_app_indicator_top_margin);
-        mMinimizedAppIndicatorTopMargin =
-                getResources().getDimensionPixelSize(
-                        R.dimen.taskbar_minimized_app_indicator_top_margin);
+
         mRunningAppIndicatorPaint = new Paint();
-        mRunningAppIndicatorPaint.setColor(getResources().getColor(
-                R.color.taskbar_running_app_indicator_color, context.getTheme()));
+        mRunningAppIndicatorColor = getResources().getColor(
+                R.color.taskbar_running_app_indicator_color, context.getTheme());
+        mMinimizedAppIndicatorColor = getResources().getColor(
+                R.color.taskbar_minimized_app_indicator_color, context.getTheme());
 
         mLongPressHelper = new CheckLongPressHelper(this);
 
         mDotParams = new DotRenderer.DrawParams();
 
-        mCurrentLocale = context.getResources().getConfiguration().locale;
+        mCurrentLanguage = context.getResources().getConfiguration().locale.getLanguage();
         setEllipsize(TruncateAt.END);
         setAccessibilityDelegate(mActivity.getAccessibilityDelegate());
         setTextAlpha(1f);
@@ -494,7 +498,7 @@
     }
 
     protected boolean isCurrentLanguageEnglish() {
-        return mCurrentLocale.equals(Locale.US);
+        return mCurrentLanguage.equals(Locale.ENGLISH.getLanguage());
     }
 
     @UiThread
@@ -519,6 +523,16 @@
         }
     }
 
+    @Override
+    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+        super.onInitializeAccessibilityNodeInfo(info);
+        if (getTag() instanceof ItemInfoWithIcon infoWithIcon && infoWithIcon.isInactiveArchive()) {
+            info.addAction(new AccessibilityNodeInfo.AccessibilityAction(
+                    AccessibilityNodeInfoCompat.ACTION_CLICK,
+                    getContext().getString(R.string.app_unarchiving_action)));
+        }
+    }
+
     /** This is used for testing to forcefully set the display. */
     @VisibleForTesting
     public void setDisplay(int display) {
@@ -710,22 +724,55 @@
         }
     }
 
+    /** Draws a background behind the App Title label when required. **/
+    public void drawAppContrastTile(Canvas canvas) {
+        RectF appTitleBounds;
+        Paint.FontMetrics fm = getPaint().getFontMetrics();
+        Rect tmpRect = new Rect();
+        getDrawingRect(tmpRect);
+
+        if (mIcon == null) {
+            appTitleBounds = new RectF(0, 0, tmpRect.right,
+                    (int) Math.ceil(fm.bottom - fm.top));
+        } else {
+            Rect iconBounds = new Rect();
+            getIconBounds(iconBounds);
+            int textStart = iconBounds.bottom + getCompoundDrawablePadding();
+            appTitleBounds = new RectF(tmpRect.left, textStart, tmpRect.right,
+                    textStart + (int) Math.ceil(fm.bottom - fm.top));
+        }
+
+        canvas.drawRoundRect(appTitleBounds, appTitleBounds.height() / 2,
+                appTitleBounds.height() / 2,
+                PillColorProvider.getInstance(getContext()).getAppTitlePillPaint());
+    }
+
     /** Draws a line under the app icon if this is representing a running app in Desktop Mode. */
     protected void drawRunningAppIndicatorIfNecessary(Canvas canvas) {
         if (mRunningAppState == RunningAppState.NOT_RUNNING || mDisplay != DISPLAY_TASKBAR) {
             return;
         }
         getIconBounds(mRunningAppIconBounds);
-        // TODO(b/333872717): update color, shape, and size of indicator
-        boolean isMinimized = mRunningAppState == RunningAppState.MINIMIZED;
-        int indicatorTop =
-                mRunningAppIconBounds.bottom + (isMinimized ? mMinimizedAppIndicatorTopMargin
-                        : mRunningAppIndicatorTopMargin);
-        final Size indicatorSize =
-                isMinimized ? mMinimizedAppIndicatorSize : mRunningAppIndicatorSize;
-        canvas.drawRect(mRunningAppIconBounds.centerX() - indicatorSize.getWidth() / 2,
-                indicatorTop, mRunningAppIconBounds.centerX() + indicatorSize.getWidth() / 2,
-                indicatorTop + indicatorSize.getHeight(), mRunningAppIndicatorPaint);
+        Utilities.scaleRectAboutCenter(
+                mRunningAppIconBounds,
+                IconShape.INSTANCE.get(getContext()).getNormalizationScale());
+
+        final boolean isMinimized = mRunningAppState == RunningAppState.MINIMIZED;
+        final int indicatorTop = mRunningAppIconBounds.bottom + mRunningAppIndicatorTopMargin;
+        final int indicatorWidth =
+                isMinimized ? mMinimizedAppIndicatorWidth : mRunningAppIndicatorWidth;
+        final float cornerRadius = mRunningAppIndicatorHeight / 2f;
+        mRunningAppIndicatorPaint.setColor(
+                isMinimized ? mMinimizedAppIndicatorColor : mRunningAppIndicatorColor);
+
+        canvas.drawRoundRect(
+                mRunningAppIconBounds.centerX() - indicatorWidth / 2f,
+                indicatorTop,
+                mRunningAppIconBounds.centerX() + indicatorWidth / 2f,
+                indicatorTop + mRunningAppIndicatorHeight,
+                cornerRadius,
+                cornerRadius,
+                mRunningAppIndicatorPaint);
     }
 
     @Override
@@ -887,7 +934,9 @@
 
     @Override
     public void setTextColor(ColorStateList colors) {
-        mTextColor = colors.getDefaultColor();
+        mTextColor = shouldDrawAppContrastTile() ? PillColorProvider.getInstance(
+                getContext()).getAppTitleTextPaint().getColor()
+                : colors.getDefaultColor();
         mTextColorStateList = colors;
         if (Float.compare(mTextAlpha, 1) == 0) {
             super.setTextColor(colors);
@@ -904,6 +953,15 @@
                 && info.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION);
     }
 
+    /**
+     * Whether or not an App title contrast tile should be drawn for this element.
+     **/
+    public boolean shouldDrawAppContrastTile() {
+        return mDisplay == DISPLAY_WORKSPACE && shouldTextBeVisible()
+                && PillColorProvider.getInstance(getContext()).isMatchaEnabled()
+                && enableContrastTiles();
+    }
+
     public void setTextVisibility(boolean visible) {
         setTextAlpha(visible ? 1 : 0);
     }
@@ -1130,6 +1188,9 @@
                 if (itemInfo.isDisabled()) {
                     setContentDescription(getContext().getString(R.string.disabled_app_label,
                             itemInfo.contentDescription));
+                } else if (itemInfo instanceof WorkspaceItemInfo wai && wai.isArchived()) {
+                    setContentDescription(
+                            getContext().getString(R.string.app_archived_title, itemInfo.title));
                 } else if (hasDot()) {
                     int count = mDotInfo.getNotificationCount();
                     setContentDescription(
@@ -1142,8 +1203,16 @@
     }
 
     private void setDownloadStateContentDescription(ItemInfoWithIcon info, int progressLevel) {
-        if ((info.runtimeStatusFlags & ItemInfoWithIcon.FLAG_ARCHIVED) != 0 && progressLevel == 0) {
-            setContentDescription(getContext().getString(R.string.app_archived_title, info.title));
+        if ((info.runtimeStatusFlags & ItemInfoWithIcon.FLAG_ARCHIVED) != 0
+                && progressLevel == 0) {
+            if (mIcon instanceof PreloadIconDrawable) {
+                // Tell user that download is pending and not to tap to download again.
+                setContentDescription(getContext().getString(
+                        R.string.app_waiting_download_title, info.title));
+            } else {
+                setContentDescription(getContext().getString(
+                        R.string.app_archived_title, info.title));
+            }
         } else if ((info.runtimeStatusFlags & ItemInfoWithIcon.FLAG_SHOW_DOWNLOAD_PROGRESS_MASK)
                 != 0) {
             String percentageString = NumberFormat.getPercentInstance()
@@ -1195,10 +1264,6 @@
         }
     }
 
-    protected boolean iconUpdateAnimationEnabled() {
-        return mEnableIconUpdateAnimation;
-    }
-
     protected void applyCompoundDrawables(Drawable icon) {
         if (icon == null) {
             // Icon can be null when we use the BubbleTextView for text only.
@@ -1216,7 +1281,7 @@
         // If the current icon is a placeholder color, animate its update.
         if (mIcon != null
                 && mIcon instanceof PlaceHolderIconDrawable
-                && iconUpdateAnimationEnabled()) {
+                && mHighResUpdateInProgress) {
             ((PlaceHolderIconDrawable) mIcon).animateIconUpdate(icon);
         }
 
@@ -1238,7 +1303,7 @@
         if (getTag() == info) {
             mIconLoadRequest = null;
             mDisableRelayout = true;
-            mEnableIconUpdateAnimation = true;
+            mHighResUpdateInProgress = true;
 
             // Optimization: Starting in N, pre-uploads the bitmap to RenderThread.
             info.bitmap.icon.prepareToDraw();
@@ -1253,7 +1318,7 @@
             }
 
             mDisableRelayout = false;
-            mEnableIconUpdateAnimation = false;
+            mHighResUpdateInProgress = false;
         }
     }
 
@@ -1265,7 +1330,7 @@
             mIconLoadRequest.cancel();
             mIconLoadRequest = null;
         }
-        if (getTag() instanceof ItemInfoWithIcon) {
+        if (getTag() instanceof ItemInfoWithIcon && !mHighResUpdateInProgress) {
             ItemInfoWithIcon info = (ItemInfoWithIcon) getTag();
             if (info.usingLowResIcon()) {
                 mIconLoadRequest = LauncherAppState.getInstance(getContext()).getIconCache()
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index 483f5f8..78535a1 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -24,6 +24,7 @@
 import static com.android.launcher3.InvariantDeviceProfile.INDEX_TWO_PANEL_LANDSCAPE;
 import static com.android.launcher3.InvariantDeviceProfile.INDEX_TWO_PANEL_PORTRAIT;
 import static com.android.launcher3.Utilities.dpiFromPx;
+import static com.android.launcher3.Utilities.isEnglishLanguage;
 import static com.android.launcher3.Utilities.pxFromSp;
 import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.ICON_OVERLAP_FACTOR;
 import static com.android.launcher3.icons.GraphicsUtils.getShapePath;
@@ -31,6 +32,8 @@
 import static com.android.launcher3.testing.shared.ResourceUtils.INVALID_RESOURCE_HANDLE;
 import static com.android.launcher3.testing.shared.ResourceUtils.pxFromDp;
 import static com.android.launcher3.testing.shared.ResourceUtils.roundPxValueFromFloat;
+import static com.android.wm.shell.Flags.enableBubbleBar;
+import static com.android.wm.shell.Flags.enableBubbleBarInPersistentTaskBar;
 import static com.android.wm.shell.Flags.enableTinyTaskbar;
 
 import android.annotation.SuppressLint;
@@ -64,6 +67,7 @@
 import com.android.launcher3.responsive.ResponsiveSpec.DimensionType;
 import com.android.launcher3.responsive.ResponsiveSpecsProvider;
 import com.android.launcher3.util.CellContentDimensions;
+import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.DisplayController.Info;
 import com.android.launcher3.util.IconSizeSteps;
 import com.android.launcher3.util.ResourceHelper;
@@ -219,6 +223,8 @@
     public int hotseatBarBottomSpacePx;
     public int hotseatBarEndOffset;
     public int hotseatQsbSpace;
+    public int inlineNavButtonsEndSpacingPx;
+    public int navButtonsLayoutWidthPx;
     public int springLoadedHotseatBarTopMarginPx;
     // These 2 values are only used for isVerticalBar
     // Padding between edge of screen and hotseat
@@ -233,7 +239,6 @@
     private final int mMinHotseatIconSpacePx;
     private final int mMinHotseatQsbWidthPx;
     private final int mMaxHotseatIconSpacePx;
-    public final int inlineNavButtonsEndSpacingPx;
     // Space required for the bubble bar between the hotseat and the edge of the screen. If there's
     // not enough space, the hotseat will adjust itself for the bubble bar.
     private final int mBubbleBarSpaceThresholdPx;
@@ -612,7 +617,7 @@
                 || inv.inlineQsb[INDEX_TWO_PANEL_LANDSCAPE]
                 : inv.inlineQsb[INDEX_DEFAULT] || inv.inlineQsb[INDEX_LANDSCAPE])
                 && hotseatQsbHeight > 0;
-        isQsbInline = mIsScalableGrid && inv.inlineQsb[mTypeIndex] && canQsbInline;
+        isQsbInline = isQsbInline(inv);
 
         areNavButtonsInline = isTaskbarPresent && !isGestureMode;
         numShownHotseatIcons =
@@ -692,17 +697,12 @@
         if (areNavButtonsInline && !isPhone) {
             inlineNavButtonsEndSpacingPx =
                     res.getDimensionPixelSize(inv.inlineNavButtonsEndSpacing);
-            /*
-             * 3 nav buttons +
-             * Spacing between nav buttons +
-             * Space at the end for contextual buttons
-             */
-            hotseatBarEndOffset = 3 * res.getDimensionPixelSize(R.dimen.taskbar_nav_buttons_size)
-                    + 2 * res.getDimensionPixelSize(R.dimen.taskbar_button_space_inbetween)
-                    + inlineNavButtonsEndSpacingPx;
-        } else {
-            inlineNavButtonsEndSpacingPx = 0;
-            hotseatBarEndOffset = 0;
+            /* 3 nav buttons + Spacing between nav buttons */
+            navButtonsLayoutWidthPx = 3 * res.getDimensionPixelSize(
+                    R.dimen.taskbar_nav_buttons_size)
+                    + 2 * res.getDimensionPixelSize(R.dimen.taskbar_button_space_inbetween);
+            /* nav buttons layout width + Space at the end for contextual buttons */
+            hotseatBarEndOffset = navButtonsLayoutWidthPx + inlineNavButtonsEndSpacingPx;
         }
 
         mBubbleBarSpaceThresholdPx =
@@ -827,7 +827,7 @@
             hotseatBorderSpace = cellLayoutBorderSpacePx.y;
         }
 
-        if (isTablet) {
+        if (shouldShowAllAppsOnSheet()) {
             allAppsPadding.top = mInsets.top;
             allAppsShiftRange = heightPx;
         } else {
@@ -850,6 +850,24 @@
         mDotRendererAllApps = createDotRenderer(context, allAppsIconSizePx, dotRendererCache);
     }
 
+    /**
+     * Takes care of the logic that determines if we show a the QSB inline or not.
+     */
+    private boolean isQsbInline(InvariantDeviceProfile inv) {
+        // For foldable (two panel), we inline the qsb if we have the screen open and we are in
+        // either Landscape or Portrait. This cal also be disabled in the device_profile.xml
+        boolean twoPanelCanInline = inv.inlineQsb[INDEX_TWO_PANEL_PORTRAIT]
+                || inv.inlineQsb[INDEX_TWO_PANEL_LANDSCAPE];
+
+        // In tablets we inline in both orientations but only if we have enough space in the QSB
+        boolean tabletInlineQsb = inv.inlineQsb[INDEX_DEFAULT] || inv.inlineQsb[INDEX_LANDSCAPE];
+        boolean canQsbInline = isTwoPanels ? twoPanelCanInline : tabletInlineQsb;
+        canQsbInline = canQsbInline && hotseatQsbHeight > 0;
+
+        return (mIsScalableGrid && inv.inlineQsb[mTypeIndex] && canQsbInline)
+                || inv.isFixedLandscapeMode;
+    }
+
     private static DotRenderer createDotRenderer(
             @NonNull Context context, int size, @NonNull SparseArray<DotRenderer> cache) {
         DotRenderer renderer = cache.get(size);
@@ -1345,8 +1363,14 @@
         }
         if ((Flags.enableTwolineToggle()
                 && LauncherPrefs.ENABLE_TWOLINE_ALLAPPS_TOGGLE.get(context))) {
-            // Add extra textHeight to the existing allAppsCellHeight.
-            allAppsCellHeightPx += Utilities.calculateTextHeight(allAppsIconTextSizePx);
+            if (!isEnglishLanguage(context)) {
+                // Set toggle preference value to false if not english here as it's possible the
+                // preference is stale after language change.
+                LauncherPrefs.get(context).put(LauncherPrefs.ENABLE_TWOLINE_ALLAPPS_TOGGLE, false);
+            } else {
+                // Add extra textHeight to the existing allAppsCellHeight.
+                allAppsCellHeightPx += Utilities.calculateTextHeight(allAppsIconTextSizePx);
+            }
         }
 
         updateHotseatSizes(iconSizePx);
@@ -1510,6 +1534,11 @@
         }
     }
 
+    /** Whether All Apps should be presented on a bottom sheet. */
+    public boolean shouldShowAllAppsOnSheet() {
+        return isTablet || Flags.allAppsSheetForHandheld();
+    }
+
     private void setupAllAppsStyle(Context context) {
         TypedArray allAppsStyle = context.obtainStyledAttributes(
                 inv.allAppsStyle != INVALID_RESOURCE_HANDLE ? inv.allAppsStyle
@@ -1830,19 +1859,14 @@
      * Returns the new border space that should be used between hotseat icons after adjusting it to
      * the bubble bar.
      *
+     * <p>Does not check for visible bubbles persistence, so caller should call
+     * {@link #shouldAdjustHotseatForBubbleBar} first.
+     *
      * <p>If there's no adjustment needed, this method returns {@code 0}.
+     * @see #shouldAdjustHotseatForBubbleBar(Context, boolean)
      */
     public float getHotseatAdjustedBorderSpaceForBubbleBar(Context context) {
-        // only need to adjust when QSB is on top of the hotseat.
-        if (isQsbInline) {
-            return 0;
-        }
-
-        // no need to adjust if there's enough space for the bubble bar to the right of the hotseat.
-        if (getHotseatLayoutPadding(context).right > mBubbleBarSpaceThresholdPx) {
-            return 0;
-        }
-
+        if (!shouldAdjustHotseatForBubbleBar(context)) return 0;
         // The adjustment is shrinking the hotseat's width by 1 icon on either side.
         int iconsWidth =
                 iconSizePx * numShownHotseatIcons + hotseatBorderSpace * (numShownHotseatIcons - 1);
@@ -1852,6 +1876,33 @@
     }
 
     /**
+     * Returns the hotseat icon translation X for the cellX index.
+     *
+     * <p>Does not check for visible bubbles persistence, so caller should call
+     * {@link #shouldAdjustHotseatForBubbleBar} first.
+     *
+     * <p>If there's no adjustment needed, this method returns {@code 0}.
+     * @see #shouldAdjustHotseatForBubbleBar(Context, boolean)
+     */
+    public float getHotseatAdjustedTranslation(Context context, int cellX) {
+        if (!shouldAdjustHotseatForBubbleBar(context)) return 0;
+        float borderSpace = getHotseatAdjustedBorderSpaceForBubbleBar(context);
+        float borderSpaceDelta = borderSpace - hotseatBorderSpace;
+        return iconSizePx + cellX * borderSpaceDelta;
+    }
+
+    /** Returns whether hotseat should be adjusted for the bubble bar. */
+    public boolean shouldAdjustHotseatForBubbleBar(Context context, boolean hasBubbles) {
+        return hasBubbles && shouldAdjustHotseatForBubbleBar(context);
+    }
+
+    private boolean shouldAdjustHotseatForBubbleBar(Context context) {
+        // only need to adjust if bubble bar is enabled, when QSB is on top of the hotseat and
+        // there's not enough space for the bubble bar to the right of the hotseat.
+        return !isQsbInline && getHotseatLayoutPadding(context).right <= mBubbleBarSpaceThresholdPx;
+    }
+
+    /**
      * Returns the padding for hotseat view
      */
     public Rect getHotseatLayoutPadding(Context context) {
@@ -1870,7 +1921,7 @@
                 hotseatBarPadding.set(mHotseatBarWorkspaceSpacePx, paddingTop,
                         mInsets.right + mHotseatBarEdgePaddingPx, paddingBottom);
             }
-        } else if (isTaskbarPresent) {
+        } else if (isTaskbarPresent || inv.isFixedLandscapeMode) {
             // Center the QSB vertically with hotseat
             int hotseatBarBottomPadding = getHotseatBarBottomPadding();
             int hotseatBarTopPadding =
@@ -1889,6 +1940,13 @@
             }
             startSpacing += getAdditionalQsbSpace();
 
+            if (inv.isFixedLandscapeMode) {
+                endSpacing += workspacePadding.right + cellLayoutPaddingPx.right
+                        + mInsets.right;
+                startSpacing += workspacePadding.left + cellLayoutPaddingPx.left
+                        + mInsets.left;
+            }
+
             hotseatBarPadding.top = hotseatBarTopPadding;
             hotseatBarPadding.bottom = hotseatBarBottomPadding;
             boolean isRtl = Utilities.isRtl(context.getResources());
@@ -1998,6 +2056,18 @@
     }
 
     /**
+     * Returns the number of pixels the hotseat icons vertical center is translated from the bottom
+     * of the screen.
+     */
+    public int getHotseatVerticalCenter() {
+        return hotseatBarSizePx
+                - (isQsbInline ? 0 : hotseatQsbVisualHeight)
+                - hotseatQsbSpace
+                - (hotseatCellHeightPx / 2)
+                + ((hotseatCellHeightPx - iconSizePx) / 2);
+    }
+
+    /**
      * Returns the number of pixels the taskbar is translated from the bottom of the screen.
      */
     public int getTaskbarOffsetY() {
@@ -2214,6 +2284,10 @@
                 mHotseatBarEdgePaddingPx));
         writer.println(prefix + pxToDpStr("mHotseatBarWorkspaceSpacePx",
                 mHotseatBarWorkspaceSpacePx));
+        writer.println(prefix
+                + pxToDpStr("inlineNavButtonsEndSpacingPx", inlineNavButtonsEndSpacingPx));
+        writer.println(prefix
+                + pxToDpStr("navButtonsLayoutWidthPx", navButtonsLayoutWidthPx));
         writer.println(prefix + pxToDpStr("hotseatBarEndOffset", hotseatBarEndOffset));
         writer.println(prefix + pxToDpStr("hotseatQsbSpace", hotseatQsbSpace));
         writer.println(prefix + pxToDpStr("hotseatQsbHeight", hotseatQsbHeight));
@@ -2328,6 +2402,29 @@
     }
 
     /**
+     * Returns whether Taskbar and Hotseat should adjust horizontally on bubble bar location update.
+     */
+    public boolean shouldAdjustHotseatOnNavBarLocationUpdate(Context context) {
+        return enableBubbleBar()
+                && enableBubbleBarInPersistentTaskBar()
+                && !DisplayController.getNavigationMode(context).hasGestures;
+    }
+
+    /** Returns hotseat translation X for the bubble bar position. */
+    public int getHotseatTranslationXForNavBar(Context context, boolean isBubblesOnLeft) {
+        if (shouldAdjustHotseatOnNavBarLocationUpdate(context)) {
+            boolean isRtl = Utilities.isRtl(context.getResources());
+            if (isBubblesOnLeft) {
+                return isRtl ? -navButtonsLayoutWidthPx : 0;
+            } else {
+                return isRtl ? 0 : navButtonsLayoutWidthPx;
+            }
+        } else {
+            return 0;
+        }
+    }
+
+    /**
      * Callback when a component changes the DeviceProfile associated with it, as a result of
      * configuration change
      */
@@ -2445,7 +2542,8 @@
                 throw new IllegalArgumentException("Window bounds not set");
             }
             if (mTransposeLayoutWithOrientation == null) {
-                mTransposeLayoutWithOrientation = !mInfo.isTablet(mWindowBounds);
+                mTransposeLayoutWithOrientation =
+                        !(mInfo.isTablet(mWindowBounds) || mInv.isFixedLandscapeMode);
             }
             if (mIsGestureMode == null) {
                 mIsGestureMode = mInfo.getNavigationMode().hasGestures;
diff --git a/src/com/android/launcher3/Hotseat.java b/src/com/android/launcher3/Hotseat.java
index 024dde4..6468f74 100644
--- a/src/com/android/launcher3/Hotseat.java
+++ b/src/com/android/launcher3/Hotseat.java
@@ -34,8 +34,10 @@
 import android.widget.FrameLayout;
 
 import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
 
 import com.android.launcher3.util.HorizontalInsettableView;
+import com.android.launcher3.util.MultiPropertyFactory;
 import com.android.launcher3.util.MultiPropertyFactory.MultiProperty;
 import com.android.launcher3.util.MultiTranslateDelegate;
 import com.android.launcher3.util.MultiValueAlpha;
@@ -61,6 +63,14 @@
     public @interface HotseatQsbAlphaId {
     }
 
+    public static final int ICONS_TRANSLATION_X_NAV_BAR_ALIGNMENT = 0;
+    public static final int ICONS_TRANSLATION_X_CHANNELS_COUNT = 1;
+
+    @Retention(RetentionPolicy.RUNTIME)
+    @IntDef({ICONS_TRANSLATION_X_NAV_BAR_ALIGNMENT})
+    public @interface IconsTranslationX {
+    }
+
     // Ratio of empty space, qsb should take up to appear visually centered.
     public static final float QSB_CENTER_FACTOR = .325f;
     private static final int BUBBLE_BAR_ADJUSTMENT_ANIMATION_DURATION_MS = 250;
@@ -72,6 +82,10 @@
     private final MultiValueAlpha mIconsAlphaChannels;
     private final MultiValueAlpha mQsbAlphaChannels;
 
+    private @Nullable MultiProperty mQsbTranslationX;
+
+    private final MultiPropertyFactory mIconsTranslationXFactory;
+
     private final View mQsb;
 
     public Hotseat(Context context) {
@@ -88,9 +102,26 @@
         addView(mQsb);
         mIconsAlphaChannels = new MultiValueAlpha(getShortcutsAndWidgets(),
                 ALPHA_CHANNEL_CHANNELS_COUNT);
+        if (mQsb instanceof Reorderable qsbReorderable) {
+            mQsbTranslationX = qsbReorderable.getTranslateDelegate()
+                    .getTranslationX(MultiTranslateDelegate.INDEX_NAV_BAR_ANIM);
+        }
+        mIconsTranslationXFactory = new MultiPropertyFactory<>(getShortcutsAndWidgets(),
+                VIEW_TRANSLATE_X, ICONS_TRANSLATION_X_CHANNELS_COUNT, Float::sum);
         mQsbAlphaChannels = new MultiValueAlpha(mQsb, ALPHA_CHANNEL_CHANNELS_COUNT);
     }
 
+    /** Provides translation X for hotseat icons for the channel. */
+    public MultiProperty getIconsTranslationX(@IconsTranslationX int channelId) {
+        return mIconsTranslationXFactory.get(channelId);
+    }
+
+    /** Provides translation X for hotseat Qsb. */
+    @Nullable
+    public MultiProperty getQsbTranslationX() {
+        return mQsbTranslationX;
+    }
+
     /**
      * Returns orientation specific cell X given invariant order in the hotseat
      */
@@ -118,12 +149,9 @@
         DeviceProfile dp = mActivity.getDeviceProfile();
 
         if (bubbleBarEnabled) {
-            float adjustedBorderSpace = dp.getHotseatAdjustedBorderSpaceForBubbleBar(getContext());
-            if (hasBubbles && Float.compare(adjustedBorderSpace, 0f) != 0) {
-                getShortcutsAndWidgets().setTranslationProvider(cellX -> {
-                    float borderSpaceDelta = adjustedBorderSpace - dp.hotseatBorderSpace;
-                    return dp.iconSizePx + cellX * borderSpaceDelta;
-                });
+            if (dp.shouldAdjustHotseatForBubbleBar(getContext(), hasBubbles)) {
+                getShortcutsAndWidgets().setTranslationProvider(
+                        cellX -> dp.getHotseatAdjustedTranslation(getContext(), cellX));
                 if (mQsb instanceof HorizontalInsettableView) {
                     HorizontalInsettableView insettableQsb = (HorizontalInsettableView) mQsb;
                     final float insetFraction = (float) dp.iconSizePx / dp.hotseatQsbWidth;
@@ -159,24 +187,22 @@
     public void adjustForBubbleBar(boolean isBubbleBarVisible) {
         DeviceProfile dp = mActivity.getDeviceProfile();
         float adjustedBorderSpace = dp.getHotseatAdjustedBorderSpaceForBubbleBar(getContext());
-        if (Float.compare(adjustedBorderSpace, 0f) == 0) {
-            return;
-        }
+        boolean adjustmentRequired = Float.compare(adjustedBorderSpace, 0f) != 0;
 
         ShortcutAndWidgetContainer icons = getShortcutsAndWidgets();
-        AnimatorSet animatorSet = new AnimatorSet();
-        float borderSpaceDelta = adjustedBorderSpace - dp.hotseatBorderSpace;
-
         // update the translation provider for future layout passes of hotseat icons.
-        if (isBubbleBarVisible) {
-            icons.setTranslationProvider(cellX -> dp.iconSizePx + cellX * borderSpaceDelta);
+        if (adjustmentRequired && isBubbleBarVisible) {
+            icons.setTranslationProvider(
+                    cellX -> dp.getHotseatAdjustedTranslation(getContext(), cellX));
         } else {
             icons.setTranslationProvider(null);
         }
+        if (!adjustmentRequired) return;
 
+        AnimatorSet animatorSet = new AnimatorSet();
         for (int i = 0; i < icons.getChildCount(); i++) {
             View child = icons.getChildAt(i);
-            float tx = isBubbleBarVisible ? dp.iconSizePx + i * borderSpaceDelta : 0;
+            float tx = isBubbleBarVisible ? dp.getHotseatAdjustedTranslation(getContext(), i) : 0;
             if (child instanceof Reorderable) {
                 MultiTranslateDelegate mtd = ((Reorderable) child).getTranslateDelegate();
                 animatorSet.play(
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index 54aea38..e18862a 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -16,6 +16,7 @@
 
 package com.android.launcher3;
 
+import static com.android.launcher3.LauncherPrefs.FIXED_LANDSCAPE_MODE;
 import static com.android.launcher3.LauncherPrefs.GRID_NAME;
 import static com.android.launcher3.Utilities.dpiFromPx;
 import static com.android.launcher3.testing.shared.ResourceUtils.INVALID_RESOURCE_HANDLE;
@@ -34,6 +35,7 @@
 import android.graphics.Point;
 import android.graphics.PointF;
 import android.graphics.Rect;
+import android.os.Trace;
 import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.util.DisplayMetrics;
@@ -51,15 +53,14 @@
 
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.icons.DotRenderer;
-import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.model.DeviceGridState;
 import com.android.launcher3.provider.RestoreDbTask;
 import com.android.launcher3.testing.shared.ResourceUtils;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.DisplayController.Info;
-import com.android.launcher3.util.LockedUserState;
 import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.util.Partner;
+import com.android.launcher3.util.ResourceHelper;
 import com.android.launcher3.util.SafeCloseable;
 import com.android.launcher3.util.WindowBounds;
 import com.android.launcher3.util.window.WindowManagerProxy;
@@ -86,7 +87,8 @@
 
     @Retention(RetentionPolicy.SOURCE)
     @IntDef({TYPE_PHONE, TYPE_MULTI_DISPLAY, TYPE_TABLET})
-    public @interface DeviceType {}
+    public @interface DeviceType {
+    }
 
     public static final int TYPE_PHONE = 0;
     public static final int TYPE_MULTI_DISPLAY = 1;
@@ -132,6 +134,7 @@
     public int iconBitmapSize;
     public int fillResIconDpi;
     public @DeviceType int deviceType;
+    public Info displayInfo;
 
     public PointF[] minCellSize;
 
@@ -185,6 +188,8 @@
     @XmlRes
     public int workspaceSpecsId = INVALID_RESOURCE_HANDLE;
     @XmlRes
+    public int rowCountSpecsId = INVALID_RESOURCE_HANDLE;;
+    @XmlRes
     public int workspaceSpecsTwoPanelId = INVALID_RESOURCE_HANDLE;
     @XmlRes
     public int allAppsSpecsId = INVALID_RESOURCE_HANDLE;
@@ -207,6 +212,13 @@
     @XmlRes
     public int allAppsCellSpecsTwoPanelId = INVALID_RESOURCE_HANDLE;
 
+
+    /**
+     * Fixed landscape mode is the landscape on the phones.
+     */
+    public boolean isFixedLandscapeMode = false;
+    private LauncherPrefChangeListener mLandscapeModePreferenceListener;
+
     public String dbFile;
     public int defaultLayoutId;
     public int demoModeLayoutId;
@@ -222,7 +234,8 @@
     private final ArrayList<OnIDPChangeListener> mChangeListeners = new ArrayList<>();
 
     @VisibleForTesting
-    public InvariantDeviceProfile() { }
+    public InvariantDeviceProfile() {
+    }
 
     @TargetApi(23)
     private InvariantDeviceProfile(Context context) {
@@ -231,8 +244,6 @@
         if (!newGridName.equals(gridName)) {
             LauncherPrefs.get(context).put(GRID_NAME, newGridName);
         }
-        LockedUserState.get(context).runOnUserUnlocked(() ->
-            new DeviceGridState(this).writeToPrefs(context));
 
         DisplayController.INSTANCE.get(context).setPriorityListener(
                 (displayContext, info, flags) -> {
@@ -242,6 +253,18 @@
                         onConfigChanged(displayContext);
                     }
                 });
+        if (Flags.oneGridSpecs()) {
+            mLandscapeModePreferenceListener = (String s) -> {
+                boolean newFixedLandscapeValue = FIXED_LANDSCAPE_MODE.get(context);
+                if (isFixedLandscapeMode != newFixedLandscapeValue) {
+                    setFixedLandscape(context, newFixedLandscapeValue);
+                }
+            };
+            LauncherPrefs.INSTANCE.get(context).addListener(
+                    mLandscapeModePreferenceListener,
+                    FIXED_LANDSCAPE_MODE
+            );
+        }
     }
 
     /**
@@ -267,8 +290,13 @@
         @DeviceType int defaultDeviceType = defaultInfo.getDeviceType();
         DisplayOption defaultDisplayOption = invDistWeightedInterpolate(
                 defaultInfo,
-                getPredefinedDeviceProfiles(context, gridName, defaultDeviceType,
-                        /*allowDisabledGrid=*/false),
+                getPredefinedDeviceProfiles(
+                        context,
+                        gridName,
+                        defaultInfo,
+                        /*allowDisabledGrid=*/false,
+                        isFixedLandscapeMode
+                ),
                 defaultDeviceType);
 
         Context displayContext = context.createDisplayContext(display);
@@ -276,8 +304,13 @@
         @DeviceType int deviceType = myInfo.getDeviceType();
         DisplayOption myDisplayOption = invDistWeightedInterpolate(
                 myInfo,
-                getPredefinedDeviceProfiles(context, gridName, deviceType,
-                        /*allowDisabledGrid=*/false),
+                getPredefinedDeviceProfiles(
+                        context,
+                        gridName,
+                        myInfo,
+                        /*allowDisabledGrid=*/false,
+                        isFixedLandscapeMode
+                ),
                 deviceType);
 
         DisplayOption result = new DisplayOption(defaultDisplayOption.grid)
@@ -294,40 +327,16 @@
         System.arraycopy(defaultDisplayOption.borderSpaces, 0, result.borderSpaces, 0,
                 COUNT_SIZES);
 
-        initGrid(context, myInfo, result, deviceType);
+        initGrid(context, myInfo, result);
     }
 
     @Override
     public void close() {
         DisplayController.INSTANCE.executeIfCreated(dc -> dc.setPriorityListener(null));
-    }
-
-    /**
-     * Reinitialize the current grid after a restore, where some grids might now be disabled.
-     */
-    public void reinitializeAfterRestore(Context context) {
-        String currentGridName = getCurrentGridName(context);
-        String currentDbFile = dbFile;
-        String newGridName = initGrid(context, currentGridName);
-        String newDbFile = dbFile;
-        FileLog.d(TAG, "Reinitializing grid after restore."
-                + " currentGridName=" + currentGridName
-                + ", currentDbFile=" + currentDbFile
-                + ", newGridName=" + newGridName
-                + ", newDbFile=" + newDbFile);
-        if (!newDbFile.equals(currentDbFile)) {
-            FileLog.d(TAG, "Restored grid is disabled : " + currentGridName
-                    + ", migrating to: " + newGridName
-                    + ", removing all other grid db files");
-            for (String gridDbFile : LauncherFiles.GRID_DB_FILES) {
-                if (gridDbFile.equals(currentDbFile)) {
-                    continue;
-                }
-                if (context.getDatabasePath(gridDbFile).delete()) {
-                    FileLog.d(TAG, "Removed old grid db file: " + gridDbFile);
-                }
-            }
-            setCurrentGrid(context, newGridName);
+        if (mLandscapeModePreferenceListener != null) {
+            LauncherPrefs.INSTANCE.executeIfCreated(
+                    lp -> lp.removeListener(mLandscapeModePreferenceListener, FIXED_LANDSCAPE_MODE)
+            );
         }
     }
 
@@ -336,25 +345,48 @@
     }
 
     private String initGrid(Context context, String gridName) {
-        Info displayInfo = DisplayController.INSTANCE.get(context).getInfo();
-        @DeviceType int deviceType = displayInfo.getDeviceType();
+        if (!Flags.oneGridSpecs() && (isFixedLandscapeMode || FIXED_LANDSCAPE_MODE.get(context))) {
+            LauncherPrefs.get(context).put(FIXED_LANDSCAPE_MODE, false);
+            isFixedLandscapeMode = false;
+        }
 
-        ArrayList<DisplayOption> allOptions =
-                getPredefinedDeviceProfiles(context, gridName, deviceType,
-                        RestoreDbTask.isPending(context));
+        Info displayInfo = DisplayController.INSTANCE.get(context).getInfo();
+
+        List<DisplayOption> allOptions = getPredefinedDeviceProfiles(
+                context,
+                gridName,
+                displayInfo,
+                RestoreDbTask.isPending(context),
+                FIXED_LANDSCAPE_MODE.get(context)
+        );
+
+        // Filter out options that don't have the same number of columns as the grid
+        DeviceGridState deviceGridState = new DeviceGridState(context);
+        List<DisplayOption> allOptionsFilteredByColCount =
+                filterByColumnCount(allOptions, deviceGridState.getColumns());
+
         DisplayOption displayOption =
-                invDistWeightedInterpolate(displayInfo, allOptions, deviceType);
-        initGrid(context, displayInfo, displayOption, deviceType);
+                invDistWeightedInterpolate(displayInfo, allOptionsFilteredByColCount.isEmpty()
+                                ? new ArrayList<>(allOptions)
+                                : new ArrayList<>(allOptionsFilteredByColCount),
+                        displayInfo.getDeviceType());
+        initGrid(context, displayInfo, displayOption);
         return displayOption.grid.name;
     }
 
+    private List<DisplayOption> filterByColumnCount(
+            List<DisplayOption> allOptions, int numColumns) {
+        return allOptions.stream().filter(
+                option -> option.grid.numColumns == numColumns).toList();
+    }
+
     /**
      * @deprecated This is a temporary solution because on the backup and restore case we modify the
      * IDP, this resets it. b/332974074
      */
     @Deprecated
     public void reset(Context context) {
-        initGrid(context, getCurrentGridName(context));
+        initGrid(context, getDefaultGridName(context));
     }
 
     @VisibleForTesting
@@ -362,8 +394,7 @@
         return new InvariantDeviceProfile().initGrid(context, null);
     }
 
-    private void initGrid(Context context, Info displayInfo, DisplayOption displayOption,
-            @DeviceType int deviceType) {
+    private void initGrid(Context context, Info displayInfo, DisplayOption displayOption) {
         DisplayMetrics metrics = context.getResources().getDisplayMetrics();
         GridOption closestProfile = displayOption.grid;
         numRows = closestProfile.numRows;
@@ -382,6 +413,7 @@
         isScalable = closestProfile.isScalable;
         devicePaddingId = closestProfile.devicePaddingId;
         workspaceSpecsId = closestProfile.mWorkspaceSpecsId;
+        rowCountSpecsId = closestProfile.mRowCountSpecsId;
         workspaceSpecsTwoPanelId = closestProfile.mWorkspaceSpecsTwoPanelId;
         allAppsSpecsId = closestProfile.mAllAppsSpecsId;
         allAppsSpecsTwoPanelId = closestProfile.mAllAppsSpecsTwoPanelId;
@@ -395,7 +427,8 @@
         allAppsCellSpecsTwoPanelId = closestProfile.mAllAppsCellSpecsTwoPanelId;
         numAllAppsRowsForCellHeightCalculation =
                 closestProfile.mNumAllAppsRowsForCellHeightCalculation;
-        this.deviceType = deviceType;
+        this.deviceType = displayInfo.getDeviceType();
+        this.displayInfo = displayInfo;
 
         inlineNavButtonsEndSpacing = closestProfile.inlineNavButtonsEndSpacing;
 
@@ -439,6 +472,9 @@
 
         startAlignTaskbar = displayOption.startAlignTaskbar;
 
+        // Fixed Landscape mode
+        isFixedLandscapeMode = FIXED_LANDSCAPE_MODE.get(context) && Flags.oneGridSpecs();
+
         // If the partner customization apk contains any grid overrides, apply them
         // Supported overrides: numRows, numColumns, iconSize
         applyPartnerDeviceProfileOverrides(context, metrics);
@@ -494,10 +530,35 @@
         mChangeListeners.remove(listener);
     }
 
+    /**
+     * Updates the current grid, this triggers a new IDP, reloads the database and triggers a grid
+     * migration.
+     */
+    public void setCurrentGrid(Context context, String newGridName) {
+        LauncherPrefs.get(context).put(GRID_NAME, newGridName);
+        MAIN_EXECUTOR.execute(() -> {
+            Trace.beginSection("InvariantDeviceProfile#setCurrentGrid");
+            onConfigChanged(context.getApplicationContext());
+            Trace.endSection();
+        });
+    }
 
-    public void setCurrentGrid(Context context, String gridName) {
-        LauncherPrefs.get(context).put(GRID_NAME, gridName);
-        MAIN_EXECUTOR.execute(() -> onConfigChanged(context.getApplicationContext()));
+    /**
+     * Updates the fixed landscape mode, this triggers a new IDP, reloads the database and triggers
+     * a grid migration.
+     */
+    public void setFixedLandscape(Context context, boolean isFixedLandscape) {
+        this.isFixedLandscapeMode = isFixedLandscape;
+        if (isFixedLandscape) {
+            // When in isFixedLandscape there should only be one default grid to choose from
+            MAIN_EXECUTOR.execute(() -> {
+                Trace.beginSection("InvariantDeviceProfile#setFixedLandscape");
+                onConfigChanged(context.getApplicationContext());
+                Trace.endSection();
+            });
+        } else {
+            setCurrentGrid(context, LauncherPrefs.get(context).get(GRID_NAME));
+        }
     }
 
     private Object[] toModelState() {
@@ -521,8 +582,19 @@
         }
     }
 
-    private static ArrayList<DisplayOption> getPredefinedDeviceProfiles(Context context,
-            String gridName, @DeviceType int deviceType, boolean allowDisabledGrid) {
+    private static boolean firstGridFilter(GridOption gridOption, int deviceType,
+            boolean allowDisabledGrid, boolean isFixedLandscapeMode) {
+        return (gridOption.isEnabled(deviceType) || allowDisabledGrid)
+                && gridOption.filterByFlag(deviceType, isFixedLandscapeMode);
+    }
+
+    private static List<DisplayOption> getPredefinedDeviceProfiles(
+            Context context,
+            String gridName,
+            Info displayInfo,
+            boolean allowDisabledGrid,
+            boolean isFixedLandscapeMode
+    ) {
         ArrayList<DisplayOption> profiles = new ArrayList<>();
 
         try (XmlResourceParser parser = context.getResources().getXml(R.xml.device_profiles)) {
@@ -532,9 +604,10 @@
                     parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
                 if ((type == XmlPullParser.START_TAG)
                         && GridOption.TAG_NAME.equals(parser.getName())) {
-
-                    GridOption gridOption = new GridOption(context, Xml.asAttributeSet(parser));
-                    if (gridOption.isEnabled(deviceType) || allowDisabledGrid) {
+                    GridOption gridOption = new GridOption(
+                            context, Xml.asAttributeSet(parser), displayInfo);
+                    if (firstGridFilter(gridOption, displayInfo.getDeviceType(), allowDisabledGrid,
+                            isFixedLandscapeMode)) {
                         final int displayDepth = parser.getDepth();
                         while (((type = parser.next()) != XmlPullParser.END_TAG
                                 || parser.getDepth() > displayDepth)
@@ -551,23 +624,25 @@
         } catch (IOException | XmlPullParserException e) {
             throw new RuntimeException(e);
         }
-
         ArrayList<DisplayOption> filteredProfiles = new ArrayList<>();
         if (!TextUtils.isEmpty(gridName)) {
             for (DisplayOption option : profiles) {
-                if (gridName.equals(option.grid.name)
-                        && (option.grid.isEnabled(deviceType) || allowDisabledGrid)) {
+                if (gridName.equals(option.grid.name) && (option.grid.isEnabled(
+                        displayInfo.getDeviceType()) || allowDisabledGrid)) {
                     filteredProfiles.add(option);
                 }
             }
         }
-        if (filteredProfiles.isEmpty()) {
-            // No grid found, use the default options
+        if (filteredProfiles.isEmpty() && TextUtils.isEmpty(gridName)) {
+            // Use the default options since gridName is empty and there's no valid grids.
             for (DisplayOption option : profiles) {
                 if (option.canBeDefault) {
                     filteredProfiles.add(option);
                 }
             }
+        } else if (filteredProfiles.isEmpty()) {
+            // In this case we had a grid selected but we couldn't find it.
+            filteredProfiles.addAll(profiles);
         }
         if (filteredProfiles.isEmpty()) {
             throw new RuntimeException("No display option with canBeDefault=true");
@@ -576,6 +651,70 @@
     }
 
     /**
+     * Parses through the xml to find NumRows specs. Then calls findBestRowCount to get the correct
+     * row count for this GridOption.
+     *
+     * @return the result of {@link #findBestRowCount(List, Info)}.
+     */
+    public static NumRows getRowCount(ResourceHelper resourceHelper, Context context,
+            Info displayInfo) {
+        ArrayList<NumRows> rowCounts = new ArrayList<>();
+
+        try (XmlResourceParser parser = resourceHelper.getXml()) {
+            final int depth = parser.getDepth();
+            int type;
+            while (((type = parser.next()) != XmlPullParser.END_TAG
+                    || parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
+                if ((type == XmlPullParser.START_TAG)
+                        && "NumRows".equals(parser.getName())) {
+                    rowCounts.add(new NumRows(context, Xml.asAttributeSet(parser)));
+                }
+            }
+        } catch (IOException | XmlPullParserException e) {
+            throw new RuntimeException(e);
+        }
+
+        return findBestRowCount(rowCounts, displayInfo);
+    }
+
+    /**
+     * @return the biggest row count that fits the display dimensions spec using NumRows to
+     * determine that. If no best row count is found, return -1.
+     */
+    public static NumRows findBestRowCount(List<NumRows> list, Info displayInfo) {
+        int minWidthPx = Integer.MAX_VALUE;
+        int minHeightPx = Integer.MAX_VALUE;
+        for (WindowBounds bounds : displayInfo.supportedBounds) {
+            boolean isTablet = displayInfo.isTablet(bounds);
+            if (isTablet && displayInfo.getDeviceType() == TYPE_MULTI_DISPLAY) {
+                // For split displays, take half width per page
+                minWidthPx = Math.min(minWidthPx, bounds.availableSize.x / 2);
+                minHeightPx = Math.min(minHeightPx, bounds.availableSize.y);
+            } else if (!isTablet && bounds.isLandscape()) {
+                // We will use transposed layout in this case
+                minWidthPx = Math.min(minWidthPx, bounds.availableSize.y);
+                minHeightPx = Math.min(minHeightPx, bounds.availableSize.x);
+            } else {
+                minWidthPx = Math.min(minWidthPx, bounds.availableSize.x);
+                minHeightPx = Math.min(minHeightPx, bounds.availableSize.y);
+            }
+        }
+
+        NumRows selectedRow = null;
+        for (NumRows item: list) {
+            if (minWidthPx >= item.mMinDeviceWidthPx && minHeightPx >= item.mMinDeviceHeightPx) {
+                if (selectedRow == null || selectedRow.mNumRowsNew < item.mNumRowsNew) {
+                    selectedRow = item;
+                }
+            }
+        }
+        if (selectedRow != null) {
+            return selectedRow;
+        }
+        return null;
+    }
+
+    /**
      * Returns the GridOption associated to the given file name or null if the fileName is not
      * supported.
      * Ej, launcher.db -> "normal grid", launcher_4_by_4.db -> "practical grid"
@@ -592,7 +731,6 @@
      * supported. Ej. 4x4 -> normal, 5x4 -> practical, etc.
      * (Note: the name of the grid can be different for the same grid size depending of
      * the values of the InvariantDeviceProfile)
-     *
      */
     public String getGridNameFromSize(Context context, Point size) {
         return parseAllGridOptions(context).stream()
@@ -618,18 +756,18 @@
      * @return all the grid options that can be shown on the device
      */
     public List<GridOption> parseAllGridOptions(Context context) {
-        return parseAllDefinedGridOptions(context)
+        return parseAllDefinedGridOptions(context, displayInfo)
                 .stream()
                 .filter(go -> go.isEnabled(deviceType))
+                .filter(go -> go.filterByFlag(deviceType, isFixedLandscapeMode))
                 .collect(Collectors.toList());
     }
 
     /**
      * @return all the grid options that can be shown on the device
      */
-    public static List<GridOption> parseAllDefinedGridOptions(Context context) {
+    public static List<GridOption> parseAllDefinedGridOptions(Context context, Info displayInfo) {
         List<GridOption> result = new ArrayList<>();
-
         try (XmlResourceParser parser = context.getResources().getXml(R.xml.device_profiles)) {
             final int depth = parser.getDepth();
             int type;
@@ -637,7 +775,7 @@
                     || parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
                 if ((type == XmlPullParser.START_TAG)
                         && GridOption.TAG_NAME.equals(parser.getName())) {
-                    result.add(new GridOption(context, Xml.asAttributeSet(parser)));
+                    result.add(new GridOption(context, Xml.asAttributeSet(parser), displayInfo));
                 }
             }
         } catch (IOException | XmlPullParserException e) {
@@ -704,7 +842,7 @@
     }
 
     private static DisplayOption invDistWeightedInterpolate(
-            Info displayInfo, ArrayList<DisplayOption> points, @DeviceType int deviceType) {
+            Info displayInfo, List<DisplayOption> points, @DeviceType int deviceType) {
         int minWidthPx = Integer.MAX_VALUE;
         int minHeightPx = Integer.MAX_VALUE;
         for (WindowBounds bounds : displayInfo.supportedBounds) {
@@ -728,7 +866,7 @@
         float height = dpiFromPx(minHeightPx, displayInfo.getDensityDpi());
 
         // Sort the profiles based on the closeness to the device size
-        Collections.sort(points, (a, b) ->
+        points.sort((a, b) ->
                 Float.compare(dist(width, height, a.minWidthDps, a.minHeightDps),
                         dist(width, height, b.minWidthDps, b.minHeightDps)));
 
@@ -850,6 +988,7 @@
         private static final int DONT_INLINE_QSB = 0;
 
         public final String name;
+        public final String title;
         public final int numRows;
         public final int numColumns;
         public final int numSearchContainerColumns;
@@ -876,6 +1015,7 @@
         private final int demoModeLayoutId;
 
         private final boolean isScalable;
+        private final boolean mIsDualGrid;
         private final int devicePaddingId;
         private final int mWorkspaceSpecsId;
         private final int mWorkspaceSpecsTwoPanelId;
@@ -889,22 +1029,40 @@
         private final int mWorkspaceCellSpecsTwoPanelId;
         private final int mAllAppsCellSpecsId;
         private final int mAllAppsCellSpecsTwoPanelId;
+        private final int mRowCountSpecsId;
+        private final boolean mIsFixedLandscape;
+        private final boolean mIsOldGrid;
 
-        public GridOption(Context context, AttributeSet attrs) {
+        public GridOption(Context context, AttributeSet attrs, Info displayInfo) {
             TypedArray a = context.obtainStyledAttributes(
                     attrs, R.styleable.GridDisplayOption);
             name = a.getString(R.styleable.GridDisplayOption_name);
-            numRows = a.getInt(R.styleable.GridDisplayOption_numRows, 0);
+            title = a.getString(R.styleable.GridDisplayOption_title);
+            deviceCategory = a.getInt(R.styleable.GridDisplayOption_deviceCategory,
+                    DEVICE_CATEGORY_ALL);
+            mRowCountSpecsId = a.getResourceId(
+                    R.styleable.GridDisplayOption_rowCountSpecsId, INVALID_RESOURCE_HANDLE);
+            mIsDualGrid = a.getBoolean(R.styleable.GridDisplayOption_isDualGrid, false);
+            if (mRowCountSpecsId != INVALID_RESOURCE_HANDLE) {
+                ResourceHelper resourceHelper = new ResourceHelper(context, mRowCountSpecsId);
+                NumRows numR = getRowCount(resourceHelper, context, displayInfo);
+                numRows = numR.mNumRowsNew;
+                dbFile = numR.mDbFile;
+                defaultLayoutId = numR.mDefaultLayoutId;
+                demoModeLayoutId = numR.mDemoModeLayoutId;
+            } else {
+                numRows = a.getInt(R.styleable.GridDisplayOption_numRows, 0);
+                dbFile = a.getString(R.styleable.GridDisplayOption_dbFile);
+                defaultLayoutId = a.getResourceId(
+                        R.styleable.GridDisplayOption_defaultLayoutId, 0);
+                demoModeLayoutId = a.getResourceId(
+                        R.styleable.GridDisplayOption_demoModeLayoutId, defaultLayoutId);
+            }
+
             numColumns = a.getInt(R.styleable.GridDisplayOption_numColumns, 0);
             numSearchContainerColumns = a.getInt(
                     R.styleable.GridDisplayOption_numSearchContainerColumns, numColumns);
 
-            dbFile = a.getString(R.styleable.GridDisplayOption_dbFile);
-            defaultLayoutId = a.getResourceId(
-                    R.styleable.GridDisplayOption_defaultLayoutId, 0);
-            demoModeLayoutId = a.getResourceId(
-                    R.styleable.GridDisplayOption_demoModeLayoutId, defaultLayoutId);
-
             allAppsStyle = a.getResourceId(R.styleable.GridDisplayOption_allAppsStyle,
                     R.style.AllAppsStyleDefault);
             numAllAppsColumns = a.getInt(
@@ -964,8 +1122,6 @@
                     R.styleable.GridDisplayOption_isScalable, false);
             devicePaddingId = a.getResourceId(
                     R.styleable.GridDisplayOption_devicePaddingId, INVALID_RESOURCE_HANDLE);
-            deviceCategory = a.getInt(R.styleable.GridDisplayOption_deviceCategory,
-                    DEVICE_CATEGORY_ALL);
 
             if (FeatureFlags.enableResponsiveWorkspace()) {
                 mWorkspaceSpecsId = a.getResourceId(
@@ -1019,6 +1175,9 @@
                 mNumAllAppsRowsForCellHeightCalculation = numRows;
             }
 
+            mIsFixedLandscape = a.getBoolean(R.styleable.GridDisplayOption_isFixedLandscape, false);
+            mIsOldGrid = a.getBoolean(R.styleable.GridDisplayOption_isOldGrid, false);
+
             int inlineForRotation = a.getInt(R.styleable.GridDisplayOption_inlineQsb,
                     DONT_INLINE_QSB);
             inlineQsb[INDEX_DEFAULT] =
@@ -1048,6 +1207,57 @@
                     return false;
             }
         }
+
+        /**
+         * Returns true if the grid option should be used given the flags that are toggled on/off.
+         */
+        public boolean filterByFlag(int deviceType, boolean isFixedLandscape) {
+            if (deviceType == TYPE_TABLET) {
+                return Flags.oneGridRotationHandling() == mIsDualGrid;
+            }
+
+            // Here we return true if fixed landscape mode should be on.
+            if (mIsFixedLandscape || isFixedLandscape) {
+                return mIsFixedLandscape && isFixedLandscape && Flags.oneGridSpecs();
+            }
+
+            // Here we return true if we want to show the new grids.
+            if (mRowCountSpecsId != INVALID_RESOURCE_HANDLE) {
+                return Flags.oneGridSpecs();
+            }
+
+            // Here we return true if we want to show the old grids.
+            if (mIsOldGrid) {
+                return !Flags.oneGridSpecs();
+            }
+
+            return true;
+        }
+    }
+
+    public static final class NumRows {
+        final int mNumRowsNew;
+        final float mMinDeviceWidthPx;
+        final float mMinDeviceHeightPx;
+        final String mDbFile;
+        final int mDefaultLayoutId;
+        final int mDemoModeLayoutId;
+
+
+        NumRows(Context context, AttributeSet attrs) {
+            TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.NumRows);
+
+            mNumRowsNew = (int) a.getFloat(R.styleable.NumRows_numRowsNew, 0);
+            mMinDeviceWidthPx = a.getFloat(R.styleable.NumRows_minDeviceWidthPx, 0);
+            mMinDeviceHeightPx = a.getFloat(R.styleable.NumRows_minDeviceHeightPx, 0);
+            mDbFile = a.getString(R.styleable.NumRows_dbFile);
+            mDefaultLayoutId = a.getResourceId(
+                    R.styleable.NumRows_defaultLayoutId, 0);
+            mDemoModeLayoutId = a.getResourceId(
+                    R.styleable.NumRows_demoModeLayoutId, mDefaultLayoutId);
+
+            a.recycle();
+        }
     }
 
     @VisibleForTesting
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index b0ec9b0..305941e 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -224,10 +224,10 @@
 import com.android.launcher3.touch.AllAppsSwipeController;
 import com.android.launcher3.touch.ItemLongClickListener;
 import com.android.launcher3.util.ActivityResultInfo;
-import com.android.launcher3.util.ActivityTracker;
 import com.android.launcher3.util.BackPressHandler;
 import com.android.launcher3.util.CannedAnimationCoordinator;
 import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.ContextTracker;
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.IntSet;
 import com.android.launcher3.util.ItemInflater;
@@ -279,6 +279,7 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Optional;
 import java.util.function.Predicate;
 import java.util.function.Supplier;
@@ -292,7 +293,8 @@
         PluginListener<LauncherOverlayPlugin> {
     public static final String TAG = "Launcher";
 
-    public static final ActivityTracker<Launcher> ACTIVITY_TRACKER = new ActivityTracker<>();
+    public static final ContextTracker.ActivityTracker<Launcher> ACTIVITY_TRACKER =
+            new ContextTracker.ActivityTracker<>();
 
     static final boolean LOGD = false;
 
@@ -412,6 +414,7 @@
 
     private final List<BackPressHandler> mBackPressedHandlers = new ArrayList<>();
     private boolean mIsColdStartupAfterReboot;
+    private boolean mForceConfigUpdate;
 
     private boolean mIsNaturalScrollingEnabled;
 
@@ -534,6 +537,7 @@
 
         mPopupDataProvider = new PopupDataProvider(this::updateNotificationDots);
         mWidgetPickerDataProvider = new WidgetPickerDataProvider();
+        PillColorProvider.getInstance(mWorkspace.getContext()).registerObserver();
 
         boolean internalStateHandled = ACTIVITY_TRACKER.handleCreate(this);
         if (internalStateHandled) {
@@ -756,10 +760,9 @@
     protected void onHandleConfigurationChanged() {
         Trace.beginSection("Launcher#onHandleconfigurationChanged");
         try {
-            if (!initDeviceProfile(mDeviceProfile.inv)) {
+            if (!initDeviceProfile(mDeviceProfile.inv) && !mForceConfigUpdate) {
                 return;
             }
-
             dispatchDeviceProfileChanged();
             reapplyUi();
             mDragLayer.recreateControllers();
@@ -770,10 +773,20 @@
             mModel.rebindCallbacks();
             updateDisallowBack();
         } finally {
+            mForceConfigUpdate = false;
             Trace.endSection();
         }
     }
 
+    private void updateFixedLandscape() {
+        if (!com.android.launcher3.Flags.oneGridSpecs()) {
+            return;
+        }
+        getRotationHelper().setFixedLandscape(
+                Objects.requireNonNull(mDeviceProfile.inv).isFixedLandscapeMode
+        );
+    }
+
     public void onAssistantVisibilityChanged(float visibility) {
         mHotseat.getQsb().setAlpha(1f - visibility);
     }
@@ -802,6 +815,7 @@
                     mDeviceProfile.numShownHotseatIcons);
         }
         mModelWriter = mModel.getWriter(true, mCellPosMapper, this);
+        updateFixedLandscape();
         return true;
     }
 
@@ -1342,7 +1356,8 @@
         if (requestArgs != null) {
             setWaitingForResult(requestArgs);
         }
-        mPendingActivityRequestCode = savedState.getInt(RUNTIME_STATE_PENDING_REQUEST_CODE);
+        mPendingActivityRequestCode = savedState.getInt(
+                RUNTIME_STATE_PENDING_REQUEST_CODE, mPendingActivityRequestCode);
 
         mPendingActivityResult = savedState.getParcelable(RUNTIME_STATE_PENDING_ACTIVITY_RESULT);
 
@@ -1776,7 +1791,7 @@
     @Override
     public void onDestroy() {
         super.onDestroy();
-        ACTIVITY_TRACKER.onActivityDestroyed(this);
+        ACTIVITY_TRACKER.onContextDestroyed(this);
 
         SettingsCache.INSTANCE.get(this).unregister(TOUCHPAD_NATURAL_SCROLLING,
                 mNaturalScrollingChangedListener);
@@ -1799,6 +1814,7 @@
         // changes while launcher is still loading.
         getRootView().getViewTreeObserver().removeOnPreDrawListener(mOnInitialBindListener);
         mOverlayManager.onActivityDestroyed();
+        PillColorProvider.getInstance(mWorkspace.getContext()).unregisterObserver();
     }
 
     public LauncherAccessibilityDelegate getAccessibilityDelegate() {
@@ -1872,13 +1888,18 @@
             }
         }
 
-        // Exit spring loaded mode if necessary after adding the widget
-        Runnable onComplete = MULTI_SELECT_EDIT_MODE.get() ? null
-                : () -> mStateManager.goToState(NORMAL, SPRING_LOADED_EXIT_DELAY);
+        // Exit spring loaded mode if necessary after adding the widget; unless config activity was
+        // started.
+        Runnable onComplete = MULTI_SELECT_EDIT_MODE.get() ? null : () -> mStateManager.goToState(
+                NORMAL, SPRING_LOADED_EXIT_DELAY);
         completeAddAppWidget(appWidgetId, info, boundWidget,
                 addFlowHandler.getProviderInfo(this), addFlowHandler.needsConfigure(),
                 false, widgetPreviewBitmap);
-        mWorkspace.removeExtraEmptyScreenDelayed(delay, false, onComplete);
+        // Remove extra screen if widget drop concluded. If a config activity was started, extra
+        // screen will be removed when we get back its result.
+        if (!isActivityStarted) {
+            mWorkspace.removeExtraEmptyScreenDelayed(delay, false, onComplete);
+        }
     }
 
     public void addPendingItem(PendingAddItemInfo info, int container, int screenId,
@@ -2620,8 +2641,9 @@
      * See {@code LauncherBindingDelegate}
      */
     @Override
-    public void bindAllWidgets(final List<WidgetsListBaseEntry> allWidgets) {
-        mModelCallbacks.bindAllWidgets(allWidgets);
+    public void bindAllWidgets(@NonNull final List<WidgetsListBaseEntry> allWidgets,
+            @NonNull final List<WidgetsListBaseEntry> defaultWidgets) {
+        mModelCallbacks.bindAllWidgets(allWidgets, defaultWidgets);
     }
 
     @Override
@@ -3146,6 +3168,13 @@
         return mAnimationCoordinator;
     }
 
+    /**
+     * Set to force config update when set to true next time onHandleConfigurationChanged is called.
+     */
+    public void setForceConfigUpdate(boolean forceConfigUpdate) {
+        mForceConfigUpdate = forceConfigUpdate;
+    }
+
     @Override
     public View.OnLongClickListener getAllAppsItemLongClickListener() {
         return ItemLongClickListener.INSTANCE_ALL_APPS;
diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java
index 15641ab..3936fe6 100644
--- a/src/com/android/launcher3/LauncherAppState.java
+++ b/src/com/android/launcher3/LauncherAppState.java
@@ -48,6 +48,7 @@
 import com.android.launcher3.icons.LauncherIconProvider;
 import com.android.launcher3.icons.LauncherIcons;
 import com.android.launcher3.model.ModelLauncherCallbacks;
+import com.android.launcher3.model.WidgetsFilterDataProvider;
 import com.android.launcher3.notification.NotificationListener;
 import com.android.launcher3.pm.InstallSessionHelper;
 import com.android.launcher3.pm.InstallSessionTracker;
@@ -163,8 +164,7 @@
 
         LockedUserState.get(context).runOnUserUnlocked(() -> {
             CustomWidgetManager cwm = CustomWidgetManager.INSTANCE.get(mContext);
-            cwm.setWidgetRefreshCallback(mModel::refreshAndBindWidgetsAndShortcuts);
-            mOnTerminateCallback.add(() -> cwm.setWidgetRefreshCallback(null));
+            mOnTerminateCallback.add(cwm.addWidgetRefreshCallback(mModel::rebindCallbacks)::close);
 
             IconObserver observer = new IconObserver();
             SafeCloseable iconChangeTracker = mIconProvider.registerIconChangeListener(
@@ -176,7 +176,7 @@
                     () -> LauncherPrefs.get(mContext).removeListener(observer, THEMED_ICONS));
 
             InstallSessionTracker installSessionTracker =
-                    InstallSessionHelper.INSTANCE.get(context).registerInstallTracker(mModel);
+                    InstallSessionHelper.INSTANCE.get(context).registerInstallTracker(callbacks);
             mOnTerminateCallback.add(installSessionTracker::unregister);
         });
 
@@ -188,7 +188,9 @@
         mOnTerminateCallback.add(() ->
                 settingsCache.unregister(NOTIFICATION_BADGING_URI, notificationLister));
         // Register an observer to notify Launcher about Private Space settings toggle.
-        registerPrivateSpaceHideWhenLockListener(settingsCache);
+        if (!android.multiuser.Flags.addLauncherUserConfig()) {
+            registerPrivateSpaceHideWhenLockListener(settingsCache);
+        }
     }
 
     public LauncherAppState(Context context, @Nullable String iconCacheFileName) {
@@ -198,7 +200,8 @@
         mIconProvider = new LauncherIconProvider(context);
         mIconCache = new IconCache(mContext, mInvariantDeviceProfile,
                 iconCacheFileName, mIconProvider);
-        mModel = new LauncherModel(context, this, mIconCache, new AppFilter(mContext),
+        mModel = new LauncherModel(context, this, mIconCache,
+                WidgetsFilterDataProvider.Companion.newInstance(context), new AppFilter(mContext),
                 PackageManagerHelper.INSTANCE.get(context), iconCacheFileName != null);
         mOnTerminateCallback.add(mIconCache::close);
         mOnTerminateCallback.add(mModel::destroy);
@@ -266,7 +269,7 @@
     }
 
     private class IconObserver
-            implements IconProvider.IconChangeListener, OnSharedPreferenceChangeListener {
+            implements IconProvider.IconChangeListener, LauncherPrefChangeListener {
 
         @Override
         public void onAppIconChanged(String packageName, UserHandle user) {
@@ -288,7 +291,7 @@
         }
 
         @Override
-        public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
+        public void onPrefChanged(String key) {
             if (Themes.KEY_THEMED_ICONS.equals(key)) {
                 mIconProvider.setIconThemeSupported(Themes.isThemedIconEnabled(mContext));
                 verifyIconChanged();
diff --git a/src/com/android/launcher3/LauncherApplication.java b/src/com/android/launcher3/LauncherApplication.java
index 4c82e56..678901b 100644
--- a/src/com/android/launcher3/LauncherApplication.java
+++ b/src/com/android/launcher3/LauncherApplication.java
@@ -26,15 +26,25 @@
  */
 public class LauncherApplication extends Application {
 
-    private LauncherBaseAppComponent mAppComponent;
+    private volatile LauncherBaseAppComponent mAppComponent;
     @Override
     public void onCreate() {
         super.onCreate();
         MainProcessInitializer.initialize(this);
-        initDagger();
     }
 
     public LauncherAppComponent getAppComponent() {
+        if (mAppComponent == null) {
+            synchronized (this) {
+                // Check for null again, as it may have been assigned on a different thread. This
+                // avoids holding synchronization locks everytime.
+                if (mAppComponent == null) {
+                    // Initialize the dagger component on demand as content providers can get
+                    // accessed before the Launcher application (b/36917845#comment4)
+                    initDaggerComponent(DaggerLauncherAppComponent.builder());
+                }
+            }
+        }
         // Since supertype setters will return a supertype.builder and @Component.Builder types
         // must not have any generic types.
         // We need to cast mAppComponent to {@link LauncherAppComponent} since appContext()
@@ -42,7 +52,10 @@
         return (LauncherAppComponent) mAppComponent;
     }
 
-    protected void initDagger() {
-        mAppComponent = DaggerLauncherAppComponent.builder().appContext(this).build();
+    /**
+     * Init with the desired dagger component.
+     */
+    public void initDaggerComponent(LauncherAppComponent.Builder componentBuilder) {
+        mAppComponent = componentBuilder.appContext(this).build();
     }
 }
diff --git a/src/com/android/launcher3/LauncherBackupAgent.java b/src/com/android/launcher3/LauncherBackupAgent.java
index 2617b93..a96495d 100644
--- a/src/com/android/launcher3/LauncherBackupAgent.java
+++ b/src/com/android/launcher3/LauncherBackupAgent.java
@@ -1,5 +1,7 @@
 package com.android.launcher3;
 
+import static com.android.launcher3.LauncherPrefs.NO_DB_FILES_RESTORED;
+
 import android.app.backup.BackupAgent;
 import android.app.backup.BackupDataInput;
 import android.app.backup.BackupDataOutput;
@@ -10,10 +12,13 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.util.Arrays;
+import java.util.stream.Collectors;
 
 public class LauncherBackupAgent extends BackupAgent {
-
     private static final String TAG = "LauncherBackupAgent";
+    private static final String DB_FILE_PREFIX = "launcher";
+    private static final String DB_FILE_SUFFIX = ".db";
 
     @Override
     public void onCreate() {
@@ -47,7 +52,34 @@
 
     @Override
     public void onRestoreFinished() {
-        FileLog.d(TAG, "onRestoreFinished: set pending for RestoreDbTask");
         RestoreDbTask.setPending(this);
+        FileLog.d(TAG, "onRestoreFinished: set pending for RestoreDbTask");
+        markIfFilesWereNotActuallyRestored();
+    }
+
+    /**
+     * When restore is finished, we check to see if any db files were successfully restored. If not,
+     * our restore will fail later, but will report a different cause. This is important to split
+     * out the metric failures that are launcher's fault, and those that are due to bugs in the
+     * backup/restore code itself.
+     */
+    private void markIfFilesWereNotActuallyRestored() {
+        File directory = new File(getDatabasePath(InvariantDeviceProfile.INSTANCE.get(this).dbFile)
+                .getParent());
+        if (!directory.exists()) {
+            FileLog.e(TAG, "restore failed as target database directory doesn't exist");
+        } else {
+            // Check for any db file that was restored, and collect as list
+            String fileNames = Arrays.stream(directory.listFiles())
+                    .map(File::getName)
+                    .filter(n -> n.startsWith(DB_FILE_PREFIX) && n.endsWith(DB_FILE_SUFFIX))
+                    .collect(Collectors.joining(", "));
+            if (fileNames.isBlank()) {
+                FileLog.e(TAG, "no database files were successfully restored");
+                LauncherPrefs.get(this).putSync(NO_DB_FILES_RESTORED.to(true));
+            } else {
+                FileLog.d(TAG, "database files successfully restored: " + fileNames);
+            }
+        }
     }
 }
diff --git a/src/com/android/launcher3/LauncherFiles.java b/src/com/android/launcher3/LauncherFiles.java
index d730cea..df75470 100644
--- a/src/com/android/launcher3/LauncherFiles.java
+++ b/src/com/android/launcher3/LauncherFiles.java
@@ -16,11 +16,15 @@
     private static final String XML = ".xml";
 
     public static final String LAUNCHER_DB = "launcher.db";
+    public static final String LAUNCHER_5_BY_8_DB = "launcher_5_by_8.db";
     public static final String LAUNCHER_6_BY_5_DB = "launcher_6_by_5.db";
     public static final String LAUNCHER_4_BY_5_DB = "launcher_4_by_5.db";
+    public static final String LAUNCHER_4_BY_6_DB = "launcher_4_by_6.db";
+    public static final String LAUNCHER_5_BY_6_DB = "launcher_5_by_6.db";
     public static final String LAUNCHER_4_BY_4_DB = "launcher_4_by_4.db";
     public static final String LAUNCHER_3_BY_3_DB = "launcher_3_by_3.db";
     public static final String LAUNCHER_2_BY_2_DB = "launcher_2_by_2.db";
+    public static final String LAUNCHER_7_BY_3_DB = "launcher_7_by_3.db";
     public static final String BACKUP_DB = "backup.db";
     public static final String SHARED_PREFERENCES_KEY = "com.android.launcher3.prefs";
     public static final String MANAGED_USER_PREFERENCES_KEY =
@@ -33,11 +37,15 @@
 
     public static final List<String> GRID_DB_FILES = Collections.unmodifiableList(Arrays.asList(
             LAUNCHER_DB,
+            LAUNCHER_5_BY_8_DB,
             LAUNCHER_6_BY_5_DB,
             LAUNCHER_4_BY_5_DB,
+            LAUNCHER_4_BY_6_DB,
+            LAUNCHER_5_BY_6_DB,
             LAUNCHER_4_BY_4_DB,
             LAUNCHER_3_BY_3_DB,
-            LAUNCHER_2_BY_2_DB));
+            LAUNCHER_2_BY_2_DB,
+            LAUNCHER_7_BY_3_DB));
 
     public static final List<String> OTHER_FILES = Collections.unmodifiableList(Arrays.asList(
             BACKUP_DB,
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
deleted file mode 100644
index ca1b2a9..0000000
--- a/src/com/android/launcher3/LauncherModel.java
+++ /dev/null
@@ -1,691 +0,0 @@
-/*
- * Copyright (C) 2008 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3;
-
-import static android.app.admin.DevicePolicyManager.ACTION_DEVICE_POLICY_RESOURCE_UPDATED;
-
-import static com.android.launcher3.LauncherAppState.ACTION_FORCE_ROLOAD;
-import static com.android.launcher3.LauncherPrefs.WORK_EDU_STEP;
-import static com.android.launcher3.config.FeatureFlags.IS_STUDIO_BUILD;
-import static com.android.launcher3.icons.cache.BaseIconCache.EMPTY_CLASS_NAME;
-import static com.android.launcher3.model.PackageUpdatedTask.OP_UPDATE;
-import static com.android.launcher3.pm.UserCache.ACTION_PROFILE_AVAILABLE;
-import static com.android.launcher3.pm.UserCache.ACTION_PROFILE_UNAVAILABLE;
-import static com.android.launcher3.testing.shared.TestProtocol.sDebugTracing;
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
-
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageInstaller;
-import android.content.pm.ShortcutInfo;
-import android.os.UserHandle;
-import android.text.TextUtils;
-import android.util.Log;
-import android.util.Pair;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.WorkerThread;
-
-import com.android.launcher3.celllayout.CellPosMapper;
-import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.icons.IconCache;
-import com.android.launcher3.model.AddWorkspaceItemsTask;
-import com.android.launcher3.model.AllAppsList;
-import com.android.launcher3.model.BaseLauncherBinder;
-import com.android.launcher3.model.BgDataModel;
-import com.android.launcher3.model.BgDataModel.Callbacks;
-import com.android.launcher3.model.CacheDataUpdatedTask;
-import com.android.launcher3.model.ItemInstallQueue;
-import com.android.launcher3.model.LoaderTask;
-import com.android.launcher3.model.ModelDbController;
-import com.android.launcher3.model.ModelDelegate;
-import com.android.launcher3.model.ModelLauncherCallbacks;
-import com.android.launcher3.model.ModelTaskController;
-import com.android.launcher3.model.ModelWriter;
-import com.android.launcher3.model.PackageInstallStateChangedTask;
-import com.android.launcher3.model.PackageUpdatedTask;
-import com.android.launcher3.model.ReloadStringCacheTask;
-import com.android.launcher3.model.ShortcutsChangedTask;
-import com.android.launcher3.model.UserLockStateChangedTask;
-import com.android.launcher3.model.data.AppInfo;
-import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.model.data.WorkspaceItemInfo;
-import com.android.launcher3.pm.InstallSessionTracker;
-import com.android.launcher3.pm.PackageInstallInfo;
-import com.android.launcher3.pm.UserCache;
-import com.android.launcher3.shortcuts.ShortcutRequest;
-import com.android.launcher3.util.IntSet;
-import com.android.launcher3.util.ItemInfoMatcher;
-import com.android.launcher3.util.PackageManagerHelper;
-import com.android.launcher3.util.PackageUserKey;
-import com.android.launcher3.util.Preconditions;
-
-import java.io.FileDescriptor;
-import java.io.PrintWriter;
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-import java.util.concurrent.CancellationException;
-import java.util.function.Consumer;
-import java.util.function.Supplier;
-
-/**
- * Maintains in-memory state of the Launcher. It is expected that there should be only one
- * LauncherModel object held in a static. Also provide APIs for updating the database state
- * for the Launcher.
- */
-public class LauncherModel implements InstallSessionTracker.Callback {
-    private static final boolean DEBUG_RECEIVER = false;
-
-    static final String TAG = "Launcher.Model";
-
-    @NonNull
-    private final LauncherAppState mApp;
-    @NonNull
-    private final PackageManagerHelper mPmHelper;
-    @NonNull
-    private final ModelDbController mModelDbController;
-    @NonNull
-    private final Object mLock = new Object();
-    @Nullable
-    private LoaderTask mLoaderTask;
-    private boolean mIsLoaderTaskRunning;
-
-    // only allow this once per reboot to reload work apps
-    private boolean mShouldReloadWorkProfile = true;
-
-    // Indicates whether the current model data is valid or not.
-    // We start off with everything not loaded. After that, we assume that
-    // our monitoring of the package manager provides all updates and we never
-    // need to do a requery. This is only ever touched from the loader thread.
-    private boolean mModelLoaded;
-    private boolean mModelDestroyed = false;
-    public boolean isModelLoaded() {
-        synchronized (mLock) {
-            return mModelLoaded && mLoaderTask == null && !mModelDestroyed;
-        }
-    }
-
-    @NonNull
-    private final ArrayList<Callbacks> mCallbacksList = new ArrayList<>(1);
-
-    // < only access in worker thread >
-    @NonNull
-    private final AllAppsList mBgAllAppsList;
-
-    /**
-     * All the static data should be accessed on the background thread, A lock should be acquired
-     * on this object when accessing any data from this model.
-     */
-    @NonNull
-    private final BgDataModel mBgDataModel = new BgDataModel();
-
-    @NonNull
-    private final ModelDelegate mModelDelegate;
-
-    private int mLastLoadId = -1;
-
-    // Runnable to check if the shortcuts permission has changed.
-    @NonNull
-    private final Runnable mDataValidationCheck = new Runnable() {
-        @Override
-        public void run() {
-            if (mModelLoaded) {
-                mModelDelegate.validateData();
-            }
-        }
-    };
-
-    LauncherModel(@NonNull final Context context, @NonNull final LauncherAppState app,
-            @NonNull final IconCache iconCache, @NonNull final AppFilter appFilter,
-            @NonNull final PackageManagerHelper pmHelper, final boolean isPrimaryInstance) {
-        mApp = app;
-        mPmHelper = pmHelper;
-        mModelDbController = new ModelDbController(context);
-        mBgAllAppsList = new AllAppsList(iconCache, appFilter);
-        mModelDelegate = ModelDelegate.newInstance(context, app, mPmHelper, mBgAllAppsList,
-                mBgDataModel, isPrimaryInstance);
-    }
-
-    @NonNull
-    public ModelDelegate getModelDelegate() {
-        return mModelDelegate;
-    }
-
-    public ModelDbController getModelDbController() {
-        return mModelDbController;
-    }
-
-    public ModelLauncherCallbacks newModelCallbacks() {
-        return new ModelLauncherCallbacks(this::enqueueModelUpdateTask);
-    }
-
-    /**
-     * Adds the provided items to the workspace.
-     */
-    public void addAndBindAddedWorkspaceItems(
-            @NonNull final List<Pair<ItemInfo, Object>> itemList) {
-        for (Callbacks cb : getCallbacks()) {
-            cb.preAddApps();
-        }
-        enqueueModelUpdateTask(new AddWorkspaceItemsTask(itemList));
-    }
-
-    @NonNull
-    public ModelWriter getWriter(final boolean verifyChanges, CellPosMapper cellPosMapper,
-            @Nullable final Callbacks owner) {
-        return new ModelWriter(mApp.getContext(), this, mBgDataModel, verifyChanges, cellPosMapper,
-                owner);
-    }
-
-    /**
-     * Called when the icon for an app changes, outside of package event
-     */
-    @WorkerThread
-    public void onAppIconChanged(@NonNull final String packageName,
-            @NonNull final UserHandle user) {
-        // Update the icon for the calendar package
-        Context context = mApp.getContext();
-        enqueueModelUpdateTask(new PackageUpdatedTask(OP_UPDATE, user, packageName));
-
-        List<ShortcutInfo> pinnedShortcuts = new ShortcutRequest(context, user)
-                .forPackage(packageName).query(ShortcutRequest.PINNED);
-        if (!pinnedShortcuts.isEmpty()) {
-            enqueueModelUpdateTask(new ShortcutsChangedTask(packageName, pinnedShortcuts, user,
-                    false));
-        }
-    }
-
-    /**
-     * Called when the workspace items have drastically changed
-     */
-    public void onWorkspaceUiChanged() {
-        MODEL_EXECUTOR.execute(mModelDelegate::workspaceLoadComplete);
-    }
-
-    /**
-     * Called when the model is destroyed
-     */
-    public void destroy() {
-        mModelDestroyed = true;
-        MODEL_EXECUTOR.execute(mModelDelegate::destroy);
-    }
-
-    public void onBroadcastIntent(@NonNull final Intent intent) {
-        if (DEBUG_RECEIVER || sDebugTracing) Log.d(TAG, "onReceive intent=" + intent);
-        final String action = intent.getAction();
-        if (Intent.ACTION_LOCALE_CHANGED.equals(action)) {
-            // If we have changed locale we need to clear out the labels in all apps/workspace.
-            forceReload();
-        } else if (ACTION_DEVICE_POLICY_RESOURCE_UPDATED.equals(action)) {
-            enqueueModelUpdateTask(new ReloadStringCacheTask(mModelDelegate));
-        } else if (IS_STUDIO_BUILD && ACTION_FORCE_ROLOAD.equals(action)) {
-            for (Callbacks cb : getCallbacks()) {
-                if (cb instanceof Launcher) {
-                    ((Launcher) cb).recreate();
-                }
-            }
-        }
-    }
-
-    /**
-     * Called then there use a user event
-     * @see UserCache#addUserEventListener
-     */
-    public void onUserEvent(UserHandle user, String action) {
-        if (Intent.ACTION_MANAGED_PROFILE_AVAILABLE.equals(action)
-                && mShouldReloadWorkProfile) {
-            mShouldReloadWorkProfile = false;
-            forceReload();
-        } else if (Intent.ACTION_MANAGED_PROFILE_AVAILABLE.equals(action)
-                || Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE.equals(action)) {
-            mShouldReloadWorkProfile = false;
-            enqueueModelUpdateTask(new PackageUpdatedTask(
-                    PackageUpdatedTask.OP_USER_AVAILABILITY_CHANGE, user));
-        } else if (UserCache.ACTION_PROFILE_LOCKED.equals(action)
-                || UserCache.ACTION_PROFILE_UNLOCKED.equals(action)) {
-            enqueueModelUpdateTask(new UserLockStateChangedTask(
-                    user, UserCache.ACTION_PROFILE_UNLOCKED.equals(action)));
-        } else if (UserCache.ACTION_PROFILE_ADDED.equals(action)
-                || UserCache.ACTION_PROFILE_REMOVED.equals(action)) {
-            forceReload();
-        } else if (ACTION_PROFILE_AVAILABLE.equals(action)
-                || ACTION_PROFILE_UNAVAILABLE.equals(action)) {
-            /*
-             * This broadcast is only available when android.os.Flags.allowPrivateProfile() is set.
-             * For Work-profile this broadcast will be sent in addition to
-             * ACTION_MANAGED_PROFILE_AVAILABLE/UNAVAILABLE.
-             * So effectively, this if block only handles the non-work profile case.
-             */
-            enqueueModelUpdateTask(new PackageUpdatedTask(
-                    PackageUpdatedTask.OP_USER_AVAILABILITY_CHANGE, user));
-        }
-        if (Intent.ACTION_MANAGED_PROFILE_REMOVED.equals(action)) {
-            LauncherPrefs.get(mApp.getContext()).put(WORK_EDU_STEP, 0);
-        }
-    }
-
-    /**
-     * Reloads the workspace items from the DB and re-binds the workspace. This should generally
-     * not be called as DB updates are automatically followed by UI update
-     */
-    public void forceReload() {
-        synchronized (mLock) {
-            // Stop any existing loaders first, so they don't set mModelLoaded to true later
-            stopLoader();
-            mModelLoaded = false;
-        }
-
-        // Start the loader if launcher is already running, otherwise the loader will run,
-        // the next time launcher starts
-        if (hasCallbacks()) {
-            startLoader();
-        }
-    }
-
-    /**
-     * Rebinds all existing callbacks with already loaded model
-     */
-    public void rebindCallbacks() {
-        if (hasCallbacks()) {
-            startLoader();
-        }
-    }
-
-    /**
-     * Removes an existing callback
-     */
-    public void removeCallbacks(@NonNull final Callbacks callbacks) {
-        synchronized (mCallbacksList) {
-            Preconditions.assertUIThread();
-            if (mCallbacksList.remove(callbacks)) {
-                if (stopLoader()) {
-                    // Rebind existing callbacks
-                    startLoader();
-                }
-            }
-        }
-    }
-
-    /**
-     * Adds a callbacks to receive model updates
-     * @return true if workspace load was performed synchronously
-     */
-    public boolean addCallbacksAndLoad(@NonNull final Callbacks callbacks) {
-        synchronized (mLock) {
-            addCallbacks(callbacks);
-            return startLoader(new Callbacks[] { callbacks });
-
-        }
-    }
-
-    /**
-     * Adds a callbacks to receive model updates
-     */
-    public void addCallbacks(@NonNull final Callbacks callbacks) {
-        Preconditions.assertUIThread();
-        synchronized (mCallbacksList) {
-            mCallbacksList.add(callbacks);
-        }
-    }
-
-    /**
-     * Starts the loader. Tries to bind {@params synchronousBindPage} synchronously if possible.
-     * @return true if the page could be bound synchronously.
-     */
-    public boolean startLoader() {
-        return startLoader(new Callbacks[0]);
-    }
-
-    private boolean startLoader(@NonNull final Callbacks[] newCallbacks) {
-        // Enable queue before starting loader. It will get disabled in Launcher#finishBindingItems
-        ItemInstallQueue.INSTANCE.get(mApp.getContext())
-                .pauseModelPush(ItemInstallQueue.FLAG_LOADER_RUNNING);
-        synchronized (mLock) {
-            // If there is already one running, tell it to stop.
-            boolean wasRunning = stopLoader();
-            boolean bindDirectly = mModelLoaded && !mIsLoaderTaskRunning;
-            boolean bindAllCallbacks = wasRunning || !bindDirectly || newCallbacks.length == 0;
-            final Callbacks[] callbacksList = bindAllCallbacks ? getCallbacks() : newCallbacks;
-
-            if (callbacksList.length > 0) {
-                // Clear any pending bind-runnables from the synchronized load process.
-                for (Callbacks cb : callbacksList) {
-                    MAIN_EXECUTOR.execute(cb::clearPendingBinds);
-                }
-
-                BaseLauncherBinder launcherBinder = new BaseLauncherBinder(
-                        mApp, mBgDataModel, mBgAllAppsList, callbacksList);
-                if (bindDirectly) {
-                    // Divide the set of loaded items into those that we are binding synchronously,
-                    // and everything else that is to be bound normally (asynchronously).
-                    launcherBinder.bindWorkspace(bindAllCallbacks, /* isBindSync= */ true);
-                    // For now, continue posting the binding of AllApps as there are other
-                    // issues that arise from that.
-                    launcherBinder.bindAllApps();
-                    launcherBinder.bindDeepShortcuts();
-                    launcherBinder.bindWidgets();
-                    if (FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) {
-                        mModelDelegate.bindAllModelExtras(callbacksList);
-                    }
-                    return true;
-                } else {
-                    stopLoader();
-                    mLoaderTask = new LoaderTask(
-                            mApp, mBgAllAppsList, mBgDataModel, mModelDelegate, launcherBinder);
-
-                    // Always post the loader task, instead of running directly
-                    // (even on same thread) so that we exit any nested synchronized blocks
-                    MODEL_EXECUTOR.post(mLoaderTask);
-                }
-            }
-        }
-        return false;
-    }
-
-    /**
-     * If there is already a loader task running, tell it to stop.
-     * @return true if an existing loader was stopped.
-     */
-    private boolean stopLoader() {
-        synchronized (mLock) {
-            LoaderTask oldTask = mLoaderTask;
-            mLoaderTask = null;
-            if (oldTask != null) {
-                oldTask.stopLocked();
-                return true;
-            }
-            return false;
-        }
-    }
-
-    /**
-     * Loads the model if not loaded
-     * @param callback called with the data model upon successful load or null on model thread.
-     */
-    public void loadAsync(@NonNull final Consumer<BgDataModel> callback) {
-        synchronized (mLock) {
-            if (!mModelLoaded && !mIsLoaderTaskRunning) {
-                startLoader();
-            }
-        }
-        MODEL_EXECUTOR.post(() -> callback.accept(isModelLoaded() ? mBgDataModel : null));
-    }
-
-    @Override
-    public void onInstallSessionCreated(@NonNull final PackageInstallInfo sessionInfo) {
-        if (FeatureFlags.PROMISE_APPS_IN_ALL_APPS.get()) {
-            enqueueModelUpdateTask((taskController, dataModel, apps) -> {
-                apps.addPromiseApp(mApp.getContext(), sessionInfo);
-                taskController.bindApplicationsIfNeeded();
-            });
-        }
-    }
-
-    @Override
-    public void onSessionFailure(@NonNull final String packageName,
-            @NonNull final UserHandle user) {
-        enqueueModelUpdateTask((taskController, dataModel, apps) -> {
-            IconCache iconCache = mApp.getIconCache();
-            final IntSet removedIds = new IntSet();
-            HashSet<WorkspaceItemInfo> archivedWorkspaceItemsToCacheRefresh = new HashSet<>();
-            boolean isAppArchived = PackageManagerHelper.INSTANCE.get(mApp.getContext())
-                    .isAppArchivedForUser(packageName, user);
-            synchronized (dataModel) {
-                if (isAppArchived) {
-                    // Remove package icon cache entry for archived app in case of a session
-                    // failure.
-                    mApp.getIconCache().remove(
-                            new ComponentName(packageName, packageName + EMPTY_CLASS_NAME),
-                            user);
-                }
-
-                for (ItemInfo info : dataModel.itemsIdMap) {
-                    if (info instanceof WorkspaceItemInfo
-                            && ((WorkspaceItemInfo) info).hasPromiseIconUi()
-                            && user.equals(info.user)
-                            && info.getIntent() != null) {
-                        if (TextUtils.equals(packageName, info.getIntent().getPackage())) {
-                            removedIds.add(info.id);
-                        }
-                        if (((WorkspaceItemInfo) info).isArchived()) {
-                            WorkspaceItemInfo workspaceItem = (WorkspaceItemInfo) info;
-                            // Refresh icons on the workspace for archived apps.
-                            iconCache.getTitleAndIcon(workspaceItem,
-                                    workspaceItem.usingLowResIcon());
-                            archivedWorkspaceItemsToCacheRefresh.add(workspaceItem);
-                        }
-                    }
-                }
-
-                if (isAppArchived) {
-                    apps.updateIconsAndLabels(new HashSet<>(List.of(packageName)), user);
-                }
-            }
-
-            if (!removedIds.isEmpty() && !isAppArchived) {
-                taskController.deleteAndBindComponentsRemoved(
-                        ItemInfoMatcher.ofItemIds(removedIds),
-                        "removed because install session failed");
-            }
-            if (!archivedWorkspaceItemsToCacheRefresh.isEmpty()) {
-                taskController.bindUpdatedWorkspaceItems(
-                        archivedWorkspaceItemsToCacheRefresh.stream().toList());
-            }
-            if (isAppArchived) {
-                taskController.bindApplicationsIfNeeded();
-            }
-        });
-    }
-
-    @Override
-    public void onPackageStateChanged(@NonNull final PackageInstallInfo installInfo) {
-        enqueueModelUpdateTask(new PackageInstallStateChangedTask(installInfo));
-    }
-
-    /**
-     * Updates the icons and label of all pending icons for the provided package name.
-     */
-    @Override
-    public void onUpdateSessionDisplay(@NonNull final PackageUserKey key,
-            @NonNull final PackageInstaller.SessionInfo info) {
-        mApp.getIconCache().updateSessionCache(key, info);
-
-        HashSet<String> packages = new HashSet<>();
-        packages.add(key.mPackageName);
-        enqueueModelUpdateTask(new CacheDataUpdatedTask(
-                CacheDataUpdatedTask.OP_SESSION_UPDATE, key.mUser, packages));
-    }
-
-    public class LoaderTransaction implements AutoCloseable {
-
-        @NonNull
-        private final LoaderTask mTask;
-
-        private LoaderTransaction(@NonNull final LoaderTask task) throws CancellationException {
-            synchronized (mLock) {
-                if (mLoaderTask != task) {
-                    throw new CancellationException("Loader already stopped");
-                }
-                mLastLoadId++;
-                mTask = task;
-                mIsLoaderTaskRunning = true;
-                mModelLoaded = false;
-            }
-        }
-
-        public void commit() {
-            synchronized (mLock) {
-                // Everything loaded bind the data.
-                mModelLoaded = true;
-            }
-        }
-
-        @Override
-        public void close() {
-            synchronized (mLock) {
-                // If we are still the last one to be scheduled, remove ourselves.
-                if (mLoaderTask == mTask) {
-                    mLoaderTask = null;
-                }
-                mIsLoaderTaskRunning = false;
-            }
-        }
-    }
-
-    public LoaderTransaction beginLoader(@NonNull final LoaderTask task)
-            throws CancellationException {
-        return new LoaderTransaction(task);
-    }
-
-    /**
-     * Refreshes the cached shortcuts if the shortcut permission has changed.
-     * Current implementation simply reloads the workspace, but it can be optimized to
-     * use partial updates similar to {@link UserCache}
-     */
-    public void validateModelDataOnResume() {
-        MODEL_EXECUTOR.getHandler().removeCallbacks(mDataValidationCheck);
-        MODEL_EXECUTOR.post(mDataValidationCheck);
-    }
-
-    /**
-     * Called when the icons for packages have been updated in the icon cache.
-     */
-    public void onPackageIconsUpdated(@NonNull final HashSet<String> updatedPackages,
-            @NonNull final UserHandle user) {
-        // If any package icon has changed (app was updated while launcher was dead),
-        // update the corresponding shortcuts.
-        enqueueModelUpdateTask(new CacheDataUpdatedTask(
-                CacheDataUpdatedTask.OP_CACHE_UPDATE, user, updatedPackages));
-    }
-
-    /**
-     * Called when the labels for the widgets has updated in the icon cache.
-     */
-    public void onWidgetLabelsUpdated(@NonNull final HashSet<String> updatedPackages,
-            @NonNull final UserHandle user) {
-        enqueueModelUpdateTask((taskController, dataModel, apps) ->  {
-            dataModel.widgetsModel.onPackageIconsUpdated(updatedPackages, user, mApp);
-            taskController.bindUpdatedWidgets(dataModel);
-        });
-    }
-
-    public void enqueueModelUpdateTask(@NonNull final ModelUpdateTask task) {
-        if (mModelDestroyed) {
-            return;
-        }
-        MODEL_EXECUTOR.execute(() -> {
-            if (!isModelLoaded()) {
-                // Loader has not yet run.
-                return;
-            }
-            ModelTaskController controller = new ModelTaskController(
-                    mApp, mBgDataModel, mBgAllAppsList, this, MAIN_EXECUTOR);
-            task.execute(controller, mBgDataModel, mBgAllAppsList);
-        });
-    }
-
-    /**
-     * A task to be executed on the current callbacks on the UI thread.
-     * If there is no current callbacks, the task is ignored.
-     */
-    public interface CallbackTask {
-
-        void execute(@NonNull Callbacks callbacks);
-    }
-
-    public interface ModelUpdateTask {
-
-        void execute(@NonNull ModelTaskController taskController,
-                @NonNull BgDataModel dataModel, @NonNull AllAppsList apps);
-    }
-
-    public void updateAndBindWorkspaceItem(@NonNull final WorkspaceItemInfo si,
-            @NonNull final ShortcutInfo info) {
-        updateAndBindWorkspaceItem(() -> {
-            si.updateFromDeepShortcutInfo(info, mApp.getContext());
-            mApp.getIconCache().getShortcutIcon(si, info);
-            return si;
-        });
-    }
-
-    /**
-     * Utility method to update a shortcut on the background thread.
-     */
-    public void updateAndBindWorkspaceItem(
-            @NonNull final Supplier<WorkspaceItemInfo> itemProvider) {
-        enqueueModelUpdateTask((taskController, dataModel, apps) ->  {
-            WorkspaceItemInfo info = itemProvider.get();
-            taskController.getModelWriter().updateItemInDatabase(info);
-            ArrayList<WorkspaceItemInfo> update = new ArrayList<>();
-            update.add(info);
-            taskController.bindUpdatedWorkspaceItems(update);
-        });
-    }
-
-    public void refreshAndBindWidgetsAndShortcuts(@Nullable final PackageUserKey packageUser) {
-        enqueueModelUpdateTask((taskController, dataModel, apps) ->  {
-            dataModel.widgetsModel.update(taskController.getApp(), packageUser);
-            taskController.bindUpdatedWidgets(dataModel);
-        });
-    }
-
-    public void dumpState(@Nullable final String prefix, @Nullable final FileDescriptor fd,
-            @NonNull final PrintWriter writer, @NonNull final String[] args) {
-        if (args.length > 0 && TextUtils.equals(args[0], "--all")) {
-            writer.println(prefix + "All apps list: size=" + mBgAllAppsList.data.size());
-            for (AppInfo info : mBgAllAppsList.data) {
-                writer.println(prefix + "   title=\"" + info.title
-                        + "\" bitmapIcon=" + info.bitmap.icon
-                        + " componentName=" + info.componentName.getPackageName());
-            }
-            writer.println();
-        }
-        mModelDelegate.dump(prefix, fd, writer, args);
-        mBgDataModel.dump(prefix, fd, writer, args);
-    }
-
-    /**
-     * Returns true if there are any callbacks attached to the model
-     */
-    public boolean hasCallbacks() {
-        synchronized (mCallbacksList) {
-            return !mCallbacksList.isEmpty();
-        }
-    }
-
-    /**
-     * Returns an array of currently attached callbacks
-     */
-    @NonNull
-    public Callbacks[] getCallbacks() {
-        synchronized (mCallbacksList) {
-            return mCallbacksList.toArray(new Callbacks[mCallbacksList.size()]);
-        }
-    }
-
-    /**
-     * Returns the ID for the last model load. If the load ID doesn't match for a transaction, the
-     * transaction should be ignored.
-     */
-    public int getLastLoadId() {
-        return mLastLoadId;
-    }
-}
diff --git a/src/com/android/launcher3/LauncherModel.kt b/src/com/android/launcher3/LauncherModel.kt
new file mode 100644
index 0000000..b56df46
--- /dev/null
+++ b/src/com/android/launcher3/LauncherModel.kt
@@ -0,0 +1,514 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3
+
+import android.app.admin.DevicePolicyManager
+import android.content.Context
+import android.content.Intent
+import android.content.pm.ShortcutInfo
+import android.os.UserHandle
+import android.text.TextUtils
+import android.util.Log
+import android.util.Pair
+import androidx.annotation.WorkerThread
+import com.android.launcher3.celllayout.CellPosMapper
+import com.android.launcher3.icons.IconCache
+import com.android.launcher3.model.AddWorkspaceItemsTask
+import com.android.launcher3.model.AllAppsList
+import com.android.launcher3.model.BaseLauncherBinder
+import com.android.launcher3.model.BgDataModel
+import com.android.launcher3.model.CacheDataUpdatedTask
+import com.android.launcher3.model.ItemInstallQueue
+import com.android.launcher3.model.LoaderTask
+import com.android.launcher3.model.ModelDbController
+import com.android.launcher3.model.ModelDelegate
+import com.android.launcher3.model.ModelLauncherCallbacks
+import com.android.launcher3.model.ModelTaskController
+import com.android.launcher3.model.ModelWriter
+import com.android.launcher3.model.PackageUpdatedTask
+import com.android.launcher3.model.ReloadStringCacheTask
+import com.android.launcher3.model.ShortcutsChangedTask
+import com.android.launcher3.model.UserLockStateChangedTask
+import com.android.launcher3.model.WidgetsFilterDataProvider
+import com.android.launcher3.model.data.ItemInfo
+import com.android.launcher3.model.data.WorkspaceItemInfo
+import com.android.launcher3.pm.UserCache
+import com.android.launcher3.shortcuts.ShortcutRequest
+import com.android.launcher3.testing.shared.TestProtocol.sDebugTracing
+import com.android.launcher3.util.Executors.MAIN_EXECUTOR
+import com.android.launcher3.util.Executors.MODEL_EXECUTOR
+import com.android.launcher3.util.PackageManagerHelper
+import com.android.launcher3.util.PackageUserKey
+import com.android.launcher3.util.Preconditions
+import java.io.FileDescriptor
+import java.io.PrintWriter
+import java.util.concurrent.CancellationException
+import java.util.function.Consumer
+
+/**
+ * Maintains in-memory state of the Launcher. It is expected that there should be only one
+ * LauncherModel object held in a static. Also provide APIs for updating the database state for the
+ * Launcher.
+ */
+class LauncherModel(
+    private val context: Context,
+    private val mApp: LauncherAppState,
+    private val iconCache: IconCache,
+    private val widgetsFilterDataProvider: WidgetsFilterDataProvider,
+    appFilter: AppFilter,
+    mPmHelper: PackageManagerHelper,
+    isPrimaryInstance: Boolean,
+) {
+
+    private val mCallbacksList = ArrayList<BgDataModel.Callbacks>(1)
+
+    // < only access in worker thread >
+    private val mBgAllAppsList = AllAppsList(iconCache, appFilter)
+
+    /**
+     * All the static data should be accessed on the background thread, A lock should be acquired on
+     * this object when accessing any data from this model.
+     */
+    private val mBgDataModel = BgDataModel()
+
+    val modelDelegate: ModelDelegate =
+        ModelDelegate.newInstance(
+            context,
+            mApp,
+            mPmHelper,
+            mBgAllAppsList,
+            mBgDataModel,
+            isPrimaryInstance,
+        )
+
+    val modelDbController = ModelDbController(context)
+
+    private val mLock = Any()
+
+    private var mLoaderTask: LoaderTask? = null
+    private var mIsLoaderTaskRunning = false
+
+    // only allow this once per reboot to reload work apps
+    private var mShouldReloadWorkProfile = true
+
+    // Indicates whether the current model data is valid or not.
+    // We start off with everything not loaded. After that, we assume that
+    // our monitoring of the package manager provides all updates and we never
+    // need to do a requery. This is only ever touched from the loader thread.
+    private var mModelLoaded = false
+    private var mModelDestroyed = false
+
+    fun isModelLoaded() =
+        synchronized(mLock) { mModelLoaded && mLoaderTask == null && !mModelDestroyed }
+
+    /**
+     * Returns the ID for the last model load. If the load ID doesn't match for a transaction, the
+     * transaction should be ignored.
+     */
+    var lastLoadId: Int = -1
+        private set
+
+    // Runnable to check if the shortcuts permission has changed.
+    private val mDataValidationCheck = Runnable {
+        if (mModelLoaded) {
+            modelDelegate.validateData()
+        }
+    }
+
+    fun newModelCallbacks() = ModelLauncherCallbacks(this::enqueueModelUpdateTask)
+
+    /** Adds the provided items to the workspace. */
+    fun addAndBindAddedWorkspaceItems(itemList: List<Pair<ItemInfo?, Any?>?>) {
+        callbacks.forEach { it.preAddApps() }
+        enqueueModelUpdateTask(AddWorkspaceItemsTask(itemList))
+    }
+
+    fun getWriter(
+        verifyChanges: Boolean,
+        cellPosMapper: CellPosMapper?,
+        owner: BgDataModel.Callbacks?,
+    ) = ModelWriter(mApp.context, this, mBgDataModel, verifyChanges, cellPosMapper, owner)
+
+    /** Returns the [WidgetsFilterDataProvider] that manages widget filters. */
+    fun getWidgetsFilterDataProvider(): WidgetsFilterDataProvider {
+        return widgetsFilterDataProvider
+    }
+
+    /** Called when the icon for an app changes, outside of package event */
+    @WorkerThread
+    fun onAppIconChanged(packageName: String, user: UserHandle) {
+        // Update the icon for the calendar package
+        enqueueModelUpdateTask(PackageUpdatedTask(PackageUpdatedTask.OP_UPDATE, user, packageName))
+        ShortcutRequest(context, user).forPackage(packageName).query(ShortcutRequest.PINNED).let {
+            if (it.isNotEmpty()) {
+                enqueueModelUpdateTask(ShortcutsChangedTask(packageName, it, user, false))
+            }
+        }
+    }
+
+    /** Called when the workspace items have drastically changed */
+    fun onWorkspaceUiChanged() {
+        MODEL_EXECUTOR.execute(modelDelegate::workspaceLoadComplete)
+    }
+
+    /** Called when the model is destroyed */
+    fun destroy() {
+        mModelDestroyed = true
+        MODEL_EXECUTOR.execute {
+            modelDelegate.destroy()
+            widgetsFilterDataProvider.destroy()
+        }
+    }
+
+    fun onBroadcastIntent(intent: Intent) {
+        if (DEBUG_RECEIVER || sDebugTracing) Log.d(TAG, "onReceive intent=$intent")
+        when (intent.action) {
+            Intent.ACTION_LOCALE_CHANGED,
+            LauncherAppState.ACTION_FORCE_ROLOAD ->
+                // If we have changed locale we need to clear out the labels in all apps/workspace.
+                forceReload()
+            DevicePolicyManager.ACTION_DEVICE_POLICY_RESOURCE_UPDATED ->
+                enqueueModelUpdateTask(ReloadStringCacheTask(this.modelDelegate))
+        }
+    }
+
+    /**
+     * Called then there use a user event
+     *
+     * @see UserCache.addUserEventListener
+     */
+    fun onUserEvent(user: UserHandle, action: String) {
+        when (action) {
+            Intent.ACTION_MANAGED_PROFILE_AVAILABLE -> {
+                if (mShouldReloadWorkProfile) {
+                    forceReload()
+                } else {
+                    enqueueModelUpdateTask(
+                        PackageUpdatedTask(PackageUpdatedTask.OP_USER_AVAILABILITY_CHANGE, user)
+                    )
+                }
+                mShouldReloadWorkProfile = false
+            }
+            Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE -> {
+                mShouldReloadWorkProfile = false
+                enqueueModelUpdateTask(
+                    PackageUpdatedTask(PackageUpdatedTask.OP_USER_AVAILABILITY_CHANGE, user)
+                )
+            }
+            UserCache.ACTION_PROFILE_LOCKED ->
+                enqueueModelUpdateTask(UserLockStateChangedTask(user, false))
+            UserCache.ACTION_PROFILE_UNLOCKED ->
+                enqueueModelUpdateTask(UserLockStateChangedTask(user, true))
+            Intent.ACTION_MANAGED_PROFILE_REMOVED -> {
+                LauncherPrefs.get(mApp.context).put(LauncherPrefs.WORK_EDU_STEP, 0)
+                forceReload()
+            }
+            UserCache.ACTION_PROFILE_ADDED,
+            UserCache.ACTION_PROFILE_REMOVED -> forceReload()
+            UserCache.ACTION_PROFILE_AVAILABLE,
+            UserCache.ACTION_PROFILE_UNAVAILABLE -> {
+                // This broadcast is only available when android.os.Flags.allowPrivateProfile() is
+                // set. For Work-profile this broadcast will be sent in addition to
+                // ACTION_MANAGED_PROFILE_AVAILABLE/UNAVAILABLE. So effectively, this if block only
+                // handles the non-work profile case.
+                enqueueModelUpdateTask(
+                    PackageUpdatedTask(PackageUpdatedTask.OP_USER_AVAILABILITY_CHANGE, user)
+                )
+            }
+        }
+    }
+
+    /**
+     * Reloads the workspace items from the DB and re-binds the workspace. This should generally not
+     * be called as DB updates are automatically followed by UI update
+     */
+    fun forceReload() {
+        synchronized(mLock) {
+            // Stop any existing loaders first, so they don't set mModelLoaded to true later
+            stopLoader()
+            mModelLoaded = false
+        }
+        rebindCallbacks()
+    }
+
+    /** Rebinds all existing callbacks with already loaded model */
+    fun rebindCallbacks() {
+        if (hasCallbacks()) {
+            startLoader()
+        }
+    }
+
+    /** Removes an existing callback */
+    fun removeCallbacks(callbacks: BgDataModel.Callbacks) {
+        synchronized(mCallbacksList) {
+            Preconditions.assertUIThread()
+            if (mCallbacksList.remove(callbacks)) {
+                if (stopLoader()) {
+                    // Rebind existing callbacks
+                    startLoader()
+                }
+            }
+        }
+    }
+
+    /**
+     * Adds a callbacks to receive model updates
+     *
+     * @return true if workspace load was performed synchronously
+     */
+    fun addCallbacksAndLoad(callbacks: BgDataModel.Callbacks): Boolean {
+        synchronized(mLock) {
+            addCallbacks(callbacks)
+            return startLoader(arrayOf(callbacks))
+        }
+    }
+
+    /** Adds a callbacks to receive model updates */
+    fun addCallbacks(callbacks: BgDataModel.Callbacks) {
+        Preconditions.assertUIThread()
+        synchronized(mCallbacksList) { mCallbacksList.add(callbacks) }
+    }
+
+    /**
+     * Starts the loader. Tries to bind {@params synchronousBindPage} synchronously if possible.
+     *
+     * @return true if the page could be bound synchronously.
+     */
+    fun startLoader() = startLoader(arrayOf())
+
+    private fun startLoader(newCallbacks: Array<BgDataModel.Callbacks>): Boolean {
+        // Enable queue before starting loader. It will get disabled in Launcher#finishBindingItems
+        ItemInstallQueue.INSTANCE.get(context).pauseModelPush(ItemInstallQueue.FLAG_LOADER_RUNNING)
+        synchronized(mLock) {
+            // If there is already one running, tell it to stop.
+            val wasRunning = stopLoader()
+            val bindDirectly = mModelLoaded && !mIsLoaderTaskRunning
+            val bindAllCallbacks = wasRunning || !bindDirectly || newCallbacks.isEmpty()
+            val callbacksList = if (bindAllCallbacks) callbacks else newCallbacks
+            if (callbacksList.isNotEmpty()) {
+                // Clear any pending bind-runnables from the synchronized load process.
+                callbacksList.forEach { MAIN_EXECUTOR.execute(it::clearPendingBinds) }
+
+                val launcherBinder =
+                    BaseLauncherBinder(mApp, mBgDataModel, mBgAllAppsList, callbacksList)
+                if (bindDirectly) {
+                    // Divide the set of loaded items into those that we are binding synchronously,
+                    // and everything else that is to be bound normally (asynchronously).
+                    launcherBinder.bindWorkspace(bindAllCallbacks, /* isBindSync= */ true)
+                    // For now, continue posting the binding of AllApps as there are other
+                    // issues that arise from that.
+                    launcherBinder.bindAllApps()
+                    launcherBinder.bindDeepShortcuts()
+                    launcherBinder.bindWidgets()
+                    return true
+                } else {
+                    mLoaderTask =
+                        LoaderTask(
+                            mApp,
+                            mBgAllAppsList,
+                            mBgDataModel,
+                            this.modelDelegate,
+                            launcherBinder,
+                            widgetsFilterDataProvider,
+                        )
+
+                    // Always post the loader task, instead of running directly
+                    // (even on same thread) so that we exit any nested synchronized blocks
+                    MODEL_EXECUTOR.post(mLoaderTask)
+                }
+            }
+        }
+        return false
+    }
+
+    /**
+     * If there is already a loader task running, tell it to stop.
+     *
+     * @return true if an existing loader was stopped.
+     */
+    private fun stopLoader(): Boolean {
+        synchronized(mLock) {
+            val oldTask: LoaderTask? = mLoaderTask
+            mLoaderTask = null
+            if (oldTask != null) {
+                oldTask.stopLocked()
+                return true
+            }
+            return false
+        }
+    }
+
+    /**
+     * Loads the model if not loaded
+     *
+     * @param callback called with the data model upon successful load or null on model thread.
+     */
+    fun loadAsync(callback: Consumer<BgDataModel?>) {
+        synchronized(mLock) {
+            if (!mModelLoaded && !mIsLoaderTaskRunning) {
+                startLoader()
+            }
+        }
+        MODEL_EXECUTOR.post { callback.accept(if (isModelLoaded()) mBgDataModel else null) }
+    }
+
+    inner class LoaderTransaction(task: LoaderTask) : AutoCloseable {
+        private var mTask: LoaderTask? = null
+
+        init {
+            synchronized(mLock) {
+                if (mLoaderTask !== task) {
+                    throw CancellationException("Loader already stopped")
+                }
+                this@LauncherModel.lastLoadId++
+                mTask = task
+                mIsLoaderTaskRunning = true
+                mModelLoaded = false
+            }
+        }
+
+        fun commit() {
+            synchronized(mLock) {
+                // Everything loaded bind the data.
+                mModelLoaded = true
+            }
+        }
+
+        override fun close() {
+            synchronized(mLock) {
+                // If we are still the last one to be scheduled, remove ourselves.
+                if (mLoaderTask === mTask) {
+                    mLoaderTask = null
+                }
+                mIsLoaderTaskRunning = false
+            }
+        }
+    }
+
+    @Throws(CancellationException::class)
+    fun beginLoader(task: LoaderTask) = LoaderTransaction(task)
+
+    /**
+     * Refreshes the cached shortcuts if the shortcut permission has changed. Current implementation
+     * simply reloads the workspace, but it can be optimized to use partial updates similar to
+     * [UserCache]
+     */
+    fun validateModelDataOnResume() {
+        MODEL_EXECUTOR.handler.removeCallbacks(mDataValidationCheck)
+        MODEL_EXECUTOR.post(mDataValidationCheck)
+    }
+
+    /** Called when the icons for packages have been updated in the icon cache. */
+    fun onPackageIconsUpdated(updatedPackages: HashSet<String?>, user: UserHandle) {
+        // If any package icon has changed (app was updated while launcher was dead),
+        // update the corresponding shortcuts.
+        enqueueModelUpdateTask(
+            CacheDataUpdatedTask(CacheDataUpdatedTask.OP_CACHE_UPDATE, user, updatedPackages)
+        )
+    }
+
+    /** Called when the labels for the widgets has updated in the icon cache. */
+    fun onWidgetLabelsUpdated(updatedPackages: HashSet<String?>, user: UserHandle) {
+        enqueueModelUpdateTask { taskController, dataModel, _ ->
+            dataModel.widgetsModel.onPackageIconsUpdated(updatedPackages, user, mApp)
+            taskController.bindUpdatedWidgets(dataModel)
+        }
+    }
+
+    /** Called when the widget filters are refreshed and available to bind to the model. */
+    fun onWidgetFiltersLoaded() {
+        enqueueModelUpdateTask { taskController, dataModel, _ ->
+            dataModel.widgetsModel.updateWidgetFilters(widgetsFilterDataProvider)
+            taskController.bindUpdatedWidgets(dataModel)
+        }
+    }
+
+    fun enqueueModelUpdateTask(task: ModelUpdateTask) {
+        if (mModelDestroyed) {
+            return
+        }
+        MODEL_EXECUTOR.execute {
+            if (!isModelLoaded()) {
+                // Loader has not yet run.
+                return@execute
+            }
+            task.execute(
+                ModelTaskController(mApp, mBgDataModel, mBgAllAppsList, this, MAIN_EXECUTOR),
+                mBgDataModel,
+                mBgAllAppsList,
+            )
+        }
+    }
+
+    /**
+     * A task to be executed on the current callbacks on the UI thread. If there is no current
+     * callbacks, the task is ignored.
+     */
+    fun interface CallbackTask {
+        fun execute(callbacks: BgDataModel.Callbacks)
+    }
+
+    fun interface ModelUpdateTask {
+        fun execute(taskController: ModelTaskController, dataModel: BgDataModel, apps: AllAppsList)
+    }
+
+    fun updateAndBindWorkspaceItem(si: WorkspaceItemInfo, info: ShortcutInfo) {
+        enqueueModelUpdateTask { taskController, _, _ ->
+            si.updateFromDeepShortcutInfo(info, context)
+            iconCache.getShortcutIcon(si, info)
+            taskController.getModelWriter().updateItemInDatabase(si)
+            taskController.bindUpdatedWorkspaceItems(listOf(si))
+        }
+    }
+
+    fun refreshAndBindWidgetsAndShortcuts(packageUser: PackageUserKey?) {
+        enqueueModelUpdateTask { taskController, dataModel, _ ->
+            dataModel.widgetsModel.update(taskController.app, packageUser)
+            taskController.bindUpdatedWidgets(dataModel)
+        }
+    }
+
+    fun dumpState(prefix: String?, fd: FileDescriptor?, writer: PrintWriter, args: Array<String?>) {
+        if (args.isNotEmpty() && TextUtils.equals(args[0], "--all")) {
+            writer.println(prefix + "All apps list: size=" + mBgAllAppsList.data.size)
+            for (info in mBgAllAppsList.data) {
+                writer.println(
+                    "$prefix   title=\"${info.title}\" bitmapIcon=${info.bitmap.icon} componentName=${info.targetPackage}"
+                )
+            }
+            writer.println()
+        }
+        modelDelegate.dump(prefix, fd, writer, args)
+        mBgDataModel.dump(prefix, fd, writer, args)
+    }
+
+    /** Returns true if there are any callbacks attached to the model */
+    fun hasCallbacks() = synchronized(mCallbacksList) { mCallbacksList.isNotEmpty() }
+
+    /** Returns an array of currently attached callbacks */
+    val callbacks: Array<BgDataModel.Callbacks>
+        get() {
+            synchronized(mCallbacksList) {
+                return mCallbacksList.toTypedArray<BgDataModel.Callbacks>()
+            }
+        }
+
+    companion object {
+        private const val DEBUG_RECEIVER = false
+
+        const val TAG = "Launcher.Model"
+    }
+}
diff --git a/src/com/android/launcher3/LauncherPrefChangeListener.java b/src/com/android/launcher3/LauncherPrefChangeListener.java
new file mode 100644
index 0000000..3e9a846
--- /dev/null
+++ b/src/com/android/launcher3/LauncherPrefChangeListener.java
@@ -0,0 +1,38 @@
+/*
+ * 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;
+
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
+
+/**
+ * Listener for changes in [LauncherPrefs].
+ * <p>
+ * The listener also serves as an [OnSharedPreferenceChangeListener] where
+ * [onSharedPreferenceChanged] delegates to [onPrefChanged]. Overriding [onSharedPreferenceChanged]
+ * breaks compatibility with [SharedPreferences].
+ */
+public interface LauncherPrefChangeListener extends OnSharedPreferenceChangeListener {
+
+    /** Callback invoked when the preference for [key] has changed. */
+    void onPrefChanged(String key);
+
+    @Override
+    default void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
+        onPrefChanged(key);
+    }
+}
diff --git a/src/com/android/launcher3/LauncherPrefs.kt b/src/com/android/launcher3/LauncherPrefs.kt
index 13181e8..712c56c 100644
--- a/src/com/android/launcher3/LauncherPrefs.kt
+++ b/src/com/android/launcher3/LauncherPrefs.kt
@@ -18,7 +18,6 @@
 import android.content.Context
 import android.content.Context.MODE_PRIVATE
 import android.content.SharedPreferences
-import android.content.SharedPreferences.OnSharedPreferenceChangeListener
 import androidx.annotation.VisibleForTesting
 import com.android.launcher3.BuildConfig.WIDGET_ON_FIRST_SCREEN
 import com.android.launcher3.LauncherFiles.DEVICE_PREFERENCES_KEY
@@ -27,6 +26,7 @@
 import com.android.launcher3.pm.InstallSessionHelper
 import com.android.launcher3.provider.RestoreDbTask
 import com.android.launcher3.provider.RestoreDbTask.FIRST_LOAD_AFTER_RESTORE_KEY
+import com.android.launcher3.settings.SettingsActivity
 import com.android.launcher3.states.RotationHelper
 import com.android.launcher3.util.DisplayController
 import com.android.launcher3.util.MainThreadInitializedObject
@@ -34,11 +34,184 @@
 import com.android.launcher3.util.Themes
 
 /**
- * Use same context for shared preferences, so that we use a single cached instance
+ * Manages Launcher [SharedPreferences] through [Item] instances.
  *
  * TODO(b/262721340): Replace all direct SharedPreference refs with LauncherPrefs / Item methods.
  */
-class LauncherPrefs(private val encryptedContext: Context) : SafeCloseable {
+abstract class LauncherPrefs : SafeCloseable {
+
+    /** Returns the value with type [T] for [item]. */
+    abstract fun <T> get(item: ContextualItem<T>): T
+
+    /** Returns the value with type [T] for [item]. */
+    abstract fun <T> get(item: ConstantItem<T>): T
+
+    /** Stores the values for each item in preferences. */
+    abstract fun put(vararg itemsToValues: Pair<Item, Any>)
+
+    /** Stores the [value] with type [T] for [item] in preferences. */
+    abstract fun <T : Any> put(item: Item, value: T)
+
+    /** Synchronous version of [put]. */
+    abstract fun putSync(vararg itemsToValues: Pair<Item, Any>)
+
+    /** Registers [listener] for [items]. */
+    abstract fun addListener(listener: LauncherPrefChangeListener, vararg items: Item)
+
+    /** Unregisters [listener] for [items]. */
+    abstract fun removeListener(listener: LauncherPrefChangeListener, vararg items: Item)
+
+    /** Returns `true` iff all [items] have a value. */
+    abstract fun has(vararg items: Item): Boolean
+
+    /** Removes the value for each item in [items]. */
+    abstract fun remove(vararg items: Item)
+
+    /** Synchronous version of [remove]. */
+    abstract fun removeSync(vararg items: Item)
+
+    companion object {
+        @VisibleForTesting const val BOOT_AWARE_PREFS_KEY = "boot_aware_prefs"
+
+        @JvmField
+        var INSTANCE = MainThreadInitializedObject<LauncherPrefs> { LauncherPrefsImpl(it) }
+
+        @JvmStatic fun get(context: Context): LauncherPrefs = INSTANCE.get(context)
+
+        const val TASKBAR_PINNING_KEY = "TASKBAR_PINNING_KEY"
+        const val TASKBAR_PINNING_DESKTOP_MODE_KEY = "TASKBAR_PINNING_DESKTOP_MODE_KEY"
+        const val SHOULD_SHOW_SMARTSPACE_KEY = "SHOULD_SHOW_SMARTSPACE_KEY"
+        @JvmField
+        val ICON_STATE = nonRestorableItem("pref_icon_shape_path", "", EncryptionType.ENCRYPTED)
+
+        @JvmField
+        val ENABLE_TWOLINE_ALLAPPS_TOGGLE = backedUpItem("pref_enable_two_line_toggle", false)
+        @JvmField
+        val THEMED_ICONS = backedUpItem(Themes.KEY_THEMED_ICONS, false, EncryptionType.ENCRYPTED)
+        @JvmField val PROMISE_ICON_IDS = backedUpItem(InstallSessionHelper.PROMISE_ICON_IDS, "")
+        @JvmField val WORK_EDU_STEP = backedUpItem("showed_work_profile_edu", 0)
+        @JvmField
+        val WORKSPACE_SIZE =
+            backedUpItem(DeviceGridState.KEY_WORKSPACE_SIZE, "", EncryptionType.ENCRYPTED)
+        @JvmField
+        val HOTSEAT_COUNT =
+            backedUpItem(DeviceGridState.KEY_HOTSEAT_COUNT, -1, EncryptionType.ENCRYPTED)
+        @JvmField
+        val TASKBAR_PINNING =
+            backedUpItem(TASKBAR_PINNING_KEY, false, EncryptionType.DEVICE_PROTECTED)
+        @JvmField
+        val TASKBAR_PINNING_IN_DESKTOP_MODE =
+            backedUpItem(TASKBAR_PINNING_DESKTOP_MODE_KEY, true, EncryptionType.DEVICE_PROTECTED)
+
+        @JvmField
+        val DEVICE_TYPE =
+            backedUpItem(
+                DeviceGridState.KEY_DEVICE_TYPE,
+                InvariantDeviceProfile.TYPE_PHONE,
+                EncryptionType.ENCRYPTED,
+            )
+        @JvmField
+        val DB_FILE = backedUpItem(DeviceGridState.KEY_DB_FILE, "", EncryptionType.ENCRYPTED)
+        @JvmField
+        val SHOULD_SHOW_SMARTSPACE =
+            backedUpItem(
+                SHOULD_SHOW_SMARTSPACE_KEY,
+                WIDGET_ON_FIRST_SCREEN,
+                EncryptionType.DEVICE_PROTECTED,
+            )
+        @JvmField
+        val RESTORE_DEVICE =
+            backedUpItem(
+                RestoreDbTask.RESTORED_DEVICE_TYPE,
+                InvariantDeviceProfile.TYPE_PHONE,
+                EncryptionType.ENCRYPTED,
+            )
+        @JvmField
+        val NO_DB_FILES_RESTORED =
+            nonRestorableItem("no_db_files_restored", false, EncryptionType.DEVICE_PROTECTED)
+        @JvmField
+        val IS_FIRST_LOAD_AFTER_RESTORE =
+            nonRestorableItem(FIRST_LOAD_AFTER_RESTORE_KEY, false, EncryptionType.ENCRYPTED)
+        @JvmField val APP_WIDGET_IDS = backedUpItem(RestoreDbTask.APPWIDGET_IDS, "")
+        @JvmField val OLD_APP_WIDGET_IDS = backedUpItem(RestoreDbTask.APPWIDGET_OLD_IDS, "")
+
+        @JvmField
+        val GRID_NAME =
+            ConstantItem(
+                "idp_grid_name",
+                isBackedUp = true,
+                defaultValue = null,
+                encryptionType = EncryptionType.ENCRYPTED,
+                type = String::class.java,
+            )
+        @JvmField
+        val ALLOW_ROTATION =
+            backedUpItem(RotationHelper.ALLOW_ROTATION_PREFERENCE_KEY, Boolean::class.java) {
+                RotationHelper.getAllowRotationDefaultValue(DisplayController.INSTANCE.get(it).info)
+            }
+
+        @JvmField
+        val FIXED_LANDSCAPE_MODE = backedUpItem(SettingsActivity.FIXED_LANDSCAPE_MODE, false)
+
+        // Preferences for widget configurations
+        @JvmField
+        val RECONFIGURABLE_WIDGET_EDUCATION_TIP_SEEN =
+            backedUpItem("launcher.reconfigurable_widget_education_tip_seen", false)
+
+        @JvmStatic
+        fun <T> backedUpItem(
+            sharedPrefKey: String,
+            defaultValue: T,
+            encryptionType: EncryptionType = EncryptionType.ENCRYPTED,
+        ): ConstantItem<T> =
+            ConstantItem(sharedPrefKey, isBackedUp = true, defaultValue, encryptionType)
+
+        @JvmStatic
+        fun <T> backedUpItem(
+            sharedPrefKey: String,
+            type: Class<out T>,
+            encryptionType: EncryptionType = EncryptionType.ENCRYPTED,
+            defaultValueFromContext: (c: Context) -> T,
+        ): ContextualItem<T> =
+            ContextualItem(
+                sharedPrefKey,
+                isBackedUp = true,
+                defaultValueFromContext,
+                encryptionType,
+                type,
+            )
+
+        @JvmStatic
+        fun <T> nonRestorableItem(
+            sharedPrefKey: String,
+            defaultValue: T,
+            encryptionType: EncryptionType = EncryptionType.ENCRYPTED,
+        ): ConstantItem<T> =
+            ConstantItem(sharedPrefKey, isBackedUp = false, defaultValue, encryptionType)
+
+        @Deprecated("Don't use shared preferences directly. Use other LauncherPref methods.")
+        @JvmStatic
+        fun getPrefs(context: Context): SharedPreferences {
+            // Use application context for shared preferences, so we use single cached instance
+            return context.applicationContext.getSharedPreferences(
+                SHARED_PREFERENCES_KEY,
+                MODE_PRIVATE,
+            )
+        }
+
+        @Deprecated("Don't use shared preferences directly. Use other LauncherPref methods.")
+        @JvmStatic
+        fun getDevicePrefs(context: Context): SharedPreferences {
+            // Use application context for shared preferences, so we use a single cached instance
+            return context.applicationContext.getSharedPreferences(
+                DEVICE_PREFERENCES_KEY,
+                MODE_PRIVATE,
+            )
+        }
+    }
+}
+
+private class LauncherPrefsImpl(private val encryptedContext: Context) : LauncherPrefs() {
     private val deviceProtectedStorageContext =
         encryptedContext.createDeviceProtectedStorageContext()
 
@@ -54,11 +227,11 @@
         else item.encryptedPrefs
 
     /** Wrapper around `getInner` for a `ContextualItem` */
-    fun <T> get(item: ContextualItem<T>): T =
+    override fun <T> get(item: ContextualItem<T>): T =
         getInner(item, item.defaultValueFromContext(encryptedContext))
 
     /** Wrapper around `getInner` for an `Item` */
-    fun <T> get(item: ConstantItem<T>): T = getInner(item, item.defaultValue)
+    override fun <T> get(item: ConstantItem<T>): T = getInner(item, item.defaultValue)
 
     /**
      * Retrieves the value for an [Item] from [SharedPreferences]. It handles method typing via the
@@ -97,17 +270,17 @@
      * prepareToPutValue(itemsToValues) for every distinct `SharedPreferences` file present in the
      * provided item configurations.
      */
-    fun put(vararg itemsToValues: Pair<Item, Any>): Unit =
+    override fun put(vararg itemsToValues: Pair<Item, Any>): Unit =
         prepareToPutValues(itemsToValues).forEach { it.apply() }
 
     /** See referenced `put` method above. */
-    fun <T : Any> put(item: Item, value: T): Unit = put(item.to(value))
+    override fun <T : Any> put(item: Item, value: T): Unit = put(item.to(value))
 
     /**
      * Synchronously stores all the values provided according to their associated Item
      * configuration.
      */
-    fun putSync(vararg itemsToValues: Pair<Item, Any>): Unit =
+    override fun putSync(vararg itemsToValues: Pair<Item, Any>): Unit =
         prepareToPutValues(itemsToValues).forEach { it.commit() }
 
     /**
@@ -152,7 +325,7 @@
     @Suppress("UNCHECKED_CAST")
     private fun SharedPreferences.Editor.putValue(
         item: Item,
-        value: Any?
+        value: Any?,
     ): SharedPreferences.Editor =
         when (item.type) {
             String::class.java -> putString(item.sharedPrefKey, value as? String)
@@ -176,7 +349,7 @@
      * `SharedPreferences` files associated with the provided list of items. The listener will need
      * to filter update notifications so they don't activate for non-relevant updates.
      */
-    fun addListener(listener: OnSharedPreferenceChangeListener, vararg items: Item) {
+    override fun addListener(listener: LauncherPrefChangeListener, vararg items: Item) {
         items
             .map { chooseSharedPreferences(it) }
             .distinct()
@@ -187,7 +360,7 @@
      * Stops the listener from getting notified of any more updates to any of the
      * `SharedPreferences` files associated with any of the provided list of [Item].
      */
-    fun removeListener(listener: OnSharedPreferenceChangeListener, vararg items: Item) {
+    override fun removeListener(listener: LauncherPrefChangeListener, vararg items: Item) {
         // If a listener is not registered to a SharedPreference, unregistering it does nothing
         items
             .map { chooseSharedPreferences(it) }
@@ -199,7 +372,7 @@
      * Checks if all the provided [Item] have values stored in their corresponding
      * `SharedPreferences` files.
      */
-    fun has(vararg items: Item): Boolean {
+    override fun has(vararg items: Item): Boolean {
         items
             .groupBy { chooseSharedPreferences(it) }
             .forEach { (prefs, itemsSublist) ->
@@ -211,10 +384,10 @@
     /**
      * Asynchronously removes the [Item]'s value from its corresponding `SharedPreferences` file.
      */
-    fun remove(vararg items: Item) = prepareToRemove(items).forEach { it.apply() }
+    override fun remove(vararg items: Item) = prepareToRemove(items).forEach { it.apply() }
 
     /** Synchronously removes the [Item]'s value from its corresponding `SharedPreferences` file. */
-    fun removeSync(vararg items: Item) = prepareToRemove(items).forEach { it.commit() }
+    override fun removeSync(vararg items: Item) = prepareToRemove(items).forEach { it.commit() }
 
     /**
      * Removes the key value pairs stored in `SharedPreferences` for each corresponding Item. If the
@@ -244,138 +417,6 @@
     }
 
     override fun close() {}
-
-    companion object {
-        @VisibleForTesting const val BOOT_AWARE_PREFS_KEY = "boot_aware_prefs"
-
-        @JvmField var INSTANCE = MainThreadInitializedObject { LauncherPrefs(it) }
-
-        @JvmStatic fun get(context: Context): LauncherPrefs = INSTANCE.get(context)
-
-        const val TASKBAR_PINNING_KEY = "TASKBAR_PINNING_KEY"
-        const val TASKBAR_PINNING_DESKTOP_MODE_KEY = "TASKBAR_PINNING_DESKTOP_MODE_KEY"
-        const val SHOULD_SHOW_SMARTSPACE_KEY = "SHOULD_SHOW_SMARTSPACE_KEY"
-        @JvmField
-        val ICON_STATE = nonRestorableItem("pref_icon_shape_path", "", EncryptionType.ENCRYPTED)
-
-        @JvmField
-        val ENABLE_TWOLINE_ALLAPPS_TOGGLE = backedUpItem("pref_enable_two_line_toggle", false)
-        @JvmField
-        val THEMED_ICONS = backedUpItem(Themes.KEY_THEMED_ICONS, false, EncryptionType.ENCRYPTED)
-        @JvmField val PROMISE_ICON_IDS = backedUpItem(InstallSessionHelper.PROMISE_ICON_IDS, "")
-        @JvmField val WORK_EDU_STEP = backedUpItem("showed_work_profile_edu", 0)
-        @JvmField
-        val WORKSPACE_SIZE =
-            backedUpItem(DeviceGridState.KEY_WORKSPACE_SIZE, "", EncryptionType.ENCRYPTED)
-        @JvmField
-        val HOTSEAT_COUNT =
-            backedUpItem(DeviceGridState.KEY_HOTSEAT_COUNT, -1, EncryptionType.ENCRYPTED)
-        @JvmField
-        val TASKBAR_PINNING =
-            backedUpItem(TASKBAR_PINNING_KEY, false, EncryptionType.DEVICE_PROTECTED)
-        @JvmField
-        val TASKBAR_PINNING_IN_DESKTOP_MODE =
-            backedUpItem(TASKBAR_PINNING_DESKTOP_MODE_KEY, true, EncryptionType.DEVICE_PROTECTED)
-
-        @JvmField
-        val DEVICE_TYPE =
-            backedUpItem(
-                DeviceGridState.KEY_DEVICE_TYPE,
-                InvariantDeviceProfile.TYPE_PHONE,
-                EncryptionType.ENCRYPTED
-            )
-        @JvmField
-        val DB_FILE = backedUpItem(DeviceGridState.KEY_DB_FILE, "", EncryptionType.ENCRYPTED)
-        @JvmField
-        val SHOULD_SHOW_SMARTSPACE =
-            backedUpItem(
-                SHOULD_SHOW_SMARTSPACE_KEY,
-                WIDGET_ON_FIRST_SCREEN,
-                EncryptionType.DEVICE_PROTECTED
-            )
-        @JvmField
-        val RESTORE_DEVICE =
-            backedUpItem(
-                RestoreDbTask.RESTORED_DEVICE_TYPE,
-                InvariantDeviceProfile.TYPE_PHONE,
-                EncryptionType.ENCRYPTED
-            )
-        @JvmField
-        val IS_FIRST_LOAD_AFTER_RESTORE =
-            nonRestorableItem(FIRST_LOAD_AFTER_RESTORE_KEY, false, EncryptionType.ENCRYPTED)
-        @JvmField val APP_WIDGET_IDS = backedUpItem(RestoreDbTask.APPWIDGET_IDS, "")
-        @JvmField val OLD_APP_WIDGET_IDS = backedUpItem(RestoreDbTask.APPWIDGET_OLD_IDS, "")
-        @JvmField
-        val GRID_NAME =
-            ConstantItem(
-                "idp_grid_name",
-                isBackedUp = true,
-                defaultValue = null,
-                encryptionType = EncryptionType.ENCRYPTED,
-                type = String::class.java
-            )
-        @JvmField
-        val ALLOW_ROTATION =
-            backedUpItem(RotationHelper.ALLOW_ROTATION_PREFERENCE_KEY, Boolean::class.java) {
-                RotationHelper.getAllowRotationDefaultValue(DisplayController.INSTANCE.get(it).info)
-            }
-
-        // Preferences for widget configurations
-        @JvmField
-        val RECONFIGURABLE_WIDGET_EDUCATION_TIP_SEEN =
-            backedUpItem("launcher.reconfigurable_widget_education_tip_seen", false)
-
-        @JvmStatic
-        fun <T> backedUpItem(
-            sharedPrefKey: String,
-            defaultValue: T,
-            encryptionType: EncryptionType = EncryptionType.ENCRYPTED
-        ): ConstantItem<T> =
-            ConstantItem(sharedPrefKey, isBackedUp = true, defaultValue, encryptionType)
-
-        @JvmStatic
-        fun <T> backedUpItem(
-            sharedPrefKey: String,
-            type: Class<out T>,
-            encryptionType: EncryptionType = EncryptionType.ENCRYPTED,
-            defaultValueFromContext: (c: Context) -> T
-        ): ContextualItem<T> =
-            ContextualItem(
-                sharedPrefKey,
-                isBackedUp = true,
-                defaultValueFromContext,
-                encryptionType,
-                type
-            )
-
-        @JvmStatic
-        fun <T> nonRestorableItem(
-            sharedPrefKey: String,
-            defaultValue: T,
-            encryptionType: EncryptionType = EncryptionType.ENCRYPTED
-        ): ConstantItem<T> =
-            ConstantItem(sharedPrefKey, isBackedUp = false, defaultValue, encryptionType)
-
-        @Deprecated("Don't use shared preferences directly. Use other LauncherPref methods.")
-        @JvmStatic
-        fun getPrefs(context: Context): SharedPreferences {
-            // Use application context for shared preferences, so we use single cached instance
-            return context.applicationContext.getSharedPreferences(
-                SHARED_PREFERENCES_KEY,
-                MODE_PRIVATE
-            )
-        }
-
-        @Deprecated("Don't use shared preferences directly. Use other LauncherPref methods.")
-        @JvmStatic
-        fun getDevicePrefs(context: Context): SharedPreferences {
-            // Use application context for shared preferences, so we use a single cached instance
-            return context.applicationContext.getSharedPreferences(
-                DEVICE_PREFERENCES_KEY,
-                MODE_PRIVATE
-            )
-        }
-    }
 }
 
 abstract class Item {
@@ -395,7 +436,7 @@
     val defaultValue: T,
     override val encryptionType: EncryptionType,
     // The default value can be null. If so, the type needs to be explicitly stated, or else NPE
-    override val type: Class<out T> = defaultValue!!::class.java
+    override val type: Class<out T> = defaultValue!!::class.java,
 ) : Item() {
 
     fun get(c: Context): T = LauncherPrefs.get(c).get(this)
@@ -406,7 +447,7 @@
     override val isBackedUp: Boolean,
     private val defaultSupplier: (c: Context) -> T,
     override val encryptionType: EncryptionType,
-    override val type: Class<out T>
+    override val type: Class<out T>,
 ) : Item() {
     private var default: T? = null
 
diff --git a/src/com/android/launcher3/LauncherRootView.java b/src/com/android/launcher3/LauncherRootView.java
index 7176733..d645734 100644
--- a/src/com/android/launcher3/LauncherRootView.java
+++ b/src/com/android/launcher3/LauncherRootView.java
@@ -11,6 +11,7 @@
 
 import com.android.launcher3.graphics.SysUiScrim;
 import com.android.launcher3.statemanager.StatefulActivity;
+import com.android.launcher3.statemanager.StatefulContainer;
 import com.android.launcher3.util.window.WindowManagerProxy;
 
 import java.util.Collections;
@@ -20,7 +21,7 @@
 
     private final Rect mTempRect = new Rect();
 
-    private final StatefulActivity mActivity;
+    private final StatefulContainer mStatefulContainer;
 
     @ViewDebug.ExportedProperty(category = "launcher")
     private static final List<Rect> SYSTEM_GESTURE_EXCLUSION_RECT =
@@ -36,24 +37,25 @@
 
     public LauncherRootView(Context context, AttributeSet attrs) {
         super(context, attrs);
-        mActivity = StatefulActivity.fromContext(context);
+        mStatefulContainer = StatefulContainer.fromContext(context);
         mSysUiScrim = new SysUiScrim(this);
     }
 
     private void handleSystemWindowInsets(Rect insets) {
         // Update device profile before notifying the children.
-        mActivity.getDeviceProfile().updateInsets(insets);
+        mStatefulContainer.getDeviceProfile().updateInsets(insets);
         boolean resetState = !insets.equals(mInsets);
         setInsets(insets);
 
         if (resetState) {
-            mActivity.getStateManager().reapplyState(true /* cancelCurrentAnimation */);
+            mStatefulContainer.getStateManager().reapplyState(true /* cancelCurrentAnimation */);
         }
     }
 
     @Override
     public WindowInsets onApplyWindowInsets(WindowInsets insets) {
-        mActivity.handleConfigurationChanged(mActivity.getResources().getConfiguration());
+        mStatefulContainer.handleConfigurationChanged(
+                mStatefulContainer.getContext().getResources().getConfiguration());
 
         insets = WindowManagerProxy.INSTANCE.get(getContext())
                 .normalizeWindowInsets(getContext(), insets, mTempRect);
@@ -72,7 +74,7 @@
     }
 
     public void dispatchInsets() {
-        mActivity.getDeviceProfile().updateInsets(mInsets);
+        mStatefulContainer.getDeviceProfile().updateInsets(mInsets);
         super.setInsets(mInsets);
     }
 
diff --git a/src/com/android/launcher3/LauncherSettings.java b/src/com/android/launcher3/LauncherSettings.java
index 87ac193..1d2d161 100644
--- a/src/com/android/launcher3/LauncherSettings.java
+++ b/src/com/android/launcher3/LauncherSettings.java
@@ -16,8 +16,12 @@
 
 package com.android.launcher3;
 
+import static android.util.Base64.NO_PADDING;
+import static android.util.Base64.NO_WRAP;
+
 import android.database.sqlite.SQLiteDatabase;
 import android.provider.BaseColumns;
+import android.util.Base64;
 
 import androidx.annotation.NonNull;
 
@@ -354,8 +358,17 @@
      * Launcher settings
      */
     public static final class Settings {
-        public static final String LAYOUT_DIGEST_KEY = "launcher3.layout.provider.blob";
+        public static final String LAYOUT_PROVIDER_KEY = "launcher3.layout.provider";
         public static final String LAYOUT_DIGEST_LABEL = "launcher-layout";
         public static final String LAYOUT_DIGEST_TAG = "ignore";
+        public static final String BLOB_KEY_PREFIX = "blob://";
+
+        /**
+         * Creates a key to be used for {@link #LAYOUT_PROVIDER_KEY}
+         * @param digest byte[] representing the message digest for the blob handle
+         */
+        public static String createBlobProviderKey(byte[] digest) {
+            return BLOB_KEY_PREFIX + Base64.encodeToString(digest, NO_WRAP | NO_PADDING);
+        }
     }
 }
diff --git a/src/com/android/launcher3/LauncherState.java b/src/com/android/launcher3/LauncherState.java
index 102189b..7d5e481 100644
--- a/src/com/android/launcher3/LauncherState.java
+++ b/src/com/android/launcher3/LauncherState.java
@@ -375,8 +375,14 @@
     }
 
     public PageAlphaProvider getWorkspacePageAlphaProvider(Launcher launcher) {
-        if ((this != NORMAL && this != HINT_STATE)
-                || !launcher.getDeviceProfile().shouldFadeAdjacentWorkspaceScreens()) {
+        DeviceProfile dp = launcher.getDeviceProfile();
+        boolean shouldFadeAdjacentScreens = (this == NORMAL || this == HINT_STATE)
+                && dp.shouldFadeAdjacentWorkspaceScreens();
+        // Avoid showing adjacent screens behind handheld All Apps sheet.
+        if (Flags.allAppsSheetForHandheld() && dp.isPhone && this == ALL_APPS) {
+            shouldFadeAdjacentScreens = true;
+        }
+        if (!shouldFadeAdjacentScreens) {
             return DEFAULT_ALPHA_PROVIDER;
         }
         final int centerPage = launcher.getWorkspace().getNextPage();
diff --git a/src/com/android/launcher3/ModelCallbacks.kt b/src/com/android/launcher3/ModelCallbacks.kt
index 496d517..5d32525 100644
--- a/src/com/android/launcher3/ModelCallbacks.kt
+++ b/src/com/android/launcher3/ModelCallbacks.kt
@@ -252,8 +252,11 @@
         PopupContainerWithArrow.dismissInvalidPopup(launcher)
     }
 
-    override fun bindAllWidgets(allWidgets: List<WidgetsListBaseEntry>) {
-        launcher.widgetPickerDataProvider.setWidgets(allWidgets, /* defaultWidgets= */ listOf())
+    override fun bindAllWidgets(
+        allWidgets: List<WidgetsListBaseEntry>,
+        defaultWidgets: List<WidgetsListBaseEntry>,
+    ) {
+        launcher.widgetPickerDataProvider.setWidgets(allWidgets, defaultWidgets)
     }
 
     /** Returns the ids of the workspaces to bind. */
diff --git a/src/com/android/launcher3/PillColorPorovider.kt b/src/com/android/launcher3/PillColorPorovider.kt
new file mode 100644
index 0000000..347c5d6
--- /dev/null
+++ b/src/com/android/launcher3/PillColorPorovider.kt
@@ -0,0 +1,85 @@
+/*
+ * 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
+
+import android.content.Context
+import android.database.ContentObserver
+import android.graphics.Paint
+import android.net.Uri
+import android.provider.Settings
+import com.android.launcher3.util.Executors.ORDERED_BG_EXECUTOR
+
+class PillColorProvider private constructor(c: Context) {
+    private val context = c.applicationContext
+
+    private val matchaUri by lazy { Settings.Secure.getUriFor(MATCHA_SETTING) }
+    var appTitlePillPaint = Paint()
+        private set
+
+    var appTitleTextPaint = Paint()
+        private set
+
+    private var isMatchaEnabledInternal = 0
+
+    var isMatchaEnabled = isMatchaEnabledInternal != 0
+
+    private val pillColorObserver =
+        object : ContentObserver(ORDERED_BG_EXECUTOR.handler) {
+            override fun onChange(selfChange: Boolean, uri: Uri?) {
+                if (uri == matchaUri) {
+                    isMatchaEnabledInternal =
+                        Settings.Secure.getInt(context.contentResolver, MATCHA_SETTING, 0)
+                    isMatchaEnabled = isMatchaEnabledInternal != 0
+                }
+            }
+        }
+
+    fun registerObserver() {
+        context.contentResolver.registerContentObserver(matchaUri, false, pillColorObserver)
+        setup()
+    }
+
+    fun unregisterObserver() {
+        context.contentResolver.unregisterContentObserver(pillColorObserver)
+    }
+
+    fun setup() {
+        appTitlePillPaint.color =
+            context.resources.getColor(
+                R.color.material_color_surface_container_lowest,
+                context.theme,
+            )
+        appTitleTextPaint.color =
+            context.resources.getColor(R.color.material_color_on_surface, context.theme)
+        isMatchaEnabledInternal = Settings.Secure.getInt(context.contentResolver, MATCHA_SETTING, 0)
+        isMatchaEnabled = isMatchaEnabledInternal != 0
+    }
+
+    companion object {
+        private var INSTANCE: PillColorProvider? = null
+        private const val MATCHA_SETTING = "matcha_enable"
+
+        // TODO: Replace with a Dagger injection that is a singleton.
+        @JvmStatic
+        fun getInstance(context: Context): PillColorProvider {
+            if (INSTANCE == null) {
+                INSTANCE = PillColorProvider(context)
+            }
+            return INSTANCE!!
+        }
+    }
+}
diff --git a/src/com/android/launcher3/SecondaryDropTarget.java b/src/com/android/launcher3/SecondaryDropTarget.java
index 8d1e61f..b3cb948 100644
--- a/src/com/android/launcher3/SecondaryDropTarget.java
+++ b/src/com/android/launcher3/SecondaryDropTarget.java
@@ -22,7 +22,6 @@
 import android.content.pm.ApplicationInfo;
 import android.content.pm.LauncherActivityInfo;
 import android.content.pm.LauncherApps;
-import android.content.pm.PackageManager;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.UserHandle;
@@ -44,7 +43,7 @@
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.ItemInfoWithIcon;
 import com.android.launcher3.pm.UserCache;
-import com.android.launcher3.util.PackageManagerHelper;
+import com.android.launcher3.util.ApplicationInfoWrapper;
 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 
 import java.net.URISyntaxException;
@@ -342,9 +341,8 @@
         }
 
         public void onLauncherResume() {
-            // We use MATCH_UNINSTALLED_PACKAGES as the app can be on SD card as well.
-            if (PackageManagerHelper.INSTANCE.get(mContext).getApplicationInfo(mPackageName,
-                    mDragObject.dragInfo.user, PackageManager.MATCH_UNINSTALLED_PACKAGES) == null) {
+            if (new ApplicationInfoWrapper(mContext, mPackageName, mDragObject.dragInfo.user)
+                    .getInfo() == null) {
                 mDragObject.dragSource = mOriginal;
                 mOriginal.onDropCompleted(SecondaryDropTarget.this, mDragObject, true);
                 mStatsLogManager.logger().withInstanceId(mDragObject.logInstanceId)
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index f8ac48a..71a2589 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -83,8 +83,8 @@
 import com.android.launcher3.dragndrop.FolderAdaptiveIcon;
 import com.android.launcher3.graphics.TintedDrawableSpan;
 import com.android.launcher3.icons.BitmapInfo;
+import com.android.launcher3.icons.CacheableShortcutInfo;
 import com.android.launcher3.icons.LauncherIcons;
-import com.android.launcher3.icons.ShortcutCachingLogic;
 import com.android.launcher3.icons.ThemedIconDrawable;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.ItemInfoWithIcon;
@@ -420,6 +420,25 @@
         return mapRange(interpolator.getInterpolation(progress), toMin, toMax);
     }
 
+    /**
+     * 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.
+     * @param fromMax The upper bound of the range that t is being mapped from.
+     * @param toMin The lower bound of the range that t is being mapped to.
+     * @param toMax The upper bound of the range that t is being mapped to.
+     * @return The mapped value of t.
+     */
+    public static int mapToRange(int t, int fromMin, int fromMax, int toMin, int toMax,
+            Interpolator interpolator) {
+        if (fromMin == fromMax || toMin == toMax) {
+            Log.e(TAG, "mapToRange: range has 0 length");
+            return toMin;
+        }
+        float progress = getProgress(t, fromMin, fromMax);
+        return (int) mapRange(interpolator.getInterpolation(progress), toMin, toMax);
+    }
+
     /** Bounds t between a lower and upper bound and maps the result to a range. */
     public static float mapBoundToRange(float t, float lowerBound, float upperBound,
             float toMin, float toMax, Interpolator interpolator) {
@@ -597,6 +616,14 @@
     }
 
     /**
+     * Utility method to know if a device's primary language is English.
+     */
+    public static boolean isEnglishLanguage(Context context) {
+        return context.getResources().getConfiguration().locale.getLanguage()
+                .equals(Locale.ENGLISH.getLanguage());
+    }
+
+    /**
      * Returns the full drawable for info as multiple layers of AdaptiveIconDrawable. The second
      * drawable in the Pair is the badge used with the icon.
      *
@@ -626,8 +653,7 @@
             if (activityInfo == null) {
                 return null;
             }
-            mainIcon = appState.getIconProvider().getIcon(
-                    activityInfo, appState.getInvariantDeviceProfile().fillResIconDpi);
+            mainIcon = appState.getIconCache().getFullResIcon(activityInfo.getActivityInfo());
         } else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
             List<ShortcutInfo> siList = ShortcutKey.fromItemInfo(info)
                     .buildRequest(context)
@@ -636,7 +662,7 @@
                 return null;
             } else {
                 ShortcutInfo si = siList.get(0);
-                mainIcon = ShortcutCachingLogic.getIcon(context, si,
+                mainIcon = CacheableShortcutInfo.getIcon(context, si,
                         appState.getInvariantDeviceProfile().fillResIconDpi);
                 // Only fetch badge if the icon is on workspace
                 if (info.id != ItemInfo.NO_ID && badge == null) {
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 0e9c861..95dbf5f 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -16,6 +16,7 @@
 
 package com.android.launcher3;
 
+import static com.android.launcher3.AbstractFloatingView.TYPE_WIDGET_RESIZE_FRAME;
 import static com.android.launcher3.BubbleTextView.DISPLAY_FOLDER;
 import static com.android.launcher3.Flags.enableSmartspaceRemovalToggle;
 import static com.android.launcher3.LauncherAnimUtils.SPRING_LOADED_EXIT_DELAY;
@@ -1222,6 +1223,10 @@
     }
 
     protected void onPageBeginTransition() {
+        // Widget resize frame doesn't receive events to close when talkback is enabled. For that
+        // case, close it here.
+        AbstractFloatingView.closeOpenViews(mLauncher, false, TYPE_WIDGET_RESIZE_FRAME);
+
         super.onPageBeginTransition();
         updateChildrenLayersEnabled();
     }
diff --git a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
index 1094768..1b58987 100644
--- a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
@@ -47,7 +47,6 @@
 import android.os.Process;
 import android.os.UserManager;
 import android.util.AttributeSet;
-import android.util.FloatProperty;
 import android.util.Log;
 import android.util.SparseArray;
 import android.view.KeyEvent;
@@ -96,6 +95,7 @@
 import com.android.launcher3.views.ScrimView;
 import com.android.launcher3.views.SpringRelativeLayout;
 import com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip;
+import com.android.systemui.plugins.AllAppsRow;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -115,19 +115,6 @@
         ScrimView.ScrimDrawingController {
 
 
-    public static final FloatProperty<ActivityAllAppsContainerView<?>> BOTTOM_SHEET_ALPHA =
-            new FloatProperty<>("bottomSheetAlpha") {
-                @Override
-                public Float get(ActivityAllAppsContainerView<?> containerView) {
-                    return containerView.mBottomSheetAlpha;
-                }
-
-                @Override
-                public void setValue(ActivityAllAppsContainerView<?> containerView, float v) {
-                    containerView.setBottomSheetAlpha(v);
-                }
-            };
-
     public static final float PULL_MULTIPLIER = .02f;
     public static final float FLING_VELOCITY_MULTIPLIER = 1200f;
     protected static final String BUNDLE_KEY_CURRENT_PAGE = "launcher.allapps.current_page";
@@ -166,6 +153,7 @@
     private final RectF mTmpRectF = new RectF();
     protected AllAppsPagedView mViewPager;
     protected FloatingHeaderView mHeader;
+    protected final List<AllAppsRow> mAdditionalHeaderRows = new ArrayList<>();
     protected View mBottomSheetBackground;
     protected RecyclerViewFastScroller mFastScroller;
     private ConstraintLayout mFastScrollLetterLayout;
@@ -191,8 +179,6 @@
     private ScrimView mScrimView;
     private int mHeaderColor;
     private int mBottomSheetBackgroundColor;
-    private float mBottomSheetAlpha = 1f;
-    private boolean mForceBottomSheetVisible;
     private int mTabsProtectionAlpha;
     @Nullable private AllAppsTransitionController mAllAppsTransitionController;
 
@@ -278,6 +264,8 @@
 
         getLayoutInflater().inflate(R.layout.all_apps_content, this);
         mHeader = findViewById(R.id.all_apps_header);
+        mAdditionalHeaderRows.clear();
+        mAdditionalHeaderRows.addAll(getAdditionalHeaderRows());
         mBottomSheetBackground = findViewById(R.id.bottom_sheet_background);
         mBottomSheetHandleArea = findViewById(R.id.bottom_sheet_handle_area);
         mSearchRecyclerView = findViewById(R.id.search_results_list_view);
@@ -296,10 +284,18 @@
             // Add the search box above everything else in this container (if the flag is enabled,
             // it's added to drag layer in onAttach instead).
             addView(mSearchContainer);
+            // The search container is visually at the top of the all apps UI, and should thus be
+            // focused by default. It's added to end of the children list, so it needs to be
+            // explicitly marked as focused by default.
+            mSearchContainer.setFocusedByDefault(true);
         }
         mSearchUiManager = (SearchUiManager) mSearchContainer;
     }
 
+    public List<AllAppsRow> getAdditionalHeaderRows() {
+        return List.of();
+    }
+
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
@@ -347,20 +343,6 @@
         return mSearchUiManager;
     }
 
-    public View getBottomSheetBackground() {
-        return mBottomSheetBackground;
-    }
-
-    /**
-     * Temporarily force the bottom sheet to be visible on non-tablets.
-     *
-     * @param force {@code true} means bottom sheet will be visible on phones until {@code reset()}.
-     */
-    public void forceBottomSheetVisible(boolean force) {
-        mForceBottomSheetVisible = force;
-        updateBackgroundVisibility(mActivityContext.getDeviceProfile());
-    }
-
     public View getSearchView() {
         return mSearchContainer;
     }
@@ -492,7 +474,7 @@
         if (mHeader != null && mHeader.getVisibility() == VISIBLE) {
             mHeader.reset(animate);
         }
-        forceBottomSheetVisible(false);
+        updateBackgroundVisibility(mActivityContext.getDeviceProfile());
         // Reset the base recycler view after transitioning home.
         updateHeaderScroll(0);
         if (exitSearch) {
@@ -722,7 +704,7 @@
             post(() -> mAH.get(AdapterHolder.WORK).applyPadding());
 
         } else {
-            mWorkManager.detachWorkModeSwitch();
+            mWorkManager.detachWorkUtilityViews();
             mViewPager = null;
         }
 
@@ -740,6 +722,8 @@
     }
 
     void setupHeader() {
+        mAdditionalHeaderRows.forEach(row -> mHeader.onPluginDisconnected(row));
+
         mHeader.setVisibility(View.VISIBLE);
         boolean tabsHidden = !mUsingTabs;
         mHeader.setup(
@@ -757,6 +741,7 @@
                 adapterHolder.mRecyclerView.scrollToTop();
             }
         });
+        mAdditionalHeaderRows.forEach(row -> mHeader.onPluginConnected(row, mActivityContext));
 
         removeCustomRules(mHeader);
         if (isSearchBarFloating()) {
@@ -998,18 +983,13 @@
     }
 
     protected void updateBackgroundVisibility(DeviceProfile deviceProfile) {
-        boolean visible = deviceProfile.isTablet || mForceBottomSheetVisible;
-        mBottomSheetBackground.setVisibility(visible ? View.VISIBLE : View.GONE);
-        // Note: For tablets, the opaque background and header protection are added in drawOnScrim.
+        mBottomSheetBackground.setVisibility(
+                deviceProfile.shouldShowAllAppsOnSheet() ? View.VISIBLE : View.GONE);
+        // Note: The opaque sheet background and header protection are added in drawOnScrim.
         // For the taskbar entrypoint, the scrim is drawn by its abstract slide in view container,
         // so its header protection is derived from this scrim instead.
     }
 
-    private void setBottomSheetAlpha(float alpha) {
-        // Bottom sheet alpha is always 1 for tablets.
-        mBottomSheetAlpha = mActivityContext.getDeviceProfile().isTablet ? 1f : alpha;
-    }
-
     @VisibleForTesting
     public void onAppsUpdated() {
         mHasWorkApps = Stream.of(mAllAppsStore.getApps())
@@ -1147,8 +1127,8 @@
         applyAdapterSideAndBottomPaddings(grid);
 
         MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams();
-        // Ignore left/right insets on tablet because we are already centered in-screen.
-        if (grid.isTablet) {
+        // Ignore left/right insets on bottom sheet because we are already centered in-screen.
+        if (grid.shouldShowAllAppsOnSheet()) {
             mlp.leftMargin = mlp.rightMargin = 0;
         } else {
             mlp.leftMargin = insets.left;
@@ -1253,8 +1233,8 @@
     /** Called in Launcher#bindStringCache() to update the UI when cache is updated. */
     public void updateWorkUI() {
         setDeviceManagementResources();
-        if (mWorkManager.getWorkModeSwitch() != null) {
-            mWorkManager.getWorkModeSwitch().updateStringFromCache();
+        if (mWorkManager.getWorkUtilityView() != null) {
+            mWorkManager.getWorkUtilityView().updateStringFromCache();
         }
         inflateWorkCardsIfNeeded();
     }
@@ -1394,7 +1374,7 @@
         // Draw full background panel for tablets.
         if (hasBottomSheet) {
             mHeaderPaint.setColor(mBottomSheetBackgroundColor);
-            mHeaderPaint.setAlpha((int) (255 * mBottomSheetAlpha));
+            mHeaderPaint.setAlpha(255);
 
             mTmpRectF.set(
                     leftWithScale,
@@ -1577,8 +1557,8 @@
         void applyPadding() {
             if (mRecyclerView != null) {
                 int bottomOffset = 0;
-                if (isWork() && mWorkManager.getWorkModeSwitch() != null) {
-                    bottomOffset = mInsets.bottom + mWorkManager.getWorkModeSwitch().getHeight();
+                if (isWork() && mWorkManager.getWorkUtilityView() != null) {
+                    bottomOffset = mInsets.bottom + mWorkManager.getWorkUtilityView().getHeight();
                 } else if (isMain() && mPrivateProfileManager != null) {
                     Optional<AdapterItem> privateSpaceHeaderItem = mAppsList.getAdapterItems()
                             .stream()
diff --git a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
index 4e1e950..51d1c9f 100644
--- a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
@@ -331,6 +331,9 @@
 
     public void setLettersToScrollLayout(
             List<AlphabeticalAppsList.FastScrollSectionInfo> fastScrollSections) {
+        if (fastScrollSections.isEmpty()) {
+            return;
+        }
         if (mLetterList != null) {
             mLetterList.removeAllViews();
         }
@@ -346,17 +349,12 @@
                     (LetterListTextView) LayoutInflater.from(context).inflate(
                             R.layout.fast_scroller_letter_list_text_view, mLetterList, false);
             int viewId = View.generateViewId();
-            textView.setId(viewId);
+            textView.apply(sectionInfo /* FastScrollSectionInfo */, viewId /* viewId */);
             sectionInfo.setId(viewId);
-            textView.setText(sectionInfo.sectionName);
             if (i == fastScrollSections.size() - 1) {
                 // The last section info is just a duplicate so that user can scroll to the bottom.
                 textView.setVisibility(INVISIBLE);
             }
-            ConstraintLayout.LayoutParams lp = new ConstraintLayout.LayoutParams(
-                    MATCH_CONSTRAINT, WRAP_CONTENT);
-            lp.dimensionRatio = "v,1:1";
-            textView.setLayoutParams(lp);
             textViews.add(textView);
             mLetterList.addView(textView);
         }
@@ -369,6 +367,8 @@
         mLetterList.addView(lastLetterListTextView);
         constraintTextViewsVertically(mLetterList, textViews);
         mLetterList.setVisibility(VISIBLE);
+        // Set the alpha to 0 to avoid the letter list being shown when it shouldn't be.
+        mLetterList.setAlpha(0);
     }
 
     private void constraintTextViewsVertically(ConstraintLayout constraintLayout,
diff --git a/src/com/android/launcher3/allapps/AllAppsTransitionController.java b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
index c6852e0..8554de5 100644
--- a/src/com/android/launcher3/allapps/AllAppsTransitionController.java
+++ b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
@@ -16,7 +16,6 @@
 package com.android.launcher3.allapps;
 
 import static com.android.app.animation.Interpolators.DECELERATE_1_7;
-import static com.android.app.animation.Interpolators.INSTANT;
 import static com.android.app.animation.Interpolators.LINEAR;
 import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
 import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
@@ -28,7 +27,6 @@
 import static com.android.launcher3.UtilitiesKt.modifyAttributesOnViewTree;
 import static com.android.launcher3.UtilitiesKt.restoreAttributesOnViewTree;
 import static com.android.launcher3.anim.PropertySetter.NO_ANIM_PROPERTY_SETTER;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_APPS_BOTTOM_SHEET_FADE;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_APPS_FADE;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_VERTICAL_PROGRESS;
 import static com.android.launcher3.util.SystemUiController.FLAG_DARK_NAV;
@@ -45,9 +43,9 @@
 import androidx.annotation.FloatRange;
 import androidx.annotation.Nullable;
 
-import com.android.app.animation.Interpolators;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
+import com.android.launcher3.Flags;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.R;
@@ -57,14 +55,16 @@
 import com.android.launcher3.statemanager.StateManager.StateHandler;
 import com.android.launcher3.states.StateAnimationConfig;
 import com.android.launcher3.touch.AllAppsSwipeController;
+import com.android.launcher3.util.MSDLPlayerWrapper;
 import com.android.launcher3.util.MultiPropertyFactory;
 import com.android.launcher3.util.MultiPropertyFactory.MultiProperty;
 import com.android.launcher3.util.MultiValueAlpha;
 import com.android.launcher3.util.ScrollableLayoutManager;
 import com.android.launcher3.util.Themes;
-import com.android.launcher3.util.VibratorWrapper;
 import com.android.launcher3.views.ScrimView;
 
+import com.google.android.msdl.data.model.MSDLToken;
+
 /**
  * Handles AllApps view transition.
  * 1) Slides all apps view using direct manipulation
@@ -106,7 +106,7 @@
 
                 @Override
                 public Float get(AllAppsTransitionController controller) {
-                    if (controller.mIsTablet) {
+                    if (controller.mShouldShowAllAppsOnSheet) {
                         return controller.mAppsView.getActiveRecyclerView().getTranslationY();
                     } else {
                         return controller.getAppsViewPullbackTranslationY().getValue();
@@ -115,7 +115,7 @@
 
                 @Override
                 public void setValue(AllAppsTransitionController controller, float translation) {
-                    if (controller.mIsTablet) {
+                    if (controller.mShouldShowAllAppsOnSheet) {
                         controller.mAppsView.getActiveRecyclerView().setTranslationY(translation);
                         controller.getAppsViewPullbackTranslationY().setValue(
                                 ALL_APPS_PULL_BACK_TRANSLATION_DEFAULT);
@@ -134,7 +134,7 @@
 
                 @Override
                 public Float get(AllAppsTransitionController controller) {
-                    if (controller.mIsTablet) {
+                    if (controller.mShouldShowAllAppsOnSheet) {
                         return controller.mAppsView.getActiveRecyclerView().getAlpha();
                     } else {
                         return controller.getAppsViewPullbackAlpha().getValue();
@@ -143,7 +143,7 @@
 
                 @Override
                 public void setValue(AllAppsTransitionController controller, float alpha) {
-                    if (controller.mIsTablet) {
+                    if (controller.mShouldShowAllAppsOnSheet) {
                         controller.mAppsView.getActiveRecyclerView().setAlpha(alpha);
                         controller.getAppsViewPullbackAlpha().setValue(
                                 ALL_APPS_PULL_BACK_ALPHA_DEFAULT);
@@ -168,6 +168,7 @@
     @Nullable private Animator.AnimatorListener mAllAppsSearchBackAnimationListener;
 
     private boolean mIsVerticalLayout;
+    private boolean mShouldShowAllAppsOnSheet;
 
     // Animation in this class is controlled by a single variable {@link mProgress}.
     // Visually, it represents top y coordinate of the all apps container if multiplied with
@@ -183,24 +184,22 @@
     private MultiValueAlpha mAppsViewAlpha;
     private MultiPropertyFactory<View> mAppsViewTranslationY;
 
-    private boolean mIsTablet;
-
     private boolean mHasScaleEffect;
-    private final VibratorWrapper mVibratorWrapper;
+    private final MSDLPlayerWrapper mMSDLPlayerWrapper;
 
     public AllAppsTransitionController(Launcher l) {
         mLauncher = l;
         DeviceProfile dp = mLauncher.getDeviceProfile();
         mProgress = 1f;
         mIsVerticalLayout = dp.isVerticalBarLayout();
-        mIsTablet = dp.isTablet;
+        mShouldShowAllAppsOnSheet = dp.shouldShowAllAppsOnSheet();
         mNavScrimFlag = Themes.getAttrBoolean(l, R.attr.isMainColorDark)
                 ? FLAG_DARK_NAV : FLAG_LIGHT_NAV;
 
         setShiftRange(dp.allAppsShiftRange);
         mAllAppScale.value = 1;
         mLauncher.addOnDeviceProfileChangeListener(this);
-        mVibratorWrapper = VibratorWrapper.INSTANCE.get(mLauncher.getApplicationContext());
+        mMSDLPlayerWrapper = MSDLPlayerWrapper.INSTANCE.get(mLauncher.getApplicationContext());
     }
 
     public float getShiftRange() {
@@ -217,7 +216,7 @@
             mLauncher.getWorkspace().getPageIndicator().setTranslationY(0);
         }
 
-        mIsTablet = dp.isTablet;
+        mShouldShowAllAppsOnSheet = dp.shouldShowAllAppsOnSheet();
     }
 
     /**
@@ -280,10 +279,9 @@
             return;
         }
 
-        float deceleratedProgress = Interpolators.BACK_GESTURE.getInterpolation(backProgress);
         float scaleProgress = ScrollableLayoutManager.PREDICTIVE_BACK_MIN_SCALE
                 + (1 - ScrollableLayoutManager.PREDICTIVE_BACK_MIN_SCALE)
-                * (1 - deceleratedProgress);
+                * (1 - backProgress);
 
         mAllAppScale.updateValue(scaleProgress);
     }
@@ -373,8 +371,16 @@
         setAlphas(toState, config, builder);
         // This controls both haptics for tapping on QSB and going to all apps.
         if (ALL_APPS.equals(toState) && mLauncher.isInState(NORMAL)) {
-            mLauncher.getAppsView().performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY,
-                    HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
+            if (Flags.msdlFeedback()) {
+                if (config.isUserControlled()) {
+                    mMSDLPlayerWrapper.playToken(MSDLToken.SWIPE_THRESHOLD_INDICATOR);
+                } else {
+                    mMSDLPlayerWrapper.playToken(MSDLToken.TAP_HIGH_EMPHASIS);
+                }
+            } else {
+                mLauncher.getAppsView().performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY,
+                        HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
+            }
         }
     }
 
@@ -395,10 +401,6 @@
         setter.setFloat(getAppsViewPullbackAlpha(), MultiPropertyFactory.MULTI_PROPERTY_VALUE,
                 hasAllAppsContent ? 1 : 0, allAppsFade);
 
-        setter.setFloat(mLauncher.getAppsView(),
-                ActivityAllAppsContainerView.BOTTOM_SHEET_ALPHA, hasAllAppsContent ? 1 : 0,
-                config.getInterpolator(ANIM_ALL_APPS_BOTTOM_SHEET_FADE, INSTANT));
-
         boolean shouldProtectHeader = !config.hasAnimationFlag(StateAnimationConfig.SKIP_SCRIM)
                 && (ALL_APPS == state || mLauncher.getStateManager().getState() == ALL_APPS);
         mScrimView.setDrawingController(shouldProtectHeader ? mAppsView : null);
diff --git a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
index 8e44d65..709b52a 100644
--- a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
+++ b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
@@ -106,6 +106,7 @@
     // The of ordered component names as a result of a search query
     private final ArrayList<AdapterItem> mSearchResults = new ArrayList<>();
     private final SpannableString mPrivateProfileAppScrollerBadge;
+    private final SpannableString mPrivateProfileDividerBadge;
     private BaseAllAppsAdapter<T> mAdapter;
     private AppInfoComparator mAppNameComparator;
     private int mNumAppsPerRowAllApps;
@@ -124,9 +125,14 @@
             mAllAppsStore.addUpdateListener(this);
         }
         mPrivateProfileAppScrollerBadge = new SpannableString(" ");
-        mPrivateProfileAppScrollerBadge.setSpan(new ImageSpan(context,
+        mPrivateProfileAppScrollerBadge.setSpan(new ImageSpan(context, Flags.letterFastScroller()
+                        ? R.drawable.ic_private_profile_letter_list_fast_scroller_badge :
                         R.drawable.ic_private_profile_app_scroller_badge, ImageSpan.ALIGN_CENTER),
                 0, 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+        mPrivateProfileDividerBadge = new SpannableString(" ");
+        mPrivateProfileDividerBadge.setSpan(new ImageSpan(context,
+                        R.drawable.ic_private_profile_divider_badge, ImageSpan.ALIGN_CENTER),
+                0, 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
     }
 
     /** Set the number of apps per row when device profile changes. */
@@ -404,6 +410,11 @@
         // Add system apps separator.
         if (Flags.privateSpaceSysAppsSeparation()) {
             position = mPrivateProviderManager.addSystemAppsDivider(mAdapterItems);
+            if (Flags.letterFastScroller()) {
+                FastScrollSectionInfo sectionInfo =
+                        new FastScrollSectionInfo(mPrivateProfileDividerBadge, position);
+                mFastScrollerSections.add(sectionInfo);
+            }
         }
         // Add system apps.
         position = addAppsWithSections(split.get(false), position);
@@ -437,8 +448,11 @@
                 Log.d(TAG, "addAppsWithSections: adding sectionName: " + sectionName
                     + " with appInfoTitle: " + info.title);
                 lastSectionName = sectionName;
-                mFastScrollerSections.add(new FastScrollSectionInfo(hasPrivateApps ?
-                        mPrivateProfileAppScrollerBadge : sectionName, position));
+                boolean usePrivateAppScrollerBadge = !Flags.letterFastScroller() && hasPrivateApps;
+                FastScrollSectionInfo sectionInfo = new FastScrollSectionInfo(
+                        usePrivateAppScrollerBadge ?
+                                mPrivateProfileAppScrollerBadge : sectionName, position);
+                mFastScrollerSections.add(sectionInfo);
             }
             position++;
         }
diff --git a/src/com/android/launcher3/allapps/FloatingHeaderView.java b/src/com/android/launcher3/allapps/FloatingHeaderView.java
index ac06ab4..8193511 100644
--- a/src/com/android/launcher3/allapps/FloatingHeaderView.java
+++ b/src/com/android/launcher3/allapps/FloatingHeaderView.java
@@ -15,6 +15,8 @@
  */
 package com.android.launcher3.allapps;
 
+import static com.android.launcher3.allapps.FloatingHeaderRow.NO_ROWS;
+
 import android.animation.ValueAnimator;
 import android.content.Context;
 import android.graphics.Point;
@@ -109,11 +111,11 @@
 
     // This is initialized once during inflation and stays constant after that. Fixed views
     // cannot be added or removed dynamically.
-    private FloatingHeaderRow[] mFixedRows = FloatingHeaderRow.NO_ROWS;
+    private FloatingHeaderRow[] mFixedRows = NO_ROWS;
 
     // Array of all fixed rows and plugin rows. This is initialized every time a plugin is
     // enabled or disabled, and represent the current set of all rows.
-    private FloatingHeaderRow[] mAllRows = FloatingHeaderRow.NO_ROWS;
+    private FloatingHeaderRow[] mAllRows = NO_ROWS;
 
     public FloatingHeaderView(@NonNull Context context) {
         this(context, null);
@@ -180,6 +182,10 @@
 
     @Override
     public void onPluginConnected(AllAppsRow allAppsRowPlugin, Context context) {
+        if (mPluginRows.containsKey(allAppsRowPlugin)) {
+            // Plugin has already been connected
+            return;
+        }
         PluginHeaderRow headerRow = new PluginHeaderRow(allAppsRowPlugin, this);
         addView(headerRow.mView, indexOfChild(mTabLayout));
         mPluginRows.put(allAppsRowPlugin, headerRow);
@@ -211,6 +217,9 @@
     @Override
     public void onPluginDisconnected(AllAppsRow plugin) {
         PluginHeaderRow row = mPluginRows.get(plugin);
+        if (row == null) {
+            return;
+        }
         removeView(row.mView);
         mPluginRows.remove(plugin);
         recreateAllRowsArray();
diff --git a/src/com/android/launcher3/allapps/LetterListTextView.java b/src/com/android/launcher3/allapps/LetterListTextView.java
index 9326d79..8586078 100644
--- a/src/com/android/launcher3/allapps/LetterListTextView.java
+++ b/src/com/android/launcher3/allapps/LetterListTextView.java
@@ -16,6 +16,9 @@
 
 package com.android.launcher3.allapps;
 
+import static androidx.constraintlayout.widget.ConstraintSet.MATCH_CONSTRAINT;
+import static androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT;
+
 import android.content.Context;
 import android.graphics.PorterDuff;
 import android.graphics.PorterDuffColorFilter;
@@ -23,6 +26,7 @@
 import android.util.AttributeSet;
 import android.widget.TextView;
 
+import androidx.constraintlayout.widget.ConstraintLayout;
 import androidx.core.graphics.ColorUtils;
 
 import com.android.launcher3.R;
@@ -38,8 +42,6 @@
     private final Drawable mLetterBackground;
     private final int mLetterListTextWidthAndHeight;
     private final int mTextColor;
-    private final int mBackgroundColor;
-    private final int mSelectedColor;
 
     public LetterListTextView(Context context) {
         this(context, null, 0);
@@ -55,8 +57,6 @@
         mLetterListTextWidthAndHeight = context.getResources().getDimensionPixelSize(
                 R.dimen.fastscroll_list_letter_size);
         mTextColor = Themes.getAttrColor(context, R.attr.materialColorOnSurface);
-        mBackgroundColor = Themes.getAttrColor(context, R.attr.materialColorSurfaceContainer);
-        mSelectedColor = Themes.getAttrColor(context, R.attr.materialColorOnSecondary);
     }
 
     @Override
@@ -71,6 +71,20 @@
     }
 
     /**
+     * Applies a viewId to the letter list text view and sets the background and text based on the
+     * sectionInfo.
+     */
+    public void apply(AlphabeticalAppsList.FastScrollSectionInfo fastScrollSectionInfo,
+            int viewId) {
+        setId(viewId);
+        setText(fastScrollSectionInfo.sectionName);
+        ConstraintLayout.LayoutParams lp = new ConstraintLayout.LayoutParams(
+                MATCH_CONSTRAINT, WRAP_CONTENT);
+        lp.dimensionRatio = "v,1:1";
+        setLayoutParams(lp);
+    }
+
+    /**
      * Animates the letter list text view based on the current finger position.
      *
      * @param currentFingerY The Y position of where the finger is placed on the fastScroller in
@@ -83,26 +97,11 @@
         float cutOffMin = currentFingerY - (getHeight() * 2);
         float cutOffMax = currentFingerY + (getHeight() * 2);
         float cutOffDistance = cutOffMax - cutOffMin;
-        // Update the background blend color
         boolean isWithinAnimationBounds = getY() < cutOffMax && getY() > cutOffMin;
-        if (isWithinAnimationBounds) {
-            getBackground().setColorFilter(new PorterDuffColorFilter(
-                    getBlendColorBasedOnYPosition(currentFingerY, cutOffDistance),
-                    PorterDuff.Mode.MULTIPLY));
-        } else {
-            getBackground().setColorFilter(new PorterDuffColorFilter(
-                    mBackgroundColor, PorterDuff.Mode.MULTIPLY));
-        }
         translateBasedOnYPosition(currentFingerY, cutOffDistance, isWithinAnimationBounds);
         scaleBasedOnYPosition(currentFingerY, cutOffDistance, isWithinAnimationBounds);
     }
 
-    private int getBlendColorBasedOnYPosition(int y, float cutOffDistance) {
-        float raisedCosineBlend = (float) Math.cos(((y - getY()) / (cutOffDistance)) * Math.PI);
-        float blendRatio = Utilities.boundToRange(raisedCosineBlend, 0f, 1f);
-        return ColorUtils.blendARGB(mBackgroundColor, mSelectedColor, blendRatio);
-    }
-
     private void scaleBasedOnYPosition(int y, float cutOffDistance,
             boolean isWithinAnimationBounds) {
         float raisedCosineScale = (float) Math.cos(((y - getY()) / (cutOffDistance)) * Math.PI)
diff --git a/src/com/android/launcher3/allapps/PrivateProfileManager.java b/src/com/android/launcher3/allapps/PrivateProfileManager.java
index e215cab..e1c1b39 100644
--- a/src/com/android/launcher3/allapps/PrivateProfileManager.java
+++ b/src/com/android/launcher3/allapps/PrivateProfileManager.java
@@ -16,6 +16,7 @@
 
 package com.android.launcher3.allapps;
 
+import static android.content.pm.LauncherUserInfo.PRIVATE_SPACE_ENTRYPOINT_HIDDEN;
 import static android.view.View.GONE;
 import static android.view.View.INVISIBLE;
 import static android.view.View.VISIBLE;
@@ -43,6 +44,7 @@
 import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
 import android.animation.ValueAnimator;
+import android.annotation.SuppressLint;
 import android.content.Context;
 import android.content.Intent;
 import android.os.UserHandle;
@@ -66,6 +68,7 @@
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Flags;
 import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimatedPropertySetter;
 import com.android.launcher3.anim.PropertySetter;
 import com.android.launcher3.icons.BitmapInfo;
@@ -77,11 +80,13 @@
 import com.android.launcher3.util.ApiWrapper;
 import com.android.launcher3.util.Preconditions;
 import com.android.launcher3.util.SettingsCache;
+import com.android.launcher3.util.UserIconInfo;
 import com.android.launcher3.views.ActivityContext;
 import com.android.launcher3.views.RecyclerViewFastScroller;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Objects;
 import java.util.function.Predicate;
 
 /**
@@ -208,9 +213,20 @@
     }
 
     /** Whether private profile should be hidden on Launcher. */
+    @SuppressLint("NewApi")
     public boolean isPrivateSpaceHidden() {
-        return getCurrentState() == STATE_DISABLED && SettingsCache.INSTANCE
-                    .get(mAllApps.getContext()).getValue(PRIVATE_SPACE_HIDE_WHEN_LOCKED_URI, 0);
+        UserHandle profileHandle = getProfileUser();
+        if (android.multiuser.Flags.addLauncherUserConfig() && !Objects.isNull(profileHandle)) {
+            UserIconInfo userInconInfo = UserCache.INSTANCE.get(mAllApps.getContext()).getUserInfo(
+                    profileHandle);
+
+            return getCurrentState() == STATE_DISABLED && userInconInfo.getUserConfig().getBoolean(
+                    PRIVATE_SPACE_ENTRYPOINT_HIDDEN, false);
+        }
+
+        return getCurrentState() == STATE_DISABLED && SettingsCache.INSTANCE.get(
+                mAllApps.getContext()).getValue(PRIVATE_SPACE_HIDE_WHEN_LOCKED_URI, 0);
+
     }
 
     /**
@@ -219,7 +235,7 @@
      * when animation is not running.
      */
     public void reset() {
-        // Ensure the state of the header views is what it should be before animating.
+        // Ensure the state of the header view is what it should be before animating.
         updateView();
         getMainRecyclerView().setChildAttachedConsumer(null);
         int previousState = getCurrentState();
@@ -434,6 +450,7 @@
                 lockPill.setVisibility(GONE);
             }
         }
+        mPSHeader.invalidate();
     }
 
     /** Sets the enablement of the profile when header or button is clicked. */
@@ -473,7 +490,8 @@
                 break;
             }
             // Make the private space apps gone to "collapse".
-            if (mFloatingMaskView == null && isPrivateSpaceItem(currentItem)) {
+            if ((mFloatingMaskView == null && isPrivateSpaceItem(currentItem)) ||
+                    currentItem.viewType == VIEW_TYPE_PRIVATE_SPACE_SYS_APPS_DIVIDER) {
                 RecyclerView.ViewHolder viewHolder =
                         allAppsRecyclerView.findViewHolderForAdapterPosition(i);
                 if (viewHolder != null) {
@@ -699,7 +717,9 @@
                 mAllApps.mAH.get(MAIN).mRecyclerView.removeItemDecoration(
                         mPrivateAppsSectionDecorator);
                 // Call onAppsUpdated() because it may be canceled when this animation occurs.
-                mAllApps.getPersonalAppList().onAppsUpdated();
+                if (!Utilities.isRunningInTestHarness()) {
+                    mAllApps.getPersonalAppList().onAppsUpdated();
+                }
                 if (isPrivateSpaceHidden()) {
                     // TODO (b/325455879): Figure out if we can avoid this.
                     getMainRecyclerView().getAdapter().notifyDataSetChanged();
diff --git a/src/com/android/launcher3/allapps/WorkModeSwitch.java b/src/com/android/launcher3/allapps/WorkModeSwitch.java
deleted file mode 100644
index 6049574..0000000
--- a/src/com/android/launcher3/allapps/WorkModeSwitch.java
+++ /dev/null
@@ -1,203 +0,0 @@
-/*
- * Copyright (C) 2020 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.allapps;
-
-import android.content.Context;
-import android.graphics.Rect;
-import android.util.AttributeSet;
-import android.view.WindowInsets;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import androidx.annotation.NonNull;
-import androidx.core.graphics.Insets;
-import androidx.core.view.WindowInsetsCompat;
-
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.Insettable;
-import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.anim.KeyboardInsetAnimationCallback;
-import com.android.launcher3.model.StringCache;
-import com.android.launcher3.views.ActivityContext;
-/**
- * Work profile toggle switch shown at the bottom of AllApps work tab
- */
-public class WorkModeSwitch extends LinearLayout implements Insettable,
-        KeyboardInsetAnimationCallback.KeyboardInsetListener {
-
-    private static final int FLAG_FADE_ONGOING = 1 << 1;
-    private static final int FLAG_TRANSLATION_ONGOING = 1 << 2;
-    private static final int FLAG_PROFILE_TOGGLE_ONGOING = 1 << 3;
-    private static final int SCROLL_THRESHOLD_DP = 10;
-
-    private final Rect mInsets = new Rect();
-    private final Rect mImeInsets = new Rect();
-    private int mFlags;
-    private final ActivityContext mActivityContext;
-    private final Context mContext;
-
-    // Threshold when user scrolls up/down to determine when should button extend/collapse
-    private final int mScrollThreshold;
-    private TextView mTextView;
-
-
-    public WorkModeSwitch(@NonNull Context context) {
-        this(context, null, 0);
-    }
-
-    public WorkModeSwitch(@NonNull Context context, @NonNull AttributeSet attrs) {
-        this(context, attrs, 0);
-    }
-
-    public WorkModeSwitch(@NonNull Context context, @NonNull AttributeSet attrs, int defStyleAttr) {
-        super(context, attrs, defStyleAttr);
-        mContext = context;
-        mScrollThreshold = Utilities.dpToPx(SCROLL_THRESHOLD_DP);
-        mActivityContext = ActivityContext.lookupContext(getContext());
-    }
-
-    @Override
-    protected void onFinishInflate() {
-        super.onFinishInflate();
-
-        mTextView = findViewById(R.id.pause_text);
-        setSelected(true);
-        KeyboardInsetAnimationCallback keyboardInsetAnimationCallback =
-                new KeyboardInsetAnimationCallback(this);
-        setWindowInsetsAnimationCallback(keyboardInsetAnimationCallback);
-
-        setInsets(mActivityContext.getDeviceProfile().getInsets());
-        updateStringFromCache();
-    }
-
-    @Override
-    public void setInsets(Rect insets) {
-        mInsets.set(insets);
-        updateTranslationY();
-        MarginLayoutParams lp = (MarginLayoutParams) getLayoutParams();
-        if (lp != null) {
-            int bottomMargin = getResources().getDimensionPixelSize(R.dimen.work_fab_margin_bottom);
-            DeviceProfile dp = ActivityContext.lookupContext(getContext()).getDeviceProfile();
-            if (mActivityContext.getAppsView().isSearchBarFloating()) {
-                bottomMargin += dp.hotseatQsbHeight;
-            }
-
-            if (!dp.isGestureMode && dp.isTaskbarPresent) {
-                bottomMargin += dp.taskbarHeight;
-            }
-
-            lp.bottomMargin = bottomMargin;
-        }
-    }
-
-    @Override
-    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
-        super.onLayout(changed, left, top, right, bottom);
-        boolean isRtl = Utilities.isRtl(getResources());
-        int shift = mActivityContext.getDeviceProfile().getAllAppsIconStartMargin(mContext);
-        setTranslationX(isRtl ? shift : -shift);
-    }
-
-    @Override
-    public boolean isEnabled() {
-        return super.isEnabled() && getVisibility() == VISIBLE && mFlags == 0;
-    }
-
-    public void animateVisibility(boolean visible) {
-        clearAnimation();
-        if (visible) {
-            setFlag(FLAG_FADE_ONGOING);
-            setVisibility(VISIBLE);
-            extend();
-            animate().alpha(1).withEndAction(() -> removeFlag(FLAG_FADE_ONGOING)).start();
-        } else if (getVisibility() != GONE) {
-            setFlag(FLAG_FADE_ONGOING);
-            animate().alpha(0).withEndAction(() -> {
-                removeFlag(FLAG_FADE_ONGOING);
-                setVisibility(GONE);
-            }).start();
-        }
-    }
-
-    @Override
-    public WindowInsets onApplyWindowInsets(WindowInsets insets) {
-        WindowInsetsCompat windowInsetsCompat =
-                WindowInsetsCompat.toWindowInsetsCompat(insets, this);
-        if (windowInsetsCompat.isVisible(WindowInsetsCompat.Type.ime())) {
-            setInsets(mImeInsets, windowInsetsCompat.getInsets(WindowInsetsCompat.Type.ime()));
-        } else {
-            mImeInsets.setEmpty();
-        }
-        updateTranslationY();
-        return super.onApplyWindowInsets(insets);
-    }
-
-    void updateTranslationY() {
-        setTranslationY(-mImeInsets.bottom);
-    }
-
-    @Override
-    public void setTranslationY(float translationY) {
-        // Always translate at least enough for nav bar insets.
-        super.setTranslationY(Math.min(translationY, -mInsets.bottom));
-    }
-
-    private void setInsets(Rect rect, Insets insets) {
-        rect.set(insets.left, insets.top, insets.right, insets.bottom);
-    }
-
-    public Rect getImeInsets() {
-        return mImeInsets;
-    }
-
-    @Override
-    public void onTranslationStart() {
-        setFlag(FLAG_TRANSLATION_ONGOING);
-    }
-
-    @Override
-    public void onTranslationEnd() {
-        removeFlag(FLAG_TRANSLATION_ONGOING);
-    }
-
-    private void setFlag(int flag) {
-        mFlags |= flag;
-    }
-
-    private void removeFlag(int flag) {
-        mFlags &= ~flag;
-    }
-
-    public void extend() {
-        mTextView.setVisibility(VISIBLE);
-    }
-
-    public void shrink(){
-        mTextView.setVisibility(GONE);
-    }
-
-    public int getScrollThreshold() {
-        return mScrollThreshold;
-    }
-
-    public void updateStringFromCache(){
-        StringCache cache = mActivityContext.getStringCache();
-        if (cache != null) {
-            mTextView.setText(cache.workProfilePauseButton);
-        }
-    }
-}
diff --git a/src/com/android/launcher3/allapps/WorkProfileManager.java b/src/com/android/launcher3/allapps/WorkProfileManager.java
index 96998a3..3d0c1d0 100644
--- a/src/com/android/launcher3/allapps/WorkProfileManager.java
+++ b/src/com/android/launcher3/allapps/WorkProfileManager.java
@@ -58,7 +58,7 @@
         implements PersonalWorkSlidingTabStrip.OnActivePageChangedListener {
     private static final String TAG = "WorkProfileManager";
     private final ActivityAllAppsContainerView<?> mAllApps;
-    private WorkModeSwitch mWorkModeSwitch;
+    private WorkUtilityView mWorkUtilityView;
     private final Predicate<UserHandle> mWorkProfileMatcher;
 
     public WorkProfileManager(
@@ -79,15 +79,15 @@
 
     @Override
     public void onActivePageChanged(int page) {
-        updateWorkFAB(page);
+        updateWorkUtilityViews(page);
     }
 
-    private void updateWorkFAB(int page) {
-        if (mWorkModeSwitch != null) {
+    private void updateWorkUtilityViews(int page) {
+        if (mWorkUtilityView != null) {
             if (page == MAIN || page == SEARCH) {
-                mWorkModeSwitch.animateVisibility(false);
+                mWorkUtilityView.animateVisibility(false);
             } else if (page == WORK && getCurrentState() == STATE_ENABLED) {
-                mWorkModeSwitch.animateVisibility(true);
+                mWorkUtilityView.animateVisibility(true);
             }
         }
     }
@@ -104,10 +104,10 @@
         }
         boolean isEnabled = !mAllApps.getAppsStore().hasModelFlag(quietModeFlag);
         updateCurrentState(isEnabled ? STATE_ENABLED : STATE_DISABLED);
-        if (mWorkModeSwitch != null) {
+        if (mWorkUtilityView != null) {
             // reset the position of the button and clear IME insets.
-            mWorkModeSwitch.getImeInsets().setEmpty();
-            mWorkModeSwitch.updateTranslationY();
+            mWorkUtilityView.getImeInsets().setEmpty();
+            mWorkUtilityView.updateTranslationY();
         }
     }
 
@@ -116,54 +116,54 @@
         if (getAH() != null) {
             getAH().mAppsList.updateAdapterItems();
         }
-        if (mWorkModeSwitch != null) {
-            updateWorkFAB(mAllApps.getCurrentPage());
+        if (mWorkUtilityView != null) {
+            updateWorkUtilityViews(mAllApps.getCurrentPage());
         }
         if (getCurrentState() == STATE_ENABLED) {
-            attachWorkModeSwitch();
+            attachWorkUtilityViews();
         } else if (getCurrentState() == STATE_DISABLED) {
-            detachWorkModeSwitch();
+            detachWorkUtilityViews();
         }
     }
 
     /**
      * Creates and attaches for profile toggle button to {@link ActivityAllAppsContainerView}
      */
-    public boolean attachWorkModeSwitch() {
+    public boolean attachWorkUtilityViews() {
         if (!mAllApps.getAppsStore().hasModelFlag(
                 FLAG_HAS_SHORTCUT_PERMISSION | FLAG_QUIET_MODE_CHANGE_PERMISSION)) {
             Log.e(TAG, "unable to attach work mode switch; Missing required permissions");
             return false;
         }
-        if (mWorkModeSwitch == null) {
-            mWorkModeSwitch = (WorkModeSwitch) mAllApps.getLayoutInflater().inflate(
-                    R.layout.work_mode_fab, mAllApps, false);
+        if (mWorkUtilityView == null) {
+            mWorkUtilityView = (WorkUtilityView) mAllApps.getLayoutInflater().inflate(
+                    R.layout.work_mode_utility_view, mAllApps, false);
         }
-        if (mWorkModeSwitch.getParent() == null) {
-            mAllApps.addView(mWorkModeSwitch);
+        if (mWorkUtilityView.getParent() == null) {
+            mAllApps.addView(mWorkUtilityView);
         }
         if (mAllApps.getCurrentPage() != WORK) {
-            mWorkModeSwitch.animateVisibility(false);
+            mWorkUtilityView.animateVisibility(false);
         }
         if (getAH() != null) {
             getAH().applyPadding();
         }
-        mWorkModeSwitch.setOnClickListener(this::onWorkFabClicked);
+        mWorkUtilityView.setOnClickListener(this::onWorkFabClicked);
         return true;
     }
     /**
      * Removes work profile toggle button from {@link ActivityAllAppsContainerView}
      */
-    public void detachWorkModeSwitch() {
-        if (mWorkModeSwitch != null && mWorkModeSwitch.getParent() == mAllApps) {
-            mAllApps.removeView(mWorkModeSwitch);
+    public void detachWorkUtilityViews() {
+        if (mWorkUtilityView != null && mWorkUtilityView.getParent() == mAllApps) {
+            mAllApps.removeView(mWorkUtilityView);
         }
-        mWorkModeSwitch = null;
+        mWorkUtilityView = null;
     }
 
     @Nullable
-    public WorkModeSwitch getWorkModeSwitch() {
-        return mWorkModeSwitch;
+    public WorkUtilityView getWorkUtilityView() {
+        return mWorkUtilityView;
     }
 
     private ActivityAllAppsContainerView.AdapterHolder getAH() {
@@ -199,7 +199,7 @@
     }
 
     private void onWorkFabClicked(View view) {
-        if (getCurrentState() == STATE_ENABLED && mWorkModeSwitch.isEnabled()) {
+        if (getCurrentState() == STATE_ENABLED && mWorkUtilityView.isEnabled()) {
             logEvents(LAUNCHER_TURN_OFF_WORK_APPS_TAP);
             setWorkProfileEnabled(false);
         }
@@ -216,7 +216,7 @@
             }
             @Override
             public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
-                WorkModeSwitch fab = getWorkModeSwitch();
+                WorkUtilityView fab = getWorkUtilityView();
                 if (fab == null){
                     return;
                 }
diff --git a/src/com/android/launcher3/allapps/WorkUtilityView.java b/src/com/android/launcher3/allapps/WorkUtilityView.java
new file mode 100644
index 0000000..4b58ab0
--- /dev/null
+++ b/src/com/android/launcher3/allapps/WorkUtilityView.java
@@ -0,0 +1,399 @@
+/*
+ * Copyright (C) 2020 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.allapps;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Rect;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.ViewGroup;
+import android.view.WindowInsets;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.core.graphics.Insets;
+import androidx.core.view.WindowInsetsCompat;
+
+import com.android.app.animation.Interpolators;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Flags;
+import com.android.launcher3.Insettable;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.AnimatedPropertySetter;
+import com.android.launcher3.anim.KeyboardInsetAnimationCallback;
+import com.android.launcher3.model.StringCache;
+import com.android.launcher3.views.ActivityContext;
+
+import java.util.ArrayList;
+
+/**
+ * Work profile utility ViewGroup that is shown at the bottom of AllApps work tab
+ */
+public class WorkUtilityView extends LinearLayout implements Insettable,
+        KeyboardInsetAnimationCallback.KeyboardInsetListener {
+
+    private static final int TEXT_EXPAND_OPACITY_DURATION = 300;
+    private static final int TEXT_COLLAPSE_OPACITY_DURATION = 50;
+    private static final int EXPAND_COLLAPSE_DURATION = 300;
+    private static final int TEXT_ALPHA_EXPAND_DELAY = 80;
+    private static final int TEXT_ALPHA_COLLAPSE_DELAY = 0;
+    private static final int WORK_SCHEDULER_OPACITY_DURATION =
+            (int) (EXPAND_COLLAPSE_DURATION * 0.75f);
+    private static final int FLAG_FADE_ONGOING = 1 << 1;
+    private static final int FLAG_TRANSLATION_ONGOING = 1 << 2;
+    private static final int FLAG_IS_EXPAND = 1 << 3;
+    private static final int SCROLL_THRESHOLD_DP = 10;
+    private static final float WORK_SCHEDULER_SCALE_MIN = 0.25f;
+    private static final float WORK_SCHEDULER_SCALE_MAX = 1f;
+
+    private final Rect mInsets = new Rect();
+    private final Rect mImeInsets = new Rect();
+    private int mFlags;
+    private final ActivityContext mActivityContext;
+    private final Context mContext;
+    private final int mTextMarginStart;
+    private final int mTextMarginEnd;
+    private final int mIconMarginStart;
+    private final String mWorkSchedulerIntentAction;
+
+    // Threshold when user scrolls up/down to determine when should button extend/collapse
+    private final int mScrollThreshold;
+    private ValueAnimator mPauseFABAnim;
+    private TextView mPauseText;
+    private ImageView mWorkIcon;
+    private ImageButton mSchedulerButton;
+
+    public WorkUtilityView(@NonNull Context context) {
+        this(context, null, 0);
+    }
+
+    public WorkUtilityView(@NonNull Context context, @NonNull AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public WorkUtilityView(@NonNull Context context, @NonNull AttributeSet attrs,
+            int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        mContext = context;
+        mScrollThreshold = Utilities.dpToPx(SCROLL_THRESHOLD_DP);
+        mActivityContext = ActivityContext.lookupContext(getContext());
+        mTextMarginStart = mContext.getResources().getDimensionPixelSize(
+                R.dimen.work_fab_text_start_margin);
+        mTextMarginEnd = mContext.getResources().getDimensionPixelSize(
+                R.dimen.work_fab_text_end_margin);
+        mIconMarginStart = mContext.getResources().getDimensionPixelSize(
+                R.dimen.work_fab_icon_start_margin_expanded);
+        mWorkSchedulerIntentAction = mContext.getResources().getString(
+                R.string.work_profile_scheduler_intent);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+
+        mPauseText = findViewById(R.id.pause_text);
+        mWorkIcon = findViewById(R.id.work_icon);
+        mSchedulerButton = findViewById(R.id.work_scheduler);
+        setSelected(true);
+        KeyboardInsetAnimationCallback keyboardInsetAnimationCallback =
+                new KeyboardInsetAnimationCallback(this);
+        setWindowInsetsAnimationCallback(keyboardInsetAnimationCallback);
+        // Expand is the default state upon initialization.
+        addFlag(FLAG_IS_EXPAND);
+        setInsets(mActivityContext.getDeviceProfile().getInsets());
+        updateStringFromCache();
+        mSchedulerButton.setVisibility(GONE);
+        if (shouldUseScheduler()) {
+            mSchedulerButton.setVisibility(VISIBLE);
+            mSchedulerButton.setOnClickListener(view ->
+                    mContext.startActivity(new Intent(mWorkSchedulerIntentAction)));
+        }
+    }
+
+    @Override
+    public void setInsets(Rect insets) {
+        mInsets.set(insets);
+        updateTranslationY();
+        MarginLayoutParams lp = (MarginLayoutParams) getLayoutParams();
+        if (lp != null) {
+            int bottomMargin = getResources().getDimensionPixelSize(R.dimen.work_fab_margin_bottom);
+            DeviceProfile dp = ActivityContext.lookupContext(getContext()).getDeviceProfile();
+            if (mActivityContext.getAppsView().isSearchBarFloating()) {
+                bottomMargin += dp.hotseatQsbHeight;
+            }
+
+            if (!dp.isGestureMode && dp.isTaskbarPresent) {
+                bottomMargin += dp.taskbarHeight;
+            }
+
+            lp.bottomMargin = bottomMargin;
+        }
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+        boolean isRtl = Utilities.isRtl(getResources());
+        int shift = mActivityContext.getDeviceProfile().getAllAppsIconStartMargin(mContext);
+        setTranslationX(isRtl ? shift : -shift);
+    }
+
+    @Override
+    public boolean isEnabled() {
+        return super.isEnabled() && getVisibility() == VISIBLE;
+    }
+
+    public void animateVisibility(boolean visible) {
+        clearAnimation();
+        if (visible) {
+            addFlag(FLAG_FADE_ONGOING);
+            setVisibility(VISIBLE);
+            extend();
+            animate().alpha(1).withEndAction(() -> removeFlag(FLAG_FADE_ONGOING)).start();
+        } else if (getVisibility() != GONE) {
+            addFlag(FLAG_FADE_ONGOING);
+            animate().alpha(0).withEndAction(() -> {
+                removeFlag(FLAG_FADE_ONGOING);
+                setVisibility(GONE);
+            }).start();
+        }
+    }
+
+    @Override
+    public WindowInsets onApplyWindowInsets(WindowInsets insets) {
+        WindowInsetsCompat windowInsetsCompat =
+                WindowInsetsCompat.toWindowInsetsCompat(insets, this);
+        if (windowInsetsCompat.isVisible(WindowInsetsCompat.Type.ime())) {
+            setInsets(mImeInsets, windowInsetsCompat.getInsets(WindowInsetsCompat.Type.ime()));
+            shrink();
+        } else {
+            mImeInsets.setEmpty();
+            extend();
+        }
+        updateTranslationY();
+        return super.onApplyWindowInsets(insets);
+    }
+
+    void updateTranslationY() {
+        setTranslationY(-mImeInsets.bottom);
+    }
+
+    @Override
+    public void setTranslationY(float translationY) {
+        // Always translate at least enough for nav bar insets.
+        super.setTranslationY(Math.min(translationY, -mInsets.bottom));
+    }
+
+    private ValueAnimator animateSchedulerScale(boolean isExpanding) {
+        float scaleFrom = isExpanding ? WORK_SCHEDULER_SCALE_MIN : WORK_SCHEDULER_SCALE_MAX;
+        float scaleTo = isExpanding ? WORK_SCHEDULER_SCALE_MAX : WORK_SCHEDULER_SCALE_MIN;
+        ValueAnimator schedulerScaleAnim = ObjectAnimator.ofFloat(scaleFrom, scaleTo);
+        schedulerScaleAnim.setDuration(EXPAND_COLLAPSE_DURATION);
+        schedulerScaleAnim.setInterpolator(Interpolators.STANDARD);
+        schedulerScaleAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator valueAnimator) {
+                float scale = (float) valueAnimator.getAnimatedValue();
+                mSchedulerButton.setScaleX(scale);
+                mSchedulerButton.setScaleY(scale);
+            }
+        });
+        schedulerScaleAnim.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationStart(Animator animation) {
+                if (isExpanding) {
+                    mSchedulerButton.setVisibility(VISIBLE);
+                }
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                if (!isExpanding) {
+                    mSchedulerButton.setVisibility(GONE);
+                }
+            }
+        });
+        return schedulerScaleAnim;
+    }
+
+    private ValueAnimator animateSchedulerAlpha(boolean isExpanding) {
+        float alphaFrom = isExpanding ? 0 : 1;
+        float alphaTo = isExpanding ? 1 : 0;
+        ValueAnimator schedulerAlphaAnim = ObjectAnimator.ofFloat(alphaFrom, alphaTo);
+        schedulerAlphaAnim.setDuration(WORK_SCHEDULER_OPACITY_DURATION);
+        schedulerAlphaAnim.setStartDelay(isExpanding ? 0 :
+                EXPAND_COLLAPSE_DURATION - WORK_SCHEDULER_OPACITY_DURATION);
+        schedulerAlphaAnim.setInterpolator(Interpolators.STANDARD);
+        schedulerAlphaAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator valueAnimator) {
+                mSchedulerButton.setAlpha((float) valueAnimator.getAnimatedValue());
+            }
+        });
+        return schedulerAlphaAnim;
+    }
+
+    private void animateWorkUtilityViews(boolean isExpanding) {
+        if (!shouldAnimate(isExpanding)) {
+            return;
+        }
+        AnimatorSet animatorSet = new AnimatedPropertySetter().buildAnim();
+        mPauseText.measure(0,0);
+        int currentWidth = mPauseText.getWidth();
+        int fullWidth = mPauseText.getMeasuredWidth();
+        float from = isExpanding ? 0 : currentWidth;
+        float to = isExpanding ? fullWidth : 0;
+        mPauseFABAnim = ObjectAnimator.ofFloat(from, to);
+        mPauseFABAnim.setDuration(EXPAND_COLLAPSE_DURATION);
+        mPauseFABAnim.setInterpolator(Interpolators.STANDARD);
+        mPauseFABAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator valueAnimator) {
+                float translation = (float) valueAnimator.getAnimatedValue();
+                float translationFraction = translation / fullWidth;
+                ViewGroup.MarginLayoutParams textViewLayoutParams =
+                        (ViewGroup.MarginLayoutParams) mPauseText.getLayoutParams();
+                textViewLayoutParams.width = (int) translation;
+                textViewLayoutParams.setMarginStart((int) (mTextMarginStart * translationFraction));
+                textViewLayoutParams.setMarginEnd((int) (mTextMarginEnd * translationFraction));
+                mPauseText.setLayoutParams(textViewLayoutParams);
+                ViewGroup.MarginLayoutParams iconLayoutParams =
+                        (ViewGroup.MarginLayoutParams) mWorkIcon.getLayoutParams();
+                iconLayoutParams.setMarginStart((int) (mIconMarginStart * translationFraction));
+                mWorkIcon.setLayoutParams(iconLayoutParams);
+            }
+        });
+        mPauseFABAnim.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animator) {
+                if (isExpanding) {
+                    addFlag(FLAG_IS_EXPAND);
+                } else {
+                    mPauseText.setVisibility(GONE);
+                    removeFlag(FLAG_IS_EXPAND);
+                }
+                mPauseText.setHorizontallyScrolling(false);
+                mPauseText.setEllipsize(TextUtils.TruncateAt.END);
+            }
+
+            @Override
+            public void onAnimationStart(Animator animator) {
+                mPauseText.setHorizontallyScrolling(true);
+                mPauseText.setVisibility(VISIBLE);
+                mPauseText.setEllipsize(null);
+            }
+        });
+        ArrayList<Animator> animatorList = new ArrayList<>();
+        animatorList.add(mPauseFABAnim);
+        animatorList.add(updatePauseTextAlpha(isExpanding));
+        if (shouldUseScheduler()) {
+            animatorList.add(animateSchedulerScale(isExpanding));
+            animatorList.add(animateSchedulerAlpha(isExpanding));
+        }
+        animatorSet.playTogether(animatorList);
+        animatorSet.start();
+    }
+
+
+    private ValueAnimator updatePauseTextAlpha(boolean expand) {
+        float from = expand ? 0 : 1;
+        float to = expand ? 1 : 0;
+        ValueAnimator alphaAnim = ObjectAnimator.ofFloat(from, to);
+        alphaAnim.setDuration(expand ? TEXT_EXPAND_OPACITY_DURATION
+                : TEXT_COLLAPSE_OPACITY_DURATION);
+        alphaAnim.setStartDelay(expand ? TEXT_ALPHA_EXPAND_DELAY : TEXT_ALPHA_COLLAPSE_DELAY);
+        alphaAnim.setInterpolator(Interpolators.LINEAR);
+        alphaAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator valueAnimator) {
+                mPauseText.setAlpha((float) valueAnimator.getAnimatedValue());
+            }
+        });
+        return alphaAnim;
+    }
+
+    private void setInsets(Rect rect, Insets insets) {
+        rect.set(insets.left, insets.top, insets.right, insets.bottom);
+    }
+
+    public Rect getImeInsets() {
+        return mImeInsets;
+    }
+
+    @Override
+    public void onTranslationStart() {
+        addFlag(FLAG_TRANSLATION_ONGOING);
+    }
+
+    @Override
+    public void onTranslationEnd() {
+        removeFlag(FLAG_TRANSLATION_ONGOING);
+    }
+
+    private void addFlag(int flag) {
+        mFlags |= flag;
+    }
+
+    private void removeFlag(int flag) {
+        mFlags &= ~flag;
+    }
+
+    private boolean containsFlag(int flag) {
+        return (mFlags & flag) == flag;
+    }
+
+    public void extend() {
+        animateWorkUtilityViews(true);
+    }
+
+    public void shrink() {
+        animateWorkUtilityViews(false);
+    }
+
+    /**
+     * Determines if the button should animate based on current state. It should animate the button
+     * only if it is not in the same state it is animating to.
+     */
+    private boolean shouldAnimate(boolean expanding) {
+        return expanding != containsFlag(FLAG_IS_EXPAND)
+                && (mPauseFABAnim == null || !mPauseFABAnim.isRunning());
+    }
+
+    public int getScrollThreshold() {
+        return mScrollThreshold;
+    }
+
+    public void updateStringFromCache(){
+        StringCache cache = mActivityContext.getStringCache();
+        if (cache != null) {
+            mPauseText.setText(cache.workProfilePauseButton);
+        }
+    }
+
+    private boolean shouldUseScheduler() {
+        return Flags.workSchedulerInWorkProfile() && !mWorkSchedulerIntentAction.isEmpty();
+    }
+}
diff --git a/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java b/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java
index de3bb9e..7e3e392 100644
--- a/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java
+++ b/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java
@@ -118,8 +118,14 @@
     @Override
     public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
 
-        if (actionId == EditorInfo.IME_ACTION_SEARCH || actionId == EditorInfo.IME_ACTION_GO) {
-            Log.i(TAG, "User tapped ime search button");
+        if (actionId == EditorInfo.IME_ACTION_SEARCH || actionId == EditorInfo.IME_ACTION_GO || (
+                actionId == EditorInfo.IME_NULL && event != null
+                        && event.getAction() == KeyEvent.ACTION_DOWN)) {
+            if (actionId == EditorInfo.IME_NULL) {
+                Log.i(TAG, "User pressed ENTER key");
+            } else {
+                Log.i(TAG, "User tapped ime search button");
+            }
             // selectFocusedView should return SearchTargetEvent that is passed onto onClick
             return mLauncher.getAppsView().getMainAdapterProvider().launchHighlightedItem();
         }
diff --git a/src/com/android/launcher3/backuprestore/LauncherRestoreEventLogger.kt b/src/com/android/launcher3/backuprestore/LauncherRestoreEventLogger.kt
index e6654b1..b05539a 100644
--- a/src/com/android/launcher3/backuprestore/LauncherRestoreEventLogger.kt
+++ b/src/com/android/launcher3/backuprestore/LauncherRestoreEventLogger.kt
@@ -24,9 +24,10 @@
         RestoreError.WIDGETS_DISABLED,
         RestoreError.PROFILE_NOT_RESTORED,
         RestoreError.WIDGET_REMOVED,
+        RestoreError.DATABASE_FILE_NOT_RESTORED,
         RestoreError.GRID_MIGRATION_FAILURE,
         RestoreError.NO_SEARCH_WIDGET,
-        RestoreError.INVALID_WIDGET_ID
+        RestoreError.INVALID_WIDGET_ID,
     )
     annotation class RestoreError {
         companion object {
@@ -38,6 +39,7 @@
             const val APP_NOT_INSTALLED = "app_not_installed"
             const val WIDGETS_DISABLED = "widgets_disabled"
             const val PROFILE_NOT_RESTORED = "profile_not_restored"
+            const val DATABASE_FILE_NOT_RESTORED = "db_file_not_restored"
             const val WIDGET_REMOVED = "widget_not_found"
             const val GRID_MIGRATION_FAILURE = "grid_migration_failed"
             const val NO_SEARCH_WIDGET = "no_search_widget"
@@ -52,7 +54,7 @@
             return ResourceBasedOverride.Overrides.getObject(
                 LauncherRestoreEventLogger::class.java,
                 context,
-                R.string.launcher_restore_event_logger_class
+                R.string.launcher_restore_event_logger_class,
             )
         }
     }
@@ -117,7 +119,7 @@
     open fun logFavoritesItemsRestoreFailed(
         favoritesId: Int,
         count: Int,
-        @RestoreError error: String?
+        @RestoreError error: String?,
     ) {
         // no-op
     }
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index 8fe1b34..9e38824 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -62,17 +62,6 @@
      * and set a default value for the flag. This will be the default value on Debug builds.
      * <p>
      */
-    // TODO(Block 3): Clean up flags
-    public static final BooleanFlag ENABLE_WORKSPACE_LOADING_OPTIMIZATION = getDebugFlag(251502424,
-            "ENABLE_WORKSPACE_LOADING_OPTIMIZATION", DISABLED,
-            "load the current workspace screen visible to the user before the rest rather than "
-                    + "loading all of them at once.");
-
-    public static final BooleanFlag CHANGE_MODEL_DELEGATE_LOADING_ORDER = getDebugFlag(251502424,
-            "CHANGE_MODEL_DELEGATE_LOADING_ORDER", DISABLED,
-            "changes the timing of the loading and binding of delegate items during "
-                    + "data preparation for loading the home screen");
-
     // TODO(Block 6): Clean up flags
     public static final BooleanFlag SECONDARY_DRAG_N_DROP_TO_PIN = getDebugFlag(270395140,
             "SECONDARY_DRAG_N_DROP_TO_PIN", DISABLED,
diff --git a/src/com/android/launcher3/contextualeducation/ContextualEduStatsManager.java b/src/com/android/launcher3/contextualeducation/ContextualEduStatsManager.java
index da13546..5664174 100644
--- a/src/com/android/launcher3/contextualeducation/ContextualEduStatsManager.java
+++ b/src/com/android/launcher3/contextualeducation/ContextualEduStatsManager.java
@@ -16,22 +16,25 @@
 
 package com.android.launcher3.contextualeducation;
 
-import static com.android.launcher3.util.MainThreadInitializedObject.forOverride;
-
-import com.android.launcher3.R;
-import com.android.launcher3.util.MainThreadInitializedObject;
-import com.android.launcher3.util.ResourceBasedOverride;
-import com.android.launcher3.util.SafeCloseable;
+import com.android.launcher3.dagger.LauncherAppSingleton;
+import com.android.launcher3.dagger.LauncherBaseAppComponent;
+import com.android.launcher3.util.DaggerSingletonObject;
 import com.android.systemui.contextualeducation.GestureType;
 
+import javax.inject.Inject;
+
 /**
  * A class to update contextual education data. It is a no-op implementation and could be
- * overridden by changing the resource value [R.string.contextual_edu_manager_class] to provide
- * a real implementation.
+ * overridden through dagger modules to provide a real implementation.
  */
-public class ContextualEduStatsManager implements ResourceBasedOverride, SafeCloseable {
-    public static final MainThreadInitializedObject<ContextualEduStatsManager> INSTANCE =
-            forOverride(ContextualEduStatsManager.class, R.string.contextual_edu_manager_class);
+@LauncherAppSingleton
+public class ContextualEduStatsManager {
+    public static final DaggerSingletonObject<ContextualEduStatsManager> INSTANCE =
+            new DaggerSingletonObject<>(LauncherBaseAppComponent::getContextualEduStatsManager);
+
+    @Inject
+    public ContextualEduStatsManager() { }
+
 
     /**
      * Updates contextual education stats when a gesture is triggered
@@ -40,8 +43,4 @@
      */
     public void updateEduStats(boolean isTrackpadGesture, GestureType gestureType) {
     }
-
-    @Override
-    public void close() {
-    }
 }
diff --git a/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java b/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
index 0a50e8b..fb486f7 100644
--- a/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
+++ b/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
@@ -18,7 +18,20 @@
 
 import android.content.Context;
 
+import com.android.launcher3.contextualeducation.ContextualEduStatsManager;
+import com.android.launcher3.graphics.IconShape;
+import com.android.launcher3.model.ItemInstallQueue;
+import com.android.launcher3.pm.InstallSessionHelper;
+import com.android.launcher3.util.ApiWrapper;
 import com.android.launcher3.util.DaggerSingletonTracker;
+import com.android.launcher3.util.MSDLPlayerWrapper;
+import com.android.launcher3.util.PackageManagerHelper;
+import com.android.launcher3.util.PluginManagerWrapper;
+import com.android.launcher3.util.ScreenOnTracker;
+import com.android.launcher3.util.SettingsCache;
+import com.android.launcher3.util.VibratorWrapper;
+import com.android.launcher3.util.window.RefreshRateTracker;
+import com.android.launcher3.widget.custom.CustomWidgetManager;
 
 import dagger.BindsInstance;
 
@@ -32,6 +45,20 @@
  */
 public interface LauncherBaseAppComponent {
     DaggerSingletonTracker getDaggerSingletonTracker();
+    ApiWrapper getApiWrapper();
+    ContextualEduStatsManager getContextualEduStatsManager();
+    CustomWidgetManager getCustomWidgetManager();
+    IconShape getIconShape();
+    InstallSessionHelper getInstallSessionHelper();
+    ItemInstallQueue getItemInstallQueue();
+    RefreshRateTracker getRefreshRateTracker();
+    ScreenOnTracker getScreenOnTracker();
+    SettingsCache getSettingsCache();
+    PackageManagerHelper getPackageManagerHelper();
+    PluginManagerWrapper getPluginManagerWrapper();
+    VibratorWrapper getVibratorWrapper();
+    MSDLPlayerWrapper getMSDLPlayerWrapper();
+
     /** Builder for LauncherBaseAppComponent. */
     interface Builder {
         @BindsInstance Builder appContext(@ApplicationContext Context context);
diff --git a/src/com/android/launcher3/dragndrop/AddItemActivity.java b/src/com/android/launcher3/dragndrop/AddItemActivity.java
index 85eb39b..25de479 100644
--- a/src/com/android/launcher3/dragndrop/AddItemActivity.java
+++ b/src/com/android/launcher3/dragndrop/AddItemActivity.java
@@ -68,7 +68,7 @@
 import com.android.launcher3.model.data.PackageItemInfo;
 import com.android.launcher3.pm.PinRequestHelper;
 import com.android.launcher3.util.ApiWrapper;
-import com.android.launcher3.util.PackageManagerHelper;
+import com.android.launcher3.util.ApplicationInfoWrapper;
 import com.android.launcher3.util.SystemUiController;
 import com.android.launcher3.views.AbstractSlideInView;
 import com.android.launcher3.views.BaseDragLayer;
@@ -164,8 +164,8 @@
             finish();
             return;
         }
-        ApplicationInfo info = PackageManagerHelper.INSTANCE.get(this)
-                .getApplicationInfo(targetApp.packageName, targetApp.user, 0);
+        ApplicationInfo info = new ApplicationInfoWrapper(
+                this, targetApp.packageName, targetApp.user).getInfo();
         if (info == null) {
             finish();
             return;
@@ -281,7 +281,7 @@
                 new PinShortcutRequestActivityInfo(mRequest, this);
         mWidgetCell.getWidgetView().setTag(new PendingAddShortcutInfo(shortcutInfo));
         applyWidgetItemAsync(
-                () -> new WidgetItem(shortcutInfo, mApp.getIconCache(), getPackageManager()));
+                () -> new WidgetItem(shortcutInfo, mApp.getIconCache()));
         return new PackageItemInfo(mRequest.getShortcutInfo().getPackage(),
                 mRequest.getShortcutInfo().getUserHandle());
     }
diff --git a/src/com/android/launcher3/dragndrop/BaseItemDragListener.java b/src/com/android/launcher3/dragndrop/BaseItemDragListener.java
index 981e3a6..43c148a 100644
--- a/src/com/android/launcher3/dragndrop/BaseItemDragListener.java
+++ b/src/com/android/launcher3/dragndrop/BaseItemDragListener.java
@@ -34,7 +34,7 @@
 import com.android.launcher3.DragSource;
 import com.android.launcher3.DropTarget.DragObject;
 import com.android.launcher3.Launcher;
-import com.android.launcher3.util.ActivityTracker.SchedulerCallback;
+import com.android.launcher3.util.ContextTracker.SchedulerCallback;
 import com.android.launcher3.widget.PendingItemDragHelper;
 
 import java.util.UUID;
@@ -74,9 +74,9 @@
     }
 
     @Override
-    public boolean init(Launcher launcher, boolean alreadyOnHome) {
-        AbstractFloatingView.closeAllOpenViews(launcher, alreadyOnHome);
-        launcher.getStateManager().goToState(NORMAL, alreadyOnHome /* animated */);
+    public boolean init(Launcher launcher, boolean isHomeStarted) {
+        AbstractFloatingView.closeAllOpenViews(launcher, /* animate= */ isHomeStarted);
+        launcher.getStateManager().goToState(NORMAL, /* animated= */ isHomeStarted);
         launcher.getDragLayer().setOnDragListener(this);
         launcher.getRotationHelper().setStateHandlerRequest(REQUEST_LOCK);
 
diff --git a/src/com/android/launcher3/dragndrop/DragLayer.java b/src/com/android/launcher3/dragndrop/DragLayer.java
index 8b1f42b..a24f3ff 100644
--- a/src/com/android/launcher3/dragndrop/DragLayer.java
+++ b/src/com/android/launcher3/dragndrop/DragLayer.java
@@ -121,7 +121,7 @@
 
     @Override
     public void recreateControllers() {
-        mControllers = mActivity.createTouchControllers();
+        mControllers = mContainer.createTouchControllers();
     }
 
     public ViewGroupFocusHelper getFocusIndicatorHelper() {
@@ -134,15 +134,15 @@
     }
 
     private boolean isEventOverAccessibleDropTargetBar(MotionEvent ev) {
-        return isInAccessibleDrag() && isEventOverView(mActivity.getDropTargetBar(), ev);
+        return isInAccessibleDrag() && isEventOverView(mContainer.getDropTargetBar(), ev);
     }
 
     @Override
     public boolean onInterceptHoverEvent(MotionEvent ev) {
-        if (mActivity == null || mActivity.getWorkspace() == null) {
+        if (mContainer == null || mContainer.getWorkspace() == null) {
             return false;
         }
-        AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(mActivity);
+        AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(mContainer);
         if (!(topView instanceof Folder)) {
             return false;
         } else {
@@ -197,7 +197,7 @@
 
 
     private boolean isInAccessibleDrag() {
-        return mActivity.getAccessibilityDelegate().isInAccessibleDrag();
+        return mContainer.getAccessibilityDelegate().isInAccessibleDrag();
     }
 
     @Override
@@ -210,12 +210,12 @@
 
     @Override
     public void addChildrenForAccessibility(ArrayList<View> childrenForAccessibility) {
-        View topView = AbstractFloatingView.getTopOpenViewWithType(mActivity,
+        View topView = AbstractFloatingView.getTopOpenViewWithType(mContainer,
                 AbstractFloatingView.TYPE_ACCESSIBLE);
         if (topView != null) {
             addAccessibleChildToList(topView, childrenForAccessibility);
             if (isInAccessibleDrag()) {
-                addAccessibleChildToList(mActivity.getDropTargetBar(), childrenForAccessibility);
+                addAccessibleChildToList(mContainer.getDropTargetBar(), childrenForAccessibility);
             }
         } else {
             super.addChildrenForAccessibility(childrenForAccessibility);
@@ -420,14 +420,14 @@
     public void onViewAdded(View child) {
         super.onViewAdded(child);
         updateChildIndices();
-        mActivity.onDragLayerHierarchyChanged();
+        mContainer.onDragLayerHierarchyChanged();
     }
 
     @Override
     public void onViewRemoved(View child) {
         super.onViewRemoved(child);
         updateChildIndices();
-        mActivity.onDragLayerHierarchyChanged();
+        mContainer.onDragLayerHierarchyChanged();
     }
 
     @Override
diff --git a/src/com/android/launcher3/dragndrop/LauncherDragController.java b/src/com/android/launcher3/dragndrop/LauncherDragController.java
index 29fc613..4aa3673 100644
--- a/src/com/android/launcher3/dragndrop/LauncherDragController.java
+++ b/src/com/android/launcher3/dragndrop/LauncherDragController.java
@@ -119,6 +119,9 @@
                         initialDragViewScale,
                         dragViewScaleOnDrop,
                         scalePx);
+        // During a drag, we don't want to expose the descendendants of drag view to a11y users,
+        // since those decendents are not a valid position in the workspace.
+        dragView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
         dragView.setItemInfo(dragInfo);
         mDragObject.dragComplete = false;
 
diff --git a/src/com/android/launcher3/dragndrop/PinShortcutRequestActivityInfo.java b/src/com/android/launcher3/dragndrop/PinShortcutRequestActivityInfo.java
index 0f3cad6..a6a50d7 100644
--- a/src/com/android/launcher3/dragndrop/PinShortcutRequestActivityInfo.java
+++ b/src/com/android/launcher3/dragndrop/PinShortcutRequestActivityInfo.java
@@ -30,7 +30,6 @@
 import android.content.Intent;
 import android.content.pm.LauncherApps;
 import android.content.pm.LauncherApps.PinItemRequest;
-import android.content.pm.PackageManager;
 import android.content.pm.ShortcutInfo;
 import android.graphics.drawable.Drawable;
 import android.os.Build;
@@ -40,7 +39,7 @@
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.R;
-import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.icons.cache.BaseIconCache;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.pm.PinRequestHelper;
 import com.android.launcher3.pm.ShortcutConfigActivityInfo;
@@ -69,7 +68,8 @@
 
     public PinShortcutRequestActivityInfo(
             ShortcutInfo si, Supplier<PinItemRequest> requestSupplier, Context context) {
-        super(new ComponentName(si.getPackage(), STUB_COMPONENT_CLASS), si.getUserHandle());
+        super(new ComponentName(si.getPackage(), STUB_COMPONENT_CLASS),
+                si.getUserHandle(), context);
         mRequestSupplier = requestSupplier;
         mInfo = si;
         mContext = context;
@@ -81,12 +81,12 @@
     }
 
     @Override
-    public CharSequence getLabel(PackageManager pm) {
+    public CharSequence getLabel() {
         return mInfo.getShortLabel();
     }
 
     @Override
-    public Drawable getFullResIcon(IconCache cache) {
+    public Drawable getFullResIcon(BaseIconCache cache) {
         Drawable d = mContext.getSystemService(LauncherApps.class)
                 .getShortcutIconDrawable(mInfo, LauncherAppState.getIDP(mContext).fillResIconDpi);
         if (d == null) {
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index 7bec768..5defef3 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -317,10 +317,7 @@
                 | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
                 | InputType.TYPE_TEXT_FLAG_CAP_WORDS);
         mFolderName.forceDisableSuggestions(true);
-        mFolderName.setPadding(mFolderName.getPaddingLeft(),
-                (getFooterHeight() - mFolderName.getLineHeight()) / 2,
-                mFolderName.getPaddingRight(),
-                (getFooterHeight() - mFolderName.getLineHeight()) / 2);
+
 
         mKeyboardInsetAnimationCallback = new KeyboardInsetAnimationCallback(this);
         setWindowInsetsAnimationCallback(mKeyboardInsetAnimationCallback);
diff --git a/src/com/android/launcher3/folder/FolderPagedView.java b/src/com/android/launcher3/folder/FolderPagedView.java
index 9dc2d24..fe26194 100644
--- a/src/com/android/launcher3/folder/FolderPagedView.java
+++ b/src/com/android/launcher3/folder/FolderPagedView.java
@@ -373,8 +373,9 @@
         // Update footer
         mPageIndicator.setVisibility(getPageCount() > 1 ? View.VISIBLE : View.GONE);
         // Set the gravity as LEFT or RIGHT instead of START, as START depends on the actual text.
-        mFolder.getFolderName().setGravity(getPageCount() > 1
-                ? (mIsRtl ? Gravity.RIGHT : Gravity.LEFT) : Gravity.CENTER_HORIZONTAL);
+        int horizontalGravity = getPageCount() > 1
+                ? (mIsRtl ? Gravity.RIGHT : Gravity.LEFT) : Gravity.CENTER_HORIZONTAL;
+        mFolder.getFolderName().setGravity(horizontalGravity | Gravity.CENTER_VERTICAL);
     }
 
     public int getDesiredWidth() {
diff --git a/src/com/android/launcher3/graphics/GridCustomizationsProvider.java b/src/com/android/launcher3/graphics/GridCustomizationsProvider.java
index 531cdfd..7367f2e 100644
--- a/src/com/android/launcher3/graphics/GridCustomizationsProvider.java
+++ b/src/com/android/launcher3/graphics/GridCustomizationsProvider.java
@@ -30,14 +30,13 @@
 import android.os.Binder;
 import android.os.Bundle;
 import android.os.Handler;
-import android.os.IBinder;
 import android.os.IBinder.DeathRecipient;
 import android.os.Message;
 import android.os.Messenger;
 import android.text.TextUtils;
-import android.util.ArrayMap;
 import android.util.Log;
-import android.util.Pair;
+
+import androidx.annotation.NonNull;
 
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.InvariantDeviceProfile.GridOption;
@@ -45,39 +44,62 @@
 import com.android.launcher3.LauncherModel;
 import com.android.launcher3.LauncherPrefs;
 import com.android.launcher3.model.BgDataModel;
+import com.android.launcher3.shapes.AppShape;
+import com.android.launcher3.shapes.AppShapesProvider;
 import com.android.launcher3.util.Executors;
 import com.android.launcher3.util.Preconditions;
+import com.android.launcher3.util.RunnableList;
 import com.android.systemui.shared.Flags;
 
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.WeakHashMap;
 import java.util.concurrent.ExecutionException;
 
 /**
  * Exposes various launcher grid options and allows the caller to change them.
  * APIs:
- *      /list_options: List the various available grip options, has following columns
- *          name: name of the grid
+ *      /shape_options: List of various available shape options, where each has following fields
+ *          shape_key: key of the shape option
+ *          title: translated title of the shape option
+ *          path: path of the shape, assuming drawn on 100x100 view port
+ *          is_default: true if this shape option is currently set to the system
+ *
+ *      /list_options: List the various available grid options, where each has following fields
+ *          name: key of the grid option
  *          rows: number of rows in the grid
  *          cols: number of columns in the grid
  *          preview_count: number of previews available for this grid option. The preview uri
  *                         looks like /preview/<grid-name>/<preview index starting with 0>
- *          is_default: true if this grid is currently active
+ *          is_default: true if this grid option is currently set to the system
  *
- *     /preview: Opens a file stream for the grid preview
+ *     /get_preview: Open a file stream for the grid preview
  *
- *     /default_grid: Call update to set the current grid, with values
- *          name: name of the grid to apply
+ *     /default_grid: Call update to set the current shape and grid, with values
+ *          shape_key: key of the shape to apply
+ *          name: key of the grid to apply
  */
 public class GridCustomizationsProvider extends ContentProvider {
 
     private static final String TAG = "GridCustomizationsProvider";
 
     private static final String KEY_NAME = "name";
+    private static final String KEY_GRID_TITLE = "grid_title";
     private static final String KEY_ROWS = "rows";
     private static final String KEY_COLS = "cols";
     private static final String KEY_PREVIEW_COUNT = "preview_count";
+    // is_default means if a certain option is currently set to the system
     private static final String KEY_IS_DEFAULT = "is_default";
+    private static final String KEY_SHAPE_KEY = "shape_key";
+    private static final String KEY_SHAPE_TITLE = "shape_title";
+    private static final String KEY_PATH = "path";
 
+    // list_options is the key for grid option list
     private static final String KEY_LIST_OPTIONS = "/list_options";
+    private static final String KEY_SHAPE_OPTIONS = "/shape_options";
+    // default_grid is for setting grid and shape to system settings
     private static final String KEY_DEFAULT_GRID = "/default_grid";
 
     private static final String METHOD_GET_PREVIEW = "get_preview";
@@ -93,13 +115,13 @@
     public static final String KEY_GRID_NAME = "grid_name";
 
     private static final int MESSAGE_ID_UPDATE_PREVIEW = 1337;
+    private static final int MESSAGE_ID_UPDATE_SHAPE = 2586;
     private static final int MESSAGE_ID_UPDATE_GRID = 7414;
+    private static final int MESSAGE_ID_UPDATE_COLOR = 856;
 
-    /**
-     * Here we use the IBinder and the screen ID as the key of the active previews.
-     */
-    private final ArrayMap<Pair<IBinder, Integer>, PreviewLifecycleObserver> mActivePreviews =
-            new ArrayMap<>();
+    // Set of all active previews used to track duplicate memory allocations
+    private final Set<PreviewLifecycleObserver> mActivePreviews =
+            Collections.newSetFromMap(new WeakHashMap<>());
 
     @Override
     public boolean onCreate() {
@@ -109,14 +131,42 @@
     @Override
     public Cursor query(Uri uri, String[] projection, String selection,
             String[] selectionArgs, String sortOrder) {
-        switch (uri.getPath()) {
+        Context context = getContext();
+        String path = uri.getPath();
+        if (context == null || path == null) {
+            return null;
+        }
+
+        switch (path) {
+            case KEY_SHAPE_OPTIONS: {
+                if (Flags.newCustomizationPickerUi()) {
+                    MatrixCursor cursor = new MatrixCursor(new String[]{
+                            KEY_SHAPE_KEY, KEY_SHAPE_TITLE, KEY_PATH, KEY_IS_DEFAULT});
+                    List<AppShape> shapes =  AppShapesProvider.INSTANCE.getShapes();
+                    for (int i = 0; i < shapes.size(); i++) {
+                        AppShape shape = shapes.get(i);
+                        cursor.newRow()
+                                .add(KEY_SHAPE_KEY, shape.getKey())
+                                .add(KEY_SHAPE_TITLE, shape.getTitle())
+                                .add(KEY_PATH, shape.getPath())
+                                // TODO (b/348664593): We should fetch the currently-set shape
+                                //  option from the preferences.
+                                .add(KEY_IS_DEFAULT, i == 0);
+                    }
+                    return cursor;
+                } else  {
+                    return null;
+                }
+            }
             case KEY_LIST_OPTIONS: {
                 MatrixCursor cursor = new MatrixCursor(new String[]{
-                        KEY_NAME, KEY_ROWS, KEY_COLS, KEY_PREVIEW_COUNT, KEY_IS_DEFAULT});
+                        KEY_NAME, KEY_GRID_TITLE, KEY_ROWS, KEY_COLS, KEY_PREVIEW_COUNT,
+                        KEY_IS_DEFAULT});
                 InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(getContext());
                 for (GridOption gridOption : idp.parseAllGridOptions(getContext())) {
                     cursor.newRow()
                             .add(KEY_NAME, gridOption.name)
+                            .add(KEY_GRID_TITLE, gridOption.title)
                             .add(KEY_ROWS, gridOption.numRows)
                             .add(KEY_COLS, gridOption.numColumns)
                             .add(KEY_PREVIEW_COUNT, 1)
@@ -160,6 +210,14 @@
         }
         switch (path) {
             case KEY_DEFAULT_GRID: {
+                if (Flags.newCustomizationPickerUi()) {
+                    String shapeKey = values.getAsString(KEY_SHAPE_KEY);
+                    Optional<AppShape> optionalShape = AppShapesProvider.INSTANCE.getShapes()
+                            .stream().filter(shape -> shape.getKey().equals(shapeKey)).findFirst();
+                    String pathToSet = optionalShape.map(AppShape::getPath).orElse(null);
+                    // TODO (b/348664593): Apply shapeName to the system. This needs to be a
+                    //  synchronous call.
+                }
                 String gridName = values.getAsString(KEY_NAME);
                 InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(context);
                 // Verify that this is a valid grid option
@@ -217,30 +275,43 @@
     }
 
     @Override
-    public Bundle call(String method, String arg, Bundle extras) {
-        if (getContext().checkPermission("android.permission.BIND_WALLPAPER",
+    public Bundle call(@NonNull String method, String arg, Bundle extras) {
+        Context context = getContext();
+        if (context == null) {
+            return null;
+        }
+
+        if (context.checkPermission("android.permission.BIND_WALLPAPER",
                 Binder.getCallingPid(), Binder.getCallingUid())
                 != PackageManager.PERMISSION_GRANTED) {
             return null;
         }
 
-        if (!METHOD_GET_PREVIEW.equals(method)) {
+        if (METHOD_GET_PREVIEW.equals(method)) {
+            return getPreview(extras);
+        } else {
             return null;
         }
-        return getPreview(extras);
     }
 
     private synchronized Bundle getPreview(Bundle request) {
-        PreviewLifecycleObserver observer = null;
+        Context context = getContext();
+        if (context == null) {
+            return null;
+        }
+        RunnableList lifeCycleTracker = new RunnableList();
         try {
-            PreviewSurfaceRenderer renderer = new PreviewSurfaceRenderer(getContext(), request);
+            PreviewSurfaceRenderer renderer = new PreviewSurfaceRenderer(
+                    getContext(), lifeCycleTracker, request);
+            PreviewLifecycleObserver observer =
+                    new PreviewLifecycleObserver(lifeCycleTracker, renderer);
 
-            observer = new PreviewLifecycleObserver(renderer);
-            // Destroy previous
-            destroyObserver(mActivePreviews.get(observer.getIdentifier()));
-            mActivePreviews.put(observer.getIdentifier(), observer);
+            // Destroy previous renderers to avoid any duplicate memory
+            mActivePreviews.stream().filter(observer::isSameRenderer).forEach(o ->
+                    MAIN_EXECUTOR.execute(o.lifeCycleTracker::executeAllAndDestroy));
 
             renderer.loadAsync();
+            lifeCycleTracker.add(() -> renderer.getHostToken().unlinkToDeath(observer, 0));
             renderer.getHostToken().linkToDeath(observer, 0);
 
             Bundle result = new Bundle();
@@ -254,33 +325,23 @@
             return result;
         } catch (Exception e) {
             Log.e(TAG, "Unable to generate preview", e);
-            if (observer != null) {
-                destroyObserver(observer);
-            }
+            MAIN_EXECUTOR.execute(lifeCycleTracker::executeAllAndDestroy);
             return null;
         }
     }
 
-    private synchronized void destroyObserver(PreviewLifecycleObserver observer) {
-        if (observer == null || observer.destroyed) {
-            return;
-        }
-        observer.destroyed = true;
-        observer.renderer.getHostToken().unlinkToDeath(observer, 0);
-        MAIN_EXECUTOR.execute(observer.renderer::destroy);
-        PreviewLifecycleObserver cached = mActivePreviews.get(observer.getIdentifier());
-        if (cached == observer) {
-            mActivePreviews.remove(observer.getIdentifier());
-        }
-    }
+    private static class PreviewLifecycleObserver implements Handler.Callback, DeathRecipient {
 
-    private class PreviewLifecycleObserver implements Handler.Callback, DeathRecipient {
-
+        public final RunnableList lifeCycleTracker;
         public final PreviewSurfaceRenderer renderer;
         public boolean destroyed = false;
 
-        PreviewLifecycleObserver(PreviewSurfaceRenderer renderer) {
+        PreviewLifecycleObserver(
+                RunnableList lifeCycleTracker,
+                PreviewSurfaceRenderer renderer) {
+            this.lifeCycleTracker = lifeCycleTracker;
             this.renderer = renderer;
+            lifeCycleTracker.add(() -> destroyed = true);
         }
 
         @Override
@@ -293,14 +354,32 @@
                 case MESSAGE_ID_UPDATE_PREVIEW:
                     renderer.hideBottomRow(message.getData().getBoolean(KEY_HIDE_BOTTOM_ROW));
                     break;
+                case MESSAGE_ID_UPDATE_SHAPE:
+                    if (Flags.newCustomizationPickerUi()) {
+                        String shapeKey = message.getData().getString(KEY_SHAPE_KEY);
+                        Optional<AppShape> optionalShape = AppShapesProvider.INSTANCE.getShapes()
+                                .stream()
+                                .filter(shape -> shape.getKey().equals(shapeKey))
+                                .findFirst();
+                        String pathToSet = optionalShape.map(AppShape::getPath).orElse(null);
+                        // TODO (b/348664593): Update launcher preview with the given shape
+                    }
+                    break;
                 case MESSAGE_ID_UPDATE_GRID:
                     String gridName = message.getData().getString(KEY_GRID_NAME);
                     if (!TextUtils.isEmpty(gridName)) {
                         renderer.updateGrid(gridName);
                     }
                     break;
+                case MESSAGE_ID_UPDATE_COLOR:
+                    if (Flags.newCustomizationPickerUi()) {
+                        renderer.previewColor(message.getData());
+                    }
+                    break;
                 default:
-                    destroyObserver(this);
+                    // Unknown command, destroy lifecycle
+                    Log.d(TAG, "Unknown preview command: " + message.what + ", destroying preview");
+                    MAIN_EXECUTOR.execute(lifeCycleTracker::executeAllAndDestroy);
                     break;
             }
 
@@ -309,16 +388,16 @@
 
         @Override
         public void binderDied() {
-            destroyObserver(this);
+            MAIN_EXECUTOR.execute(lifeCycleTracker::executeAllAndDestroy);
         }
 
         /**
-         * Returns a key that should make the PreviewSurfaceRenderer unique and if two of them have
-         * the same key they will be treated as the same PreviewSurfaceRenderer. Primary this is
-         * used to prevent memory leaks by removing the old PreviewSurfaceRenderer.
+         * Two renderers are considered same if they have the same host token and display Id
          */
-        public Pair<IBinder, Integer> getIdentifier() {
-            return new Pair<>(renderer.getHostToken(), renderer.getDisplayId());
+        public boolean isSameRenderer(PreviewLifecycleObserver plo) {
+            return plo != null
+                    && plo.renderer.getHostToken().equals(renderer.getHostToken())
+                    && plo.renderer.getDisplayId() == renderer.getDisplayId();
         }
     }
 }
diff --git a/src/com/android/launcher3/graphics/IconShape.java b/src/com/android/launcher3/graphics/IconShape.java
index 5f8f2dc..cb14587 100644
--- a/src/com/android/launcher3/graphics/IconShape.java
+++ b/src/com/android/launcher3/graphics/IconShape.java
@@ -41,10 +41,12 @@
 
 import com.android.launcher3.R;
 import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
+import com.android.launcher3.dagger.ApplicationContext;
+import com.android.launcher3.dagger.LauncherAppSingleton;
+import com.android.launcher3.dagger.LauncherBaseAppComponent;
 import com.android.launcher3.icons.GraphicsUtils;
 import com.android.launcher3.icons.IconNormalizer;
-import com.android.launcher3.util.MainThreadInitializedObject;
-import com.android.launcher3.util.SafeCloseable;
+import com.android.launcher3.util.DaggerSingletonObject;
 import com.android.launcher3.views.ClipPathView;
 
 import org.xmlpull.v1.XmlPullParser;
@@ -54,19 +56,22 @@
 import java.util.ArrayList;
 import java.util.List;
 
+import javax.inject.Inject;
+
 /**
  * Abstract representation of the shape of an icon shape
  */
-public final class IconShape implements SafeCloseable {
+@LauncherAppSingleton
+public final class IconShape {
 
-    public static final MainThreadInitializedObject<IconShape> INSTANCE =
-            new MainThreadInitializedObject<>(IconShape::new);
-
+    public static DaggerSingletonObject<IconShape> INSTANCE =
+            new DaggerSingletonObject<>(LauncherBaseAppComponent::getIconShape);
 
     private ShapeDelegate mDelegate = new Circle();
     private float mNormalizationScale = ICON_VISIBLE_AREA_FACTOR;
 
-    private IconShape(Context context) {
+    @Inject
+    public IconShape(@ApplicationContext Context context) {
         pickBestShape(context);
     }
 
@@ -78,9 +83,6 @@
         return mNormalizationScale;
     }
 
-    @Override
-    public void close() { }
-
     /**
      * Initializes the shape which is closest to the {@link AdaptiveIconDrawable}
      */
diff --git a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
index 40c0cc6..f0e4fc4 100644
--- a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
+++ b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
@@ -98,6 +98,7 @@
 import com.android.launcher3.widget.LauncherWidgetHolder;
 import com.android.launcher3.widget.LocalColorExtractor;
 import com.android.launcher3.widget.util.WidgetSizes;
+import com.android.systemui.shared.Flags;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -150,6 +151,14 @@
             InvariantDeviceProfile idp,
             WallpaperColors wallpaperColorsOverride,
             @Nullable final SparseArray<Size> launcherWidgetSpanInfo) {
+        this(context, idp, null, wallpaperColorsOverride, launcherWidgetSpanInfo);
+    }
+
+    public LauncherPreviewRenderer(Context context,
+            InvariantDeviceProfile idp,
+            SparseIntArray previewColorOverride,
+            WallpaperColors wallpaperColorsOverride,
+            @Nullable final SparseArray<Size> launcherWidgetSpanInfo) {
 
         super(context);
         mUiHandler = new Handler(Looper.getMainLooper());
@@ -206,12 +215,29 @@
             mWorkspaceScreens.put(Workspace.SECOND_SCREEN_ID, rightPanel);
         }
 
-        WallpaperColors wallpaperColors = wallpaperColorsOverride != null
-                ? wallpaperColorsOverride
-                : WallpaperManager.getInstance(context).getWallpaperColors(FLAG_SYSTEM);
-        mWallpaperColorResources = wallpaperColors != null
-                ? LocalColorExtractor.newInstance(context).generateColorsOverride(wallpaperColors)
-                : null;
+        if (Flags.newCustomizationPickerUi()) {
+            if (previewColorOverride != null) {
+                mWallpaperColorResources = previewColorOverride;
+            } else if (wallpaperColorsOverride != null) {
+                mWallpaperColorResources = LocalColorExtractor.newInstance(
+                        context).generateColorsOverride(wallpaperColorsOverride);
+            } else {
+                WallpaperColors wallpaperColors = WallpaperManager.getInstance(
+                        context).getWallpaperColors(FLAG_SYSTEM);
+                mWallpaperColorResources = wallpaperColors != null
+                        ? LocalColorExtractor.newInstance(context).generateColorsOverride(
+                        wallpaperColors)
+                        : null;
+            }
+        } else {
+            WallpaperColors wallpaperColors = wallpaperColorsOverride != null
+                    ? wallpaperColorsOverride
+                    : WallpaperManager.getInstance(context).getWallpaperColors(FLAG_SYSTEM);
+            mWallpaperColorResources = wallpaperColors != null
+                    ? LocalColorExtractor.newInstance(context).generateColorsOverride(
+                    wallpaperColors)
+                    : null;
+        }
         mAppWidgetHost = new LauncherPreviewAppWidgetHost(context);
     }
 
diff --git a/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java b/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
index 56c4ca4..3000b25 100644
--- a/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
+++ b/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3.graphics;
 
+import static android.content.res.Configuration.UI_MODE_NIGHT_NO;
+import static android.content.res.Configuration.UI_MODE_NIGHT_YES;
 import static android.view.Display.DEFAULT_DISPLAY;
 
 import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME;
@@ -25,6 +27,7 @@
 import android.app.WallpaperColors;
 import android.appwidget.AppWidgetProviderInfo;
 import android.content.Context;
+import android.content.res.Configuration;
 import android.database.Cursor;
 import android.hardware.display.DisplayManager;
 import android.os.Bundle;
@@ -32,6 +35,7 @@
 import android.util.Log;
 import android.util.Size;
 import android.util.SparseArray;
+import android.util.SparseIntArray;
 import android.view.ContextThemeWrapper;
 import android.view.Display;
 import android.view.SurfaceControlViewHost;
@@ -54,7 +58,7 @@
 import com.android.launcher3.model.BaseLauncherBinder;
 import com.android.launcher3.model.BgDataModel;
 import com.android.launcher3.model.BgDataModel.Callbacks;
-import com.android.launcher3.model.GridSizeMigrationUtil;
+import com.android.launcher3.model.GridSizeMigrationDBController;
 import com.android.launcher3.model.LoaderTask;
 import com.android.launcher3.model.ModelDbController;
 import com.android.launcher3.provider.LauncherDbUtils;
@@ -81,6 +85,9 @@
     private static final String KEY_VIEW_HEIGHT = "height";
     private static final String KEY_DISPLAY_ID = "display_id";
     private static final String KEY_COLORS = "wallpaper_colors";
+    private static final String KEY_COLOR_RESOURCE_IDS = "color_resource_ids";
+    private static final String KEY_COLOR_VALUES = "color_values";
+    private static final String KEY_DARK_MODE = "use_dark_mode";
 
     private Context mContext;
     private final IBinder mHostToken;
@@ -91,7 +98,9 @@
     private final int mDisplayId;
     private final Display mDisplay;
     private final WallpaperColors mWallpaperColors;
-    private final RunnableList mOnDestroyCallbacks = new RunnableList();
+    private SparseIntArray mPreviewColorOverride;
+    @Nullable private Boolean mDarkMode;
+    private final RunnableList mLifeCycleTracker;
 
     private final SurfaceControlViewHost mSurfaceControlViewHost;
 
@@ -100,14 +109,19 @@
     private boolean mHideQsb;
     @Nullable private FrameLayout mViewRoot = null;
 
-    public PreviewSurfaceRenderer(Context context, Bundle bundle) throws Exception {
+    public PreviewSurfaceRenderer(
+            Context context, RunnableList lifecycleTracker, Bundle bundle) throws Exception {
         mContext = context;
+        mLifeCycleTracker = lifecycleTracker;
         mGridName = bundle.getString("name");
         bundle.remove("name");
         if (mGridName == null) {
             mGridName = InvariantDeviceProfile.getCurrentGridName(context);
         }
         mWallpaperColors = bundle.getParcelable(KEY_COLORS);
+        if (Flags.newCustomizationPickerUi()) {
+            updateColorOverrides(bundle);
+        }
         mHideQsb = bundle.getBoolean(GridCustomizationsProvider.KEY_HIDE_BOTTOM_ROW);
 
         mHostToken = bundle.getBinder(KEY_HOST_TOKEN);
@@ -120,11 +134,13 @@
             throw new IllegalArgumentException("Display ID does not match any displays.");
         }
 
-        mSurfaceControlViewHost = MAIN_EXECUTOR.submit(() ->
-                new SurfaceControlViewHost(mContext, context.getSystemService(DisplayManager.class)
-                        .getDisplay(DEFAULT_DISPLAY), mHostToken)
-        ).get(5, TimeUnit.SECONDS);
-        mOnDestroyCallbacks.add(mSurfaceControlViewHost::release);
+        mSurfaceControlViewHost = MAIN_EXECUTOR.submit(() -> new MySurfaceControlViewHost(
+                mContext,
+                context.getSystemService(DisplayManager.class).getDisplay(DEFAULT_DISPLAY),
+                mHostToken,
+                mLifeCycleTracker))
+                .get(5, TimeUnit.SECONDS);
+        mLifeCycleTracker.add(this::destroy);
     }
 
     public int getDisplayId() {
@@ -139,25 +155,18 @@
         return mSurfaceControlViewHost.getSurfacePackage();
     }
 
-    /**
-     * Destroys the preview and all associated data
-     */
-    @UiThread
-    public void destroy() {
+    private void destroy() {
         mDestroyed = true;
-        mOnDestroyCallbacks.executeAllAndDestroy();
     }
 
     /**
      * A function that queries for the launcher app widget span info
      *
-     * @param context The context to get the content resolver from, should be related to launcher
      * @return A SparseArray with the app widget id being the key and the span info being the values
      */
     @WorkerThread
     @Nullable
-    public SparseArray<Size> getLoadedLauncherWidgetInfo(
-            @NonNull final Context context) {
+    public SparseArray<Size> getLoadedLauncherWidgetInfo() {
         final SparseArray<Size> widgetInfo = new SparseArray<>();
         final String query = LauncherSettings.Favorites.ITEM_TYPE + " = "
                 + LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET;
@@ -220,27 +229,81 @@
         }
     }
 
+    /**
+     * Updates the colors of the preview.
+     *
+     * @param bundle Bundle with an int array of color ids and an int array of overriding colors.
+     */
+    public void previewColor(Bundle bundle) {
+        updateColorOverrides(bundle);
+        loadAsync();
+    }
+
+    private void updateColorOverrides(Bundle bundle) {
+        mDarkMode =
+                bundle.containsKey(KEY_DARK_MODE) ? bundle.getBoolean(KEY_DARK_MODE) : null;
+        int[] ids = bundle.getIntArray(KEY_COLOR_RESOURCE_IDS);
+        int[] colors = bundle.getIntArray(KEY_COLOR_VALUES);
+        if (ids != null && colors != null) {
+            mPreviewColorOverride = new SparseIntArray();
+            for (int i = 0; i < ids.length; i++) {
+                mPreviewColorOverride.put(ids[i], colors[i]);
+            }
+        } else {
+            mPreviewColorOverride = null;
+        }
+    }
+
     /***
      * Generates a new context overriding the theme color and the display size without affecting the
      * main application context
      */
     private Context getPreviewContext() {
         Context context = mContext.createDisplayContext(mDisplay);
-        if (mWallpaperColors == null) {
-            return new ContextThemeWrapper(context,
-                    Themes.getActivityThemeRes(context));
+        if (mDarkMode != null) {
+            Configuration configuration = new Configuration(
+                    context.getResources().getConfiguration());
+            if (mDarkMode) {
+                configuration.uiMode &= ~UI_MODE_NIGHT_NO;
+                configuration.uiMode |= UI_MODE_NIGHT_YES;
+            } else {
+                configuration.uiMode &= ~UI_MODE_NIGHT_YES;
+                configuration.uiMode |= UI_MODE_NIGHT_NO;
+            }
+            context = context.createConfigurationContext(configuration);
         }
-        LocalColorExtractor.newInstance(context)
-                .applyColorsOverride(context, mWallpaperColors);
-        return new ContextThemeWrapper(context,
-                Themes.getActivityThemeRes(context, mWallpaperColors.getColorHints()));
+        if (Flags.newCustomizationPickerUi()) {
+            if (mPreviewColorOverride != null) {
+                LocalColorExtractor.newInstance(context)
+                        .applyColorsOverride(context, mPreviewColorOverride);
+            } else if (mWallpaperColors != null) {
+                LocalColorExtractor.newInstance(context)
+                        .applyColorsOverride(context, mWallpaperColors);
+            }
+            if (mWallpaperColors != null) {
+                return new ContextThemeWrapper(context,
+                        Themes.getActivityThemeRes(context, mWallpaperColors.getColorHints()));
+            } else {
+                return new ContextThemeWrapper(context,
+                        Themes.getActivityThemeRes(context));
+            }
+        } else {
+            if (mWallpaperColors == null) {
+                return new ContextThemeWrapper(context,
+                        Themes.getActivityThemeRes(context));
+            }
+            LocalColorExtractor.newInstance(context)
+                    .applyColorsOverride(context, mWallpaperColors);
+            return new ContextThemeWrapper(context,
+                    Themes.getActivityThemeRes(context, mWallpaperColors.getColorHints()));
+        }
     }
 
     @WorkerThread
     private void loadModelData() {
         final Context inflationContext = getPreviewContext();
         final InvariantDeviceProfile idp = new InvariantDeviceProfile(inflationContext, mGridName);
-        if (GridSizeMigrationUtil.needsToMigrate(inflationContext, idp)) {
+        if (GridSizeMigrationDBController.needsToMigrate(inflationContext, idp)) {
             // Start the migration
             PreviewContext previewContext = new PreviewContext(inflationContext, idp);
             // Copy existing data to preview DB
@@ -261,7 +324,9 @@
                     bgModel,
                     LauncherAppState.getInstance(previewContext).getModel().getModelDelegate(),
                     new BaseLauncherBinder(LauncherAppState.getInstance(previewContext), bgModel,
-                            /* bgAllAppsList= */ null, new Callbacks[0])) {
+                            /* bgAllAppsList= */ null, new Callbacks[0]),
+                    LauncherAppState.getInstance(
+                            previewContext).getModel().getWidgetsFilterDataProvider()) {
 
                 @Override
                 public void run() {
@@ -276,13 +341,11 @@
                     }
                     loadWorkspace(new ArrayList<>(), query, null, null);
 
-                    final SparseArray<Size> spanInfo =
-                            getLoadedLauncherWidgetInfo(previewContext.getBaseContext());
-
+                    final SparseArray<Size> spanInfo = getLoadedLauncherWidgetInfo();
                     MAIN_EXECUTOR.execute(() -> {
                         renderView(previewContext, mBgDataModel, mWidgetProvidersMap, spanInfo,
                                 idp);
-                        mOnDestroyCallbacks.add(previewContext::onDestroy);
+                        mLifeCycleTracker.add(previewContext::onDestroy);
                     });
                 }
             }.run();
@@ -305,8 +368,13 @@
         if (mDestroyed) {
             return;
         }
-        mRenderer = new LauncherPreviewRenderer(inflationContext, idp,
-                mWallpaperColors, launcherWidgetSpanInfo);
+        if (Flags.newCustomizationPickerUi()) {
+            mRenderer = new LauncherPreviewRenderer(inflationContext, idp, mPreviewColorOverride,
+                    mWallpaperColors, launcherWidgetSpanInfo);
+        } else {
+            mRenderer = new LauncherPreviewRenderer(inflationContext, idp,
+                    mWallpaperColors, launcherWidgetSpanInfo);
+        }
         mRenderer.hideBottomRow(mHideQsb);
         View view = mRenderer.getRenderedView(dataModel, widgetProviderInfoMap);
         // This aspect scales the view to fit in the surface and centers it
@@ -355,4 +423,24 @@
             mViewRoot.addView(view);
         }
     }
+
+    private static class MySurfaceControlViewHost extends SurfaceControlViewHost {
+
+        private final RunnableList mLifecycleTracker;
+
+        MySurfaceControlViewHost(Context context, Display display, IBinder hostToken,
+                RunnableList lifeCycleTracker) {
+            super(context, display, hostToken);
+            mLifecycleTracker = lifeCycleTracker;
+            mLifecycleTracker.add(this::release);
+        }
+
+        @Override
+        public void release() {
+            super.release();
+            // RunnableList ensures that the callback is only called once
+            MAIN_EXECUTOR.execute(mLifecycleTracker::executeAllAndDestroy);
+        }
+    }
+
 }
diff --git a/src/com/android/launcher3/graphics/SysUiScrim.java b/src/com/android/launcher3/graphics/SysUiScrim.java
index 077ddfc..d59fc19 100644
--- a/src/com/android/launcher3/graphics/SysUiScrim.java
+++ b/src/com/android/launcher3/graphics/SysUiScrim.java
@@ -32,10 +32,10 @@
 import androidx.annotation.ColorInt;
 import androidx.annotation.VisibleForTesting;
 
-import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.R;
 import com.android.launcher3.anim.AnimatedFloat;
+import com.android.launcher3.statemanager.StatefulContainer;
 import com.android.launcher3.testing.shared.ResourceUtils;
 import com.android.launcher3.util.ScreenOnTracker;
 import com.android.launcher3.util.ScreenOnTracker.ScreenOnListener;
@@ -84,7 +84,7 @@
     private final int mBottomMaskHeight;
 
     private final View mRoot;
-    private final BaseDraggingActivity mActivity;
+    private final StatefulContainer mContainer;
     private final boolean mHideSysUiScrim;
     private boolean mSkipScrimAnimationForTest = false;
 
@@ -94,8 +94,8 @@
 
     public SysUiScrim(View view) {
         mRoot = view;
-        mActivity = BaseDraggingActivity.fromContext(view.getContext());
-        DisplayMetrics dm = mActivity.getResources().getDisplayMetrics();
+        mContainer = StatefulContainer.fromContext(view.getContext());
+        DisplayMetrics dm = mContainer.getContext().getResources().getDisplayMetrics();
 
         mTopMaskHeight = ResourceUtils.pxFromDp(TOP_MASK_HEIGHT_DP, dm);
         mBottomMaskHeight = ResourceUtils.pxFromDp(BOTTOM_MASK_HEIGHT_DP, dm);
@@ -130,7 +130,7 @@
 
                 ObjectAnimator oa = mSysUiAnimMultiplier.animateToValue(1);
                 oa.setDuration(600);
-                oa.setStartDelay(mActivity.getWindow().getTransitionBackgroundFadeDuration());
+                oa.setStartDelay(mContainer.getWindow().getTransitionBackgroundFadeDuration());
                 oa.start();
                 mAnimateScrimOnNextDraw = false;
             }
@@ -166,19 +166,19 @@
      * horizontal
      */
     public void onInsetsChanged(Rect insets) {
-        DeviceProfile dp = mActivity.getDeviceProfile();
+        DeviceProfile dp = mContainer.getDeviceProfile();
         mDrawTopScrim = insets.top > 0;
         mDrawBottomScrim = !dp.isVerticalBarLayout() && !dp.isGestureMode && !dp.isTaskbarPresent;
     }
 
     @Override
     public void onViewAttachedToWindow(View view) {
-        ScreenOnTracker.INSTANCE.get(mActivity).addListener(mScreenOnListener);
+        ScreenOnTracker.INSTANCE.get(mContainer.getContext()).addListener(mScreenOnListener);
     }
 
     @Override
     public void onViewDetachedFromWindow(View view) {
-        ScreenOnTracker.INSTANCE.get(mActivity).removeListener(mScreenOnListener);
+        ScreenOnTracker.INSTANCE.get(mContainer.getContext()).removeListener(mScreenOnListener);
     }
 
     /**
@@ -213,7 +213,7 @@
     }
 
     private Bitmap createDitheredAlphaMask(int height, @ColorInt int[] colors, float[] positions) {
-        DisplayMetrics dm = mActivity.getResources().getDisplayMetrics();
+        DisplayMetrics dm = mContainer.getContext().getResources().getDisplayMetrics();
         int width = ResourceUtils.pxFromDp(ALPHA_MASK_BITMAP_WIDTH_DP, dm);
         Bitmap dst = Bitmap.createBitmap(width, height, Bitmap.Config.ALPHA_8);
         Canvas c = new Canvas(dst);
diff --git a/src/com/android/launcher3/icons/CacheableShortcutInfo.kt b/src/com/android/launcher3/icons/CacheableShortcutInfo.kt
new file mode 100644
index 0000000..a78da23
--- /dev/null
+++ b/src/com/android/launcher3/icons/CacheableShortcutInfo.kt
@@ -0,0 +1,130 @@
+/*
+ * 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.icons
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.pm.LauncherActivityInfo
+import android.content.pm.LauncherApps
+import android.content.pm.ShortcutInfo
+import android.graphics.drawable.Drawable
+import android.os.UserHandle
+import android.util.Log
+import com.android.launcher3.BuildConfig
+import com.android.launcher3.LauncherAppState
+import com.android.launcher3.icons.BaseIconFactory.IconOptions
+import com.android.launcher3.icons.cache.BaseIconCache
+import com.android.launcher3.icons.cache.CachingLogic
+import com.android.launcher3.shortcuts.ShortcutKey
+import com.android.launcher3.util.ApplicationInfoWrapper
+import com.android.launcher3.util.PackageUserKey
+import com.android.launcher3.util.Themes
+
+/** Wrapper over ShortcutInfo to provide extra information related to ShortcutInfo */
+class CacheableShortcutInfo(val shortcutInfo: ShortcutInfo, val appInfo: ApplicationInfoWrapper) {
+
+    constructor(
+        info: ShortcutInfo,
+        ctx: Context,
+    ) : this(info, ApplicationInfoWrapper(ctx, info.getPackage(), info.userHandle))
+
+    companion object {
+        private const val TAG = "CacheableShortcutInfo"
+
+        /**
+         * Similar to [LauncherApps.getShortcutIconDrawable] with additional Launcher specific
+         * checks
+         */
+        @JvmStatic
+        fun getIcon(context: Context, shortcutInfo: ShortcutInfo, density: Int): Drawable? {
+            if (!BuildConfig.WIDGETS_ENABLED) {
+                return null
+            }
+            try {
+                return context
+                    .getSystemService(LauncherApps::class.java)
+                    .getShortcutIconDrawable(shortcutInfo, density)
+            } catch (e: Exception) {
+                Log.e(TAG, "Failed to get shortcut icon", e)
+                return null
+            }
+        }
+
+        /**
+         * Converts the provided list of Shortcuts to CacheableShortcuts by using the application
+         * info from the provided list of apps
+         */
+        @JvmStatic
+        fun convertShortcutsToCacheableShortcuts(
+            shortcuts: List<ShortcutInfo>,
+            activities: List<LauncherActivityInfo>,
+        ): List<CacheableShortcutInfo> {
+            // Create a map of package to applicationInfo
+            val appMap =
+                activities.associateBy(
+                    { PackageUserKey(it.componentName.packageName, it.user) },
+                    { it.applicationInfo },
+                )
+
+            return shortcuts.map {
+                CacheableShortcutInfo(
+                    it,
+                    ApplicationInfoWrapper(appMap[PackageUserKey(it.getPackage(), it.userHandle)]),
+                )
+            }
+        }
+    }
+}
+
+/** Caching logic for CacheableShortcutInfo. */
+object CacheableShortcutCachingLogic : CachingLogic<CacheableShortcutInfo> {
+
+    override fun getComponent(info: CacheableShortcutInfo): ComponentName =
+        ShortcutKey.fromInfo(info.shortcutInfo).componentName
+
+    override fun getUser(info: CacheableShortcutInfo): UserHandle = info.shortcutInfo.userHandle
+
+    override fun getLabel(info: CacheableShortcutInfo): CharSequence? = info.shortcutInfo.shortLabel
+
+    override fun getApplicationInfo(info: CacheableShortcutInfo) = info.appInfo.getInfo()
+
+    override fun loadIcon(context: Context, cache: BaseIconCache, info: CacheableShortcutInfo) =
+        LauncherIcons.obtain(context).use { li ->
+            CacheableShortcutInfo.getIcon(
+                    context,
+                    info.shortcutInfo,
+                    LauncherAppState.getIDP(context).fillResIconDpi,
+                )
+                ?.let { d ->
+                    li.createBadgedIconBitmap(
+                        d,
+                        IconOptions().setExtractedColor(Themes.getColorAccent(context)),
+                    )
+                } ?: BitmapInfo.LOW_RES_INFO
+        }
+
+    override fun getFreshnessIdentifier(
+        item: CacheableShortcutInfo,
+        provider: IconProvider,
+    ): String? =
+        // Manifest shortcuts get updated on every reboot. Don't include their change timestamp as
+        // it gets covered by the app's version
+        (if (item.shortcutInfo.isDeclaredInManifest) ""
+        else item.shortcutInfo.lastChangedTimestamp.toString()) +
+            "-" +
+            provider.getStateForApp(getApplicationInfo(item))
+}
diff --git a/src/com/android/launcher3/icons/IconCache.java b/src/com/android/launcher3/icons/IconCache.java
index 587dc27..e7c4024 100644
--- a/src/com/android/launcher3/icons/IconCache.java
+++ b/src/com/android/launcher3/icons/IconCache.java
@@ -29,14 +29,10 @@
 import android.content.pm.ApplicationInfo;
 import android.content.pm.LauncherActivityInfo;
 import android.content.pm.LauncherApps;
-import android.content.pm.PackageInfo;
 import android.content.pm.PackageInstaller;
-import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.pm.ShortcutInfo;
 import android.database.Cursor;
 import android.database.sqlite.SQLiteException;
-import android.graphics.drawable.Drawable;
 import android.os.Looper;
 import android.os.Process;
 import android.os.Trace;
@@ -55,8 +51,8 @@
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.icons.cache.BaseIconCache;
+import com.android.launcher3.icons.cache.CachedObject;
 import com.android.launcher3.icons.cache.CachedObjectCachingLogic;
-import com.android.launcher3.icons.cache.CachingLogic;
 import com.android.launcher3.icons.cache.LauncherActivityCachingLogic;
 import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.model.data.AppInfo;
@@ -66,8 +62,8 @@
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.pm.InstallSessionHelper;
 import com.android.launcher3.pm.UserCache;
-import com.android.launcher3.shortcuts.ShortcutKey;
 import com.android.launcher3.util.CancellableTask;
+import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.InstantAppResolver;
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.widget.WidgetSections;
@@ -96,10 +92,6 @@
     private final Predicate<ItemInfoWithIcon> mIsUsingFallbackOrNonDefaultIconCheck = w ->
             w.bitmap != null && (w.bitmap.isNullOrLowRes() || !isDefaultIcon(w.bitmap, w.user));
 
-    private final CachingLogic<ComponentWithLabel> mComponentWithLabelCachingLogic;
-    private final CachingLogic<LauncherActivityInfo> mLauncherActivityInfoCachingLogic;
-    private final CachingLogic<ShortcutInfo> mShortcutCachingLogic;
-
     private final LauncherApps mLauncherApps;
     private final UserCache mUserManager;
     private final InstantAppResolver mInstantAppResolver;
@@ -113,10 +105,6 @@
             IconProvider iconProvider) {
         super(context, dbFileName, MODEL_EXECUTOR.getLooper(),
                 idp.fillResIconDpi, idp.iconBitmapSize, true /* inMemoryCache */, iconProvider);
-        mComponentWithLabelCachingLogic = new CachedObjectCachingLogic(
-                context, false /* loadIcons */, false /* addToMemCache */);
-        mLauncherActivityInfoCachingLogic = LauncherActivityCachingLogic.INSTANCE;
-        mShortcutCachingLogic = new ShortcutCachingLogic();
         mLauncherApps = mContext.getSystemService(LauncherApps.class);
         mUserManager = UserCache.INSTANCE.get(mContext);
         mInstantAppResolver = InstantAppResolver.newInstance(mContext);
@@ -148,16 +136,9 @@
     public synchronized void updateIconsForPkg(@NonNull final String packageName,
             @NonNull final UserHandle user) {
         removeIconsForPkg(packageName, user);
-        try {
-            PackageInfo info = mPackageManager.getPackageInfo(packageName,
-                    PackageManager.GET_UNINSTALLED_PACKAGES);
-            long userSerial = mUserManager.getSerialNumberForUser(user);
-            for (LauncherActivityInfo app : mLauncherApps.getActivityList(packageName, user)) {
-                addIconToDBAndMemCache(app, mLauncherActivityInfoCachingLogic, info, userSerial,
-                        false /*replace existing*/);
-            }
-        } catch (NameNotFoundException e) {
-            Log.d(TAG, "Package not found", e);
+        long userSerial = mUserManager.getSerialNumberForUser(user);
+        for (LauncherActivityInfo app : mLauncherApps.getActivityList(packageName, user)) {
+            addIconToDBAndMemCache(app, LauncherActivityCachingLogic.INSTANCE, userSerial);
         }
     }
 
@@ -209,7 +190,7 @@
 
         CancellableTask<ItemInfoWithIcon> request = new CancellableTask<>(
                 task, MAIN_EXECUTOR, caller::reapplyItemInfo, endRunnable);
-        Utilities.postAsyncCallback(mWorkerHandler, request);
+        Utilities.postAsyncCallback(workerHandler, request);
         return request;
     }
 
@@ -224,28 +205,16 @@
      * Updates {@param application} only if a valid entry is found.
      */
     public synchronized void updateTitleAndIcon(AppInfo application) {
-        boolean preferPackageIcon = application.isArchived();
         CacheEntry entry = cacheLocked(application.componentName,
-                application.user, () -> null, mLauncherActivityInfoCachingLogic,
-                false, application.usingLowResIcon());
-        if (entry.bitmap == null || isDefaultIcon(entry.bitmap, application.user)) {
-            return;
-        }
-
-        if (preferPackageIcon) {
-            String packageName = application.getTargetPackage();
-            CacheEntry packageEntry =
-                    cacheLocked(new ComponentName(packageName, packageName + EMPTY_CLASS_NAME),
-                            application.user, () -> null, mLauncherActivityInfoCachingLogic,
-                            true, application.usingLowResIcon());
-            applyPackageEntry(packageEntry, application, entry);
-        } else {
+                application.user, () -> null, LauncherActivityCachingLogic.INSTANCE,
+                application.usingLowResIcon() ? LookupFlag.USE_LOW_RES : LookupFlag.DEFAULT);
+        if (entry.bitmap != null || !isDefaultIcon(entry.bitmap, application.user)) {
             applyCacheEntry(entry, application);
         }
     }
 
     /**
-     * Fill in {@param info} with the icon and label for {@param activityInfo}
+     * Fill in {@code info} with the icon and label for {@code activityInfo}
      */
     @SuppressWarnings("NewApi")
     public synchronized void getTitleAndIcon(ItemInfoWithIcon info,
@@ -253,33 +222,45 @@
         boolean isAppArchived = Flags.enableSupportForArchiving() && activityInfo != null
                 && activityInfo.getActivityInfo().isArchived;
         // If we already have activity info, no need to use package icon
-        getTitleAndIcon(info, () -> activityInfo, isAppArchived, useLowResIcon,
-                isAppArchived);
+        getTitleAndIcon(info, () -> activityInfo, isAppArchived, useLowResIcon);
     }
 
     /**
-     * Fill in {@param info} with the icon for {@param si}
+     * Fill in {@code info} with the icon for {@code si}
      */
     public void getShortcutIcon(ItemInfoWithIcon info, ShortcutInfo si) {
+        getShortcutIcon(info, new CacheableShortcutInfo(si, mContext));
+    }
+
+    /**
+     * Fill in {@code info} with the icon for {@code si}
+     */
+    public void getShortcutIcon(ItemInfoWithIcon info, CacheableShortcutInfo si) {
         getShortcutIcon(info, si, mIsUsingFallbackOrNonDefaultIconCheck);
     }
 
     /**
-     * Fill in {@param info} with the icon and label for {@param si}. If the icon is not
+     * Fill in {@code info} with the icon and label for {@code si}. If the icon is not
      * available, and fallback check returns true, it keeps the old icon.
+     * Shortcut entries are not kept in memory since they are not frequently used
      */
-    public <T extends ItemInfoWithIcon> void getShortcutIcon(T info, ShortcutInfo si,
+    public <T extends ItemInfoWithIcon> void getShortcutIcon(T info, CacheableShortcutInfo si,
             @NonNull Predicate<T> fallbackIconCheck) {
-        BitmapInfo bitmapInfo = cacheLocked(ShortcutKey.fromInfo(si).componentName,
-                si.getUserHandle(), () -> si, mShortcutCachingLogic, false, false).bitmap;
+        UserHandle user = CacheableShortcutCachingLogic.INSTANCE.getUser(si);
+        BitmapInfo bitmapInfo = cacheLocked(
+                CacheableShortcutCachingLogic.INSTANCE.getComponent(si),
+                user,
+                () -> si,
+                CacheableShortcutCachingLogic.INSTANCE,
+                LookupFlag.SKIP_ADD_TO_MEM_CACHE).bitmap;
         if (bitmapInfo.isNullOrLowRes()) {
-            bitmapInfo = getDefaultIcon(si.getUserHandle());
+            bitmapInfo = getDefaultIcon(user);
         }
 
-        if (isDefaultIcon(bitmapInfo, si.getUserHandle()) && fallbackIconCheck.test(info)) {
+        if (isDefaultIcon(bitmapInfo, user) && fallbackIconCheck.test(info)) {
             return;
         }
-        info.bitmap = bitmapInfo.withBadgeInfo(getShortcutInfoBadge(si));
+        info.bitmap = bitmapInfo.withBadgeInfo(getShortcutInfoBadge(si.getShortcutInfo()));
     }
 
     /**
@@ -333,17 +314,17 @@
         } else {
             Intent intent = info.getIntent();
             getTitleAndIcon(info, () -> mLauncherApps.resolveActivity(intent, info.user),
-                    true, useLowResIcon, info.isArchived());
+                    true, useLowResIcon);
         }
     }
 
     /**
      * Loads and returns the icon for the provided object without adding it to memCache
      */
-    public synchronized String getTitleNoCache(ComponentWithLabel info) {
+    public synchronized String getTitleNoCache(CachedObject info) {
         CacheEntry entry = cacheLocked(info.getComponent(), info.getUser(), () -> info,
-                mComponentWithLabelCachingLogic, false /* usePackageIcon */,
-                true /* useLowResIcon */);
+                CachedObjectCachingLogic.INSTANCE,
+                LookupFlag.USE_LOW_RES | LookupFlag.SKIP_ADD_TO_MEM_CACHE);
         return Utilities.trim(entry.title);
     }
 
@@ -354,40 +335,15 @@
             @NonNull ItemInfoWithIcon infoInOut,
             @NonNull Supplier<LauncherActivityInfo> activityInfoProvider,
             boolean usePkgIcon, boolean useLowResIcon) {
+        int lookupFlags = LookupFlag.DEFAULT;
+        if (usePkgIcon) lookupFlags |= LookupFlag.USE_PACKAGE_ICON;
+        if (useLowResIcon) lookupFlags |= LookupFlag.USE_LOW_RES;
         CacheEntry entry = cacheLocked(infoInOut.getTargetComponent(), infoInOut.user,
-                activityInfoProvider, mLauncherActivityInfoCachingLogic, usePkgIcon,
-                useLowResIcon);
+                activityInfoProvider, LauncherActivityCachingLogic.INSTANCE, lookupFlags);
         applyCacheEntry(entry, infoInOut);
     }
 
     /**
-     * Fill in {@param mWorkspaceItemInfo} with the icon and label for {@param info}
-     */
-    public synchronized void getTitleAndIcon(
-            @NonNull ItemInfoWithIcon infoInOut,
-            @NonNull Supplier<LauncherActivityInfo> activityInfoProvider,
-            boolean usePkgIcon, boolean useLowResIcon, boolean preferPackageEntry) {
-        CacheEntry entry = cacheLocked(infoInOut.getTargetComponent(), infoInOut.user,
-                activityInfoProvider, mLauncherActivityInfoCachingLogic, usePkgIcon,
-                useLowResIcon);
-        if (preferPackageEntry) {
-            String packageName = infoInOut.getTargetPackage();
-            CacheEntry packageEntry = cacheLocked(
-                    new ComponentName(packageName, packageName + EMPTY_CLASS_NAME),
-                    infoInOut.user, activityInfoProvider, mLauncherActivityInfoCachingLogic,
-                    usePkgIcon, useLowResIcon);
-            applyPackageEntry(packageEntry, infoInOut, entry);
-        } else if (useLowResIcon || !entry.bitmap.isNullOrLowRes()
-                || infoInOut.bitmap.isNullOrLowRes()) {
-            // Only use cache entry if it will not downgrade the current bitmap in infoInOut
-            applyCacheEntry(entry, infoInOut);
-        } else {
-            Log.d(TAG, "getTitleAndIcon: Cache entry bitmap was a downgrade of existing bitmap"
-                    + " in ItemInfo. Skipping.");
-        }
-    }
-
-    /**
      * Creates an sql cursor for a query of a set of ItemInfoWithIcon icons and titles.
      *
      * @param iconRequestInfos List of IconRequestInfos representing titles and icons to query.
@@ -484,10 +440,9 @@
                                 cn,
                                 /* user = */ sectionKey.first,
                                 () -> duplicateIconRequests.get(0).launcherActivityInfo,
-                                mLauncherActivityInfoCachingLogic,
-                                c,
-                                /* usePackageIcon= */ false,
-                                /* useLowResIcons = */ sectionKey.second);
+                                LauncherActivityCachingLogic.INSTANCE,
+                                sectionKey.second ? LookupFlag.USE_LOW_RES : LookupFlag.DEFAULT,
+                                c);
 
                         for (IconRequestInfo<T> iconRequest : duplicateIconRequests) {
                             applyCacheEntry(entry, iconRequest.itemInfo);
@@ -534,7 +489,7 @@
                     loadFallbackIcon(
                             lai,
                             entry,
-                            mLauncherActivityInfoCachingLogic,
+                            LauncherActivityCachingLogic.INSTANCE,
                             /* usePackageIcon= */ false,
                             /* usePackageTitle= */ loadFallbackTitle,
                             cn,
@@ -544,7 +499,7 @@
                     loadFallbackTitle(
                             lai,
                             entry,
-                            mLauncherActivityInfoCachingLogic,
+                            LauncherActivityCachingLogic.INSTANCE,
                             sectionKey.first);
                 }
 
@@ -603,28 +558,30 @@
         info.title = Utilities.trim(entry.title);
         info.contentDescription = entry.contentDescription;
         info.bitmap = entry.bitmap;
+        // Clear any previously set appTitle, if the packageOverride is no longer valid
+        info.appTitle = null;
         if (entry.bitmap == null) {
             // TODO: entry.bitmap can never be null, so this should not happen at all.
             Log.wtf(TAG, "Cannot find bitmap from the cache, default icon was loaded.");
             info.bitmap = getDefaultIcon(info.user);
         }
-    }
 
-    protected void applyPackageEntry(@NonNull final CacheEntry packageEntry,
-            @NonNull final ItemInfoWithIcon info, @NonNull final CacheEntry fallbackEntry) {
+        // apply package override
+        if (!Flags.enableSupportForArchiving() || !info.isArchived()) {
+            return;
+        }
+        String targetPackage = info.getTargetPackage();
+        if (targetPackage == null) {
+            return;
+        }
+        CacheEntry packageEntry = getInMemoryPackageEntryLocked(targetPackage, info.user);
+        if (packageEntry == null || packageEntry.bitmap.isLowRes()) {
+            return;
+        }
+        info.appTitle = Utilities.trim(info.title);
         info.title = Utilities.trim(packageEntry.title);
-        info.appTitle = Utilities.trim(fallbackEntry.title);
         info.contentDescription = packageEntry.contentDescription;
         info.bitmap = packageEntry.bitmap;
-        if (packageEntry.bitmap == null) {
-            // TODO: entry.bitmap can never be null, so this should not happen at all.
-            Log.wtf(TAG, "Cannot find bitmap from the cache, default icon was loaded.");
-            info.bitmap = getDefaultIcon(info.user);
-        }
-    }
-
-    public Drawable getFullResIcon(LauncherActivityInfo info) {
-        return mIconProvider.getIcon(info, mIconDpi);
     }
 
     public void updateSessionCache(PackageUserKey key, PackageInstaller.SessionInfo info) {
@@ -632,6 +589,11 @@
                 info.getAppLabel());
     }
 
+    @VisibleForTesting
+    synchronized boolean isItemInDb(ComponentKey cacheKey) {
+        return getEntryFromDBLocked(cacheKey, new CacheEntry(), false);
+    }
+
     /**
      * Interface for receiving itemInfo with high-res icon.
      */
diff --git a/src/com/android/launcher3/icons/LauncherIconProvider.java b/src/com/android/launcher3/icons/LauncherIconProvider.java
index c4d5f2b..78a3128 100644
--- a/src/com/android/launcher3/icons/LauncherIconProvider.java
+++ b/src/com/android/launcher3/icons/LauncherIconProvider.java
@@ -16,14 +16,18 @@
 package com.android.launcher3.icons;
 
 import android.content.Context;
+import android.content.pm.ApplicationInfo;
 import android.content.res.Resources;
 import android.content.res.XmlResourceParser;
 import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.Log;
 
+import androidx.annotation.NonNull;
+
 import com.android.launcher3.R;
 import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.util.ApiWrapper;
 import com.android.launcher3.util.Themes;
 
 import org.xmlpull.v1.XmlPullParser;
@@ -70,6 +74,11 @@
         return super.getSystemIconState() + (mSupportsIconTheme ? ",with-theme" : ",no-theme");
     }
 
+    @Override
+    protected String getApplicationInfoHash(@NonNull ApplicationInfo appInfo) {
+        return ApiWrapper.INSTANCE.get(mContext).getApplicationInfoHash(appInfo);
+    }
+
     private Map<String, ThemeData> getThemedIconMap() {
         if (mThemedIconMap != null) {
             return mThemedIconMap;
diff --git a/src/com/android/launcher3/icons/Legacy.kt b/src/com/android/launcher3/icons/Legacy.kt
deleted file mode 100644
index 3bf3bb2..0000000
--- a/src/com/android/launcher3/icons/Legacy.kt
+++ /dev/null
@@ -1,31 +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.icons
-
-import com.android.launcher3.icons.cache.CachedObject
-
-/**
- * This files contains some definitions used during refactoring to avoid breaking changes.
- *
- * TODO(b/366237794) remove this file once refactoring is complete
- */
-
-/** Temporary interface to allow easier refactoring */
-interface ComponentWithLabel : CachedObject<IconCache>
-
-/** Temporary interface to allow easier refactoring */
-interface ComponentWithLabelAndIcon : ComponentWithLabel
diff --git a/src/com/android/launcher3/icons/MonochromeIconFactory.java b/src/com/android/launcher3/icons/MonochromeIconFactory.java
deleted file mode 100644
index 2854d51..0000000
--- a/src/com/android/launcher3/icons/MonochromeIconFactory.java
+++ /dev/null
@@ -1,172 +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.icons;
-
-import static android.graphics.Paint.FILTER_BITMAP_FLAG;
-
-import android.annotation.TargetApi;
-import android.graphics.Bitmap;
-import android.graphics.Bitmap.Config;
-import android.graphics.BlendMode;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.ColorFilter;
-import android.graphics.ColorMatrix;
-import android.graphics.ColorMatrixColorFilter;
-import android.graphics.Paint;
-import android.graphics.PixelFormat;
-import android.graphics.Rect;
-import android.graphics.drawable.AdaptiveIconDrawable;
-import android.graphics.drawable.Drawable;
-import android.os.Build;
-
-import androidx.annotation.WorkerThread;
-
-import com.android.launcher3.icons.BaseIconFactory.ClippedMonoDrawable;
-
-import java.nio.ByteBuffer;
-
-/**
- * Utility class to generate monochrome icons version for a given drawable.
- */
-@TargetApi(Build.VERSION_CODES.TIRAMISU)
-public class MonochromeIconFactory extends Drawable {
-
-    private final Bitmap mFlatBitmap;
-    private final Canvas mFlatCanvas;
-    private final Paint mCopyPaint;
-
-    private final Bitmap mAlphaBitmap;
-    private final Canvas mAlphaCanvas;
-    private final byte[] mPixels;
-
-    private final int mBitmapSize;
-    private final int mEdgePixelLength;
-
-    private final Paint mDrawPaint;
-    private final Rect mSrcRect;
-
-    MonochromeIconFactory(int iconBitmapSize) {
-        float extraFactor = AdaptiveIconDrawable.getExtraInsetFraction();
-        float viewPortScale = 1 / (1 + 2 * extraFactor);
-        mBitmapSize = Math.round(iconBitmapSize * 2 * viewPortScale);
-        mPixels = new byte[mBitmapSize * mBitmapSize];
-        mEdgePixelLength = mBitmapSize * (mBitmapSize - iconBitmapSize) / 2;
-
-        mFlatBitmap = Bitmap.createBitmap(mBitmapSize, mBitmapSize, Config.ARGB_8888);
-        mFlatCanvas = new Canvas(mFlatBitmap);
-
-        mAlphaBitmap = Bitmap.createBitmap(mBitmapSize, mBitmapSize, Config.ALPHA_8);
-        mAlphaCanvas = new Canvas(mAlphaBitmap);
-
-        mDrawPaint = new Paint(FILTER_BITMAP_FLAG);
-        mDrawPaint.setColor(Color.WHITE);
-        mSrcRect = new Rect(0, 0, mBitmapSize, mBitmapSize);
-
-        mCopyPaint = new Paint(FILTER_BITMAP_FLAG);
-        mCopyPaint.setBlendMode(BlendMode.SRC);
-
-        // Crate a color matrix which converts the icon to grayscale and then uses the average
-        // of RGB components as the alpha component.
-        ColorMatrix satMatrix = new ColorMatrix();
-        satMatrix.setSaturation(0);
-        float[] vals = satMatrix.getArray();
-        vals[15] = vals[16] = vals[17] = .3333f;
-        vals[18] = vals[19] = 0;
-        mCopyPaint.setColorFilter(new ColorMatrixColorFilter(vals));
-    }
-
-    private void drawDrawable(Drawable drawable) {
-        if (drawable != null) {
-            drawable.setBounds(0, 0, mBitmapSize, mBitmapSize);
-            drawable.draw(mFlatCanvas);
-        }
-    }
-
-    /**
-     * Creates a monochrome version of the provided drawable
-     */
-    @WorkerThread
-    public Drawable wrap(AdaptiveIconDrawable icon) {
-        mFlatCanvas.drawColor(Color.BLACK);
-        drawDrawable(icon.getBackground());
-        drawDrawable(icon.getForeground());
-        generateMono();
-        return new ClippedMonoDrawable(this);
-    }
-
-    @WorkerThread
-    private void generateMono() {
-        mAlphaCanvas.drawBitmap(mFlatBitmap, 0, 0, mCopyPaint);
-
-        // Scale the end points:
-        ByteBuffer buffer = ByteBuffer.wrap(mPixels);
-        buffer.rewind();
-        mAlphaBitmap.copyPixelsToBuffer(buffer);
-
-        int min = 0xFF;
-        int max = 0;
-        for (byte b : mPixels) {
-            min = Math.min(min, b & 0xFF);
-            max = Math.max(max, b & 0xFF);
-        }
-
-        if (min < max) {
-            // rescale pixels to increase contrast
-            float range = max - min;
-
-            // In order to check if the colors should be flipped, we just take the average color
-            // of top and bottom edge which should correspond to be background color. If the edge
-            // colors have more opacity, we flip the colors;
-            int sum = 0;
-            for (int i = 0; i < mEdgePixelLength; i++) {
-                sum += (mPixels[i] & 0xFF);
-                sum += (mPixels[mPixels.length - 1 - i] & 0xFF);
-            }
-            float edgeAverage = sum / (mEdgePixelLength * 2f);
-            float edgeMapped = (edgeAverage - min) / range;
-            boolean flipColor = edgeMapped > .5f;
-
-            for (int i = 0; i < mPixels.length; i++) {
-                int p = mPixels[i] & 0xFF;
-                int p2 = Math.round((p - min) * 0xFF / range);
-                mPixels[i] = flipColor ? (byte) (255 - p2) : (byte) (p2);
-            }
-            buffer.rewind();
-            mAlphaBitmap.copyPixelsFromBuffer(buffer);
-        }
-    }
-
-    @Override
-    public void draw(Canvas canvas) {
-        canvas.drawBitmap(mAlphaBitmap, mSrcRect, getBounds(), mDrawPaint);
-    }
-
-    @Override
-    public int getOpacity() {
-        return PixelFormat.TRANSLUCENT;
-    }
-
-    @Override
-    public void setAlpha(int i) {
-        mDrawPaint.setAlpha(i);
-    }
-
-    @Override
-    public void setColorFilter(ColorFilter colorFilter) {
-        mDrawPaint.setColorFilter(colorFilter);
-    }
-}
diff --git a/src/com/android/launcher3/icons/ShortcutCachingLogic.java b/src/com/android/launcher3/icons/ShortcutCachingLogic.java
deleted file mode 100644
index 7bb39e1..0000000
--- a/src/com/android/launcher3/icons/ShortcutCachingLogic.java
+++ /dev/null
@@ -1,117 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.icons;
-
-import static com.android.launcher3.BuildConfig.WIDGETS_ENABLED;
-
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.pm.LauncherApps;
-import android.content.pm.PackageInfo;
-import android.content.pm.ShortcutInfo;
-import android.graphics.drawable.Drawable;
-import android.os.UserHandle;
-import android.text.TextUtils;
-import android.util.Log;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.icons.BaseIconFactory.IconOptions;
-import com.android.launcher3.icons.cache.BaseIconCache;
-import com.android.launcher3.icons.cache.CachingLogic;
-import com.android.launcher3.shortcuts.ShortcutKey;
-import com.android.launcher3.util.Themes;
-
-/**
- * Caching logic for shortcuts.
- */
-public class ShortcutCachingLogic implements CachingLogic<ShortcutInfo> {
-
-    private static final String TAG = "ShortcutCachingLogic";
-
-    @Override
-    @NonNull
-    public ComponentName getComponent(@NonNull ShortcutInfo info) {
-        return ShortcutKey.fromInfo(info).componentName;
-    }
-
-    @NonNull
-    @Override
-    public UserHandle getUser(@NonNull ShortcutInfo info) {
-        return info.getUserHandle();
-    }
-
-    @NonNull
-    @Override
-    public CharSequence getLabel(@NonNull ShortcutInfo info) {
-        return info.getShortLabel();
-    }
-
-    @Override
-    @NonNull
-    public CharSequence getDescription(@NonNull ShortcutInfo object,
-            @NonNull CharSequence fallback) {
-        CharSequence label = object.getLongLabel();
-        return TextUtils.isEmpty(label) ? fallback : label;
-    }
-
-    @NonNull
-    @Override
-    public BitmapInfo loadIcon(@NonNull Context context, @NonNull BaseIconCache cache,
-            @NonNull ShortcutInfo info) {
-        try (LauncherIcons li = LauncherIcons.obtain(context)) {
-            Drawable unbadgedDrawable = ShortcutCachingLogic.getIcon(
-                    context, info, LauncherAppState.getIDP(context).fillResIconDpi);
-            if (unbadgedDrawable == null) return BitmapInfo.LOW_RES_INFO;
-            return li.createBadgedIconBitmap(unbadgedDrawable,
-                    new IconOptions().setExtractedColor(Themes.getColorAccent(context)));
-        }
-    }
-
-    @Override
-    public long getLastUpdatedTime(@Nullable ShortcutInfo shortcutInfo,
-            @NonNull PackageInfo info) {
-        if (shortcutInfo == null) {
-            return info.lastUpdateTime;
-        }
-        return Math.max(shortcutInfo.getLastChangedTimestamp(), info.lastUpdateTime);
-    }
-
-    @Override
-    public boolean addToMemCache() {
-        return false;
-    }
-
-    /**
-     * Similar to {@link LauncherApps#getShortcutIconDrawable(ShortcutInfo, int)} with additional
-     * Launcher specific checks
-     */
-    public static Drawable getIcon(Context context, ShortcutInfo shortcutInfo, int density) {
-        if (!WIDGETS_ENABLED) {
-            return null;
-        }
-        try {
-            return context.getSystemService(LauncherApps.class)
-                    .getShortcutIconDrawable(shortcutInfo, density);
-        } catch (SecurityException | IllegalStateException | NullPointerException e) {
-            Log.e(TAG, "Failed to get shortcut icon", e);
-            return null;
-        }
-    }
-}
diff --git a/src/com/android/launcher3/logging/StatsLogManager.java b/src/com/android/launcher3/logging/StatsLogManager.java
index fbd24d8..2550ebb 100644
--- a/src/com/android/launcher3/logging/StatsLogManager.java
+++ b/src/com/android/launcher3/logging/StatsLogManager.java
@@ -175,6 +175,10 @@
         @UiEvent(doc = "User searched for a widget in the widget picker.")
         LAUNCHER_WIDGETSTRAY_SEARCHED(819),
 
+        @UiEvent(doc = "User clicked on view all button to expand the displayed list in the "
+                + "widget picker.")
+        LAUNCHER_WIDGETSTRAY_EXPAND_PRESS(1978),
+
         @UiEvent(doc = "A dragged item is dropped on 'Remove' button in the target bar")
         LAUNCHER_ITEM_DROPPED_ON_REMOVE(465),
 
@@ -221,6 +225,9 @@
         @UiEvent(doc = "User tapped on desktop icon on a task menu.")
         LAUNCHER_SYSTEM_SHORTCUT_DESKTOP_TAP(1706),
 
+        @UiEvent(doc = "Use tapped on external display icon on a task menu,")
+        LAUNCHER_SYSTEM_SHORTCUT_EXTERNAL_DISPLAY_TAP(1957),
+
         @UiEvent(doc = "User tapped on pause app system shortcut.")
         LAUNCHER_SYSTEM_SHORTCUT_PAUSE_TAP(521),
 
@@ -798,6 +805,44 @@
         @UiEvent(doc = "User long pressed on the taskbar IME switcher button")
         LAUNCHER_TASKBAR_IME_SWITCHER_BUTTON_LONGPRESS(1798),
 
+        @UiEvent(doc = "Failed to launch assistant due to Google assistant not available")
+        LAUNCHER_LAUNCH_ASSISTANT_FAILED_NOT_AVAILABLE(1465),
+
+        @UiEvent(doc = "Failed to launch assistant due to service error")
+        LAUNCHER_LAUNCH_ASSISTANT_FAILED_SERVICE_ERROR(1466),
+
+        @UiEvent(doc = "User launched assistant by long-pressing nav handle")
+        LAUNCHER_LAUNCH_ASSISTANT_SUCCESSFUL_NAV_HANDLE(1467),
+
+        @UiEvent(doc = "Failed to launch due to Contextual Search not available")
+        LAUNCHER_LAUNCH_OMNI_FAILED_NOT_AVAILABLE(1471),
+
+        @UiEvent(doc = "Failed to launch due to Contextual Search setting disabled")
+        LAUNCHER_LAUNCH_OMNI_FAILED_SETTING_DISABLED(1632),
+
+        @UiEvent(doc = "User launched Contextual Search by long-pressing home in 3-button mode")
+        LAUNCHER_LAUNCH_OMNI_SUCCESSFUL_HOME(1481),
+
+        @UiEvent(doc = "User launched Contextual Search by using accessibility System Action")
+        LAUNCHER_LAUNCH_OMNI_SUCCESSFUL_SYSTEM_ACTION(1492),
+
+        @UiEvent(doc = "User launched Contextual Search by long pressing the meta key")
+        LAUNCHER_LAUNCH_OMNI_SUCCESSFUL_META(1606),
+
+        @UiEvent(doc = "Contextual Search invocation was attempted over the notification shade")
+        LAUNCHER_LAUNCH_OMNI_ATTEMPTED_OVER_NOTIFICATION_SHADE(1485),
+
+        @UiEvent(doc = "The Contextual Search all entrypoints toggle value in Settings")
+        LAUNCHER_SETTINGS_OMNI_ALL_ENTRYPOINTS_TOGGLE_VALUE(1633),
+
+        @UiEvent(doc = "Contextual Search invocation was attempted over the keyguard")
+        LAUNCHER_LAUNCH_OMNI_ATTEMPTED_OVER_KEYGUARD(1501),
+
+        @UiEvent(doc = "Contextual Search invocation was attempted while splitscreen is active")
+        LAUNCHER_LAUNCH_OMNI_ATTEMPTED_SPLITSCREEN(1505),
+
+        @UiEvent(doc = "User long press nav handle and a long press runnable was created.")
+        LAUNCHER_OMNI_GET_LONG_PRESS_RUNNABLE(1545),
         // ADD MORE
         ;
 
@@ -828,6 +873,10 @@
 
         @UiEvent(doc = "The duration of asynchronous loading workspace")
         LAUNCHER_LATENCY_STARTUP_WORKSPACE_LOADER_ASYNC(1367),
+
+        @UiEvent(doc = "Time passed between Contextual Search runnable creation and execution. This"
+                + " ensures that Recent animations have finished before Contextual Search starts.")
+        LAUNCHER_LATENCY_OMNI_RUNNABLE(1546),
         ;
 
         private final int mId;
diff --git a/src/com/android/launcher3/model/AddWorkspaceItemsTask.java b/src/com/android/launcher3/model/AddWorkspaceItemsTask.java
index 427fb97..55bcb70 100644
--- a/src/com/android/launcher3/model/AddWorkspaceItemsTask.java
+++ b/src/com/android/launcher3/model/AddWorkspaceItemsTask.java
@@ -41,6 +41,7 @@
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.pm.InstallSessionHelper;
 import com.android.launcher3.pm.PackageInstallInfo;
+import com.android.launcher3.util.ApplicationInfoWrapper;
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.PackageManagerHelper;
 
@@ -103,8 +104,8 @@
                     }
 
                     // b/139663018 Short-circuit this logic if the icon is a system app
-                    if (PackageManagerHelper.isSystemApp(context,
-                            Objects.requireNonNull(item.getIntent()))) {
+                    if (new ApplicationInfoWrapper(context,
+                            Objects.requireNonNull(item.getIntent())).isSystem()) {
                         continue;
                     }
 
diff --git a/src/com/android/launcher3/model/AllAppsList.java b/src/com/android/launcher3/model/AllAppsList.java
index 1f60f13..7bc9273 100644
--- a/src/com/android/launcher3/model/AllAppsList.java
+++ b/src/com/android/launcher3/model/AllAppsList.java
@@ -40,6 +40,7 @@
 import com.android.launcher3.pm.PackageInstallInfo;
 import com.android.launcher3.pm.UserCache;
 import com.android.launcher3.util.ApiWrapper;
+import com.android.launcher3.util.ApplicationInfoWrapper;
 import com.android.launcher3.util.FlagOp;
 import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.util.SafeCloseable;
@@ -169,8 +170,8 @@
     public AppInfo addPromiseApp(
             Context context, PackageInstallInfo installInfo, boolean loadIcon) {
         // only if not yet installed
-        if (PackageManagerHelper.INSTANCE.get(context)
-                .isAppInstalled(installInfo.packageName, installInfo.user)) {
+        if (new ApplicationInfoWrapper(context, installInfo.packageName, installInfo.user)
+                .isInstalled()) {
             return null;
         }
         AppInfo promiseAppInfo = new AppInfo(installInfo);
diff --git a/src/com/android/launcher3/model/BaseLauncherBinder.java b/src/com/android/launcher3/model/BaseLauncherBinder.java
index 5faa2b8..c251114 100644
--- a/src/com/android/launcher3/model/BaseLauncherBinder.java
+++ b/src/com/android/launcher3/model/BaseLauncherBinder.java
@@ -24,6 +24,8 @@
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 
+import static java.util.Collections.emptyList;
+
 import android.os.Process;
 import android.os.Trace;
 import android.util.Log;
@@ -36,7 +38,6 @@
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherModel.CallbackTask;
 import com.android.launcher3.LauncherSettings;
-import com.android.launcher3.Workspace;
 import com.android.launcher3.celllayout.CellPosMapper;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.model.BgDataModel.Callbacks;
@@ -44,6 +45,7 @@
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
+import com.android.launcher3.model.data.PackageItemInfo;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.IntSet;
@@ -59,12 +61,11 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
-import java.util.Set;
 import java.util.concurrent.Executor;
+import java.util.function.Predicate;
 import java.util.stream.Collectors;
 
 /**
@@ -100,13 +101,29 @@
     public void bindWorkspace(boolean incrementBindId, boolean isBindSync) {
         Trace.beginSection("BaseLauncherBinder#bindWorkspace");
         try {
-            if (FeatureFlags.ENABLE_WORKSPACE_LOADING_OPTIMIZATION.get()) {
-                DisjointWorkspaceBinder workspaceBinder =
-                    initWorkspaceBinder(incrementBindId, mBgDataModel.collectWorkspaceScreens());
-                workspaceBinder.bindCurrentWorkspacePages(isBindSync);
-                workspaceBinder.bindOtherWorkspacePages();
-            } else {
-                bindWorkspaceAllAtOnce(incrementBindId, isBindSync);
+            // Save a copy of all the bg-thread collections
+            ArrayList<ItemInfo> workspaceItems = new ArrayList<>();
+            ArrayList<LauncherAppWidgetInfo> appWidgets = new ArrayList<>();
+            final IntArray orderedScreenIds = new IntArray();
+            ArrayList<FixedContainerItems> extraItems = new ArrayList<>();
+            final int workspaceItemCount;
+            synchronized (mBgDataModel) {
+                workspaceItems.addAll(mBgDataModel.workspaceItems);
+                appWidgets.addAll(mBgDataModel.appWidgets);
+                orderedScreenIds.addAll(mBgDataModel.collectWorkspaceScreens());
+                mBgDataModel.extraItems.forEach(extraItems::add);
+                if (incrementBindId) {
+                    mBgDataModel.lastBindId++;
+                    mBgDataModel.lastLoadId = mApp.getModel().getLastLoadId();
+                }
+                mMyBindingId = mBgDataModel.lastBindId;
+                workspaceItemCount = mBgDataModel.itemsIdMap.size();
+            }
+
+            for (Callbacks cb : mCallbacksList) {
+                new UnifiedWorkspaceBinder(cb, mUiExecutor, mApp, mBgDataModel, mMyBindingId,
+                        workspaceItems, appWidgets, extraItems, orderedScreenIds)
+                        .bind(isBindSync, workspaceItemCount);
             }
         } finally {
             Trace.endSection();
@@ -114,53 +131,6 @@
     }
 
     /**
-     * Initializes the WorkspaceBinder for binding.
-     *
-     * @param incrementBindId this is used to stop previously started binding tasks that are
-     *                        obsolete but still queued.
-     * @param workspacePages this allows the Launcher to add the correct workspace screens.
-     */
-    public DisjointWorkspaceBinder initWorkspaceBinder(boolean incrementBindId,
-            IntArray workspacePages) {
-
-        synchronized (mBgDataModel) {
-            if (incrementBindId) {
-                mBgDataModel.lastBindId++;
-                mBgDataModel.lastLoadId = mApp.getModel().getLastLoadId();
-            }
-            mMyBindingId = mBgDataModel.lastBindId;
-            return new DisjointWorkspaceBinder(workspacePages);
-        }
-    }
-
-    private void bindWorkspaceAllAtOnce(boolean incrementBindId, boolean isBindSync) {
-        // Save a copy of all the bg-thread collections
-        ArrayList<ItemInfo> workspaceItems = new ArrayList<>();
-        ArrayList<LauncherAppWidgetInfo> appWidgets = new ArrayList<>();
-        final IntArray orderedScreenIds = new IntArray();
-        ArrayList<FixedContainerItems> extraItems = new ArrayList<>();
-        final int workspaceItemCount;
-        synchronized (mBgDataModel) {
-            workspaceItems.addAll(mBgDataModel.workspaceItems);
-            appWidgets.addAll(mBgDataModel.appWidgets);
-            orderedScreenIds.addAll(mBgDataModel.collectWorkspaceScreens());
-            mBgDataModel.extraItems.forEach(extraItems::add);
-            if (incrementBindId) {
-                mBgDataModel.lastBindId++;
-                mBgDataModel.lastLoadId = mApp.getModel().getLastLoadId();
-            }
-            mMyBindingId = mBgDataModel.lastBindId;
-            workspaceItemCount = mBgDataModel.itemsIdMap.size();
-        }
-
-        for (Callbacks cb : mCallbacksList) {
-            new UnifiedWorkspaceBinder(cb, mUiExecutor, mApp, mBgDataModel, mMyBindingId,
-                    workspaceItems, appWidgets, extraItems, orderedScreenIds)
-                    .bind(isBindSync, workspaceItemCount);
-        }
-    }
-
-    /**
      * BindDeepShortcuts is abstract because it is a no-op for the go launcher.
      */
     public void bindDeepShortcuts() {
@@ -196,9 +166,17 @@
         if (!WIDGETS_ENABLED) {
             return;
         }
+        Map<PackageItemInfo, List<WidgetItem>>
+                widgetsByPackageItem = mBgDataModel.widgetsModel.getWidgetsByPackageItem();
         List<WidgetsListBaseEntry> widgets = new WidgetsListBaseEntriesBuilder(mApp.getContext())
-                .build(mBgDataModel.widgetsModel.getWidgetsByPackageItem());
-        executeCallbacksTask(c -> c.bindAllWidgets(widgets), mUiExecutor);
+                .build(widgetsByPackageItem);
+        Predicate<WidgetItem> filter = mBgDataModel.widgetsModel.getDefaultWidgetsFilter();
+        List<WidgetsListBaseEntry> defaultWidgets =
+                filter != null ? new WidgetsListBaseEntriesBuilder(
+                        mApp.getContext()).build(widgetsByPackageItem,
+                        mBgDataModel.widgetsModel.getDefaultWidgetsFilter()) : emptyList();
+
+        executeCallbacksTask(c -> c.bindAllWidgets(widgets, defaultWidgets), mUiExecutor);
     }
 
     /**
@@ -347,10 +325,8 @@
                 bindItemsInChunks(currentWorkspaceItems, ITEMS_CHUNK, mUiExecutor);
                 bindItemsInChunks(currentAppWidgets, 1, mUiExecutor);
             }
-            if (!FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) {
-                mExtraItems.forEach(item ->
-                        executeCallbacksTask(c -> c.bindExtraContainerItems(item), mUiExecutor));
-            }
+            mExtraItems.forEach(item ->
+                    executeCallbacksTask(c -> c.bindExtraContainerItems(item), mUiExecutor));
 
             RunnableList pendingTasks = new RunnableList();
             Executor pendingExecutor = pendingTasks::add;
@@ -440,126 +416,4 @@
             });
         }
     }
-
-    private class DisjointWorkspaceBinder {
-        private final IntArray mOrderedScreenIds;
-        private final IntSet mCurrentScreenIds = new IntSet();
-        private final Set<Integer> mBoundItemIds = new HashSet<>();
-
-        protected DisjointWorkspaceBinder(IntArray orderedScreenIds) {
-            mOrderedScreenIds = orderedScreenIds;
-
-            for (Callbacks cb : mCallbacksList) {
-                mCurrentScreenIds.addAll(cb.getPagesToBindSynchronously(orderedScreenIds));
-            }
-            if (mCurrentScreenIds.size() == 0) {
-                mCurrentScreenIds.add(Workspace.FIRST_SCREEN_ID);
-            }
-        }
-
-        /**
-         * Binds the currently loaded items in the Data Model. Also signals to the Callbacks[]
-         * that these items have been bound and their respective screens are ready to be shown.
-         *
-         * If this method is called after all the items on the workspace screen have already been
-         * loaded, it will bind all workspace items immediately, and bindOtherWorkspacePages() will
-         * not bind any items.
-         */
-        protected void bindCurrentWorkspacePages(boolean isBindSync) {
-            // Save a copy of all the bg-thread collections
-            ArrayList<ItemInfo> workspaceItems;
-            ArrayList<LauncherAppWidgetInfo> appWidgets;
-            ArrayList<FixedContainerItems> fciList = new ArrayList<>();
-            final int workspaceItemCount;
-            synchronized (mBgDataModel) {
-                workspaceItems = new ArrayList<>(mBgDataModel.workspaceItems);
-                appWidgets = new ArrayList<>(mBgDataModel.appWidgets);
-                if (!FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) {
-                    mBgDataModel.extraItems.forEach(fciList::add);
-                }
-                workspaceItemCount = mBgDataModel.itemsIdMap.size();
-            }
-
-            workspaceItems.forEach(it -> mBoundItemIds.add(it.id));
-            appWidgets.forEach(it -> mBoundItemIds.add(it.id));
-            if (!FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) {
-                fciList.forEach(item ->
-                        executeCallbacksTask(c -> c.bindExtraContainerItems(item), mUiExecutor));
-            }
-
-            sortWorkspaceItemsSpatially(mApp.getInvariantDeviceProfile(), workspaceItems);
-
-            // Tell the workspace that we're about to start binding items
-            executeCallbacksTask(c -> {
-                c.clearPendingBinds();
-                c.startBinding();
-            }, mUiExecutor);
-
-            // Bind workspace screens
-            executeCallbacksTask(c -> c.bindScreens(mOrderedScreenIds), mUiExecutor);
-
-            bindWorkspaceItems(workspaceItems);
-            bindAppWidgets(appWidgets);
-            executeCallbacksTask(c -> {
-                MODEL_EXECUTOR.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
-
-                RunnableList onCompleteSignal = new RunnableList();
-                onCompleteSignal.executeAllAndDestroy();
-                c.onInitialBindComplete(mCurrentScreenIds, new RunnableList(), onCompleteSignal,
-                        workspaceItemCount, isBindSync);
-            }, mUiExecutor);
-        }
-
-        protected void bindOtherWorkspacePages() {
-            // Save a copy of all the bg-thread collections
-            ArrayList<ItemInfo> workspaceItems;
-            ArrayList<LauncherAppWidgetInfo> appWidgets;
-
-            synchronized (mBgDataModel) {
-                workspaceItems = new ArrayList<>(mBgDataModel.workspaceItems);
-                appWidgets = new ArrayList<>(mBgDataModel.appWidgets);
-            }
-
-            workspaceItems.removeIf(it -> mBoundItemIds.contains(it.id));
-            appWidgets.removeIf(it -> mBoundItemIds.contains(it.id));
-
-            sortWorkspaceItemsSpatially(mApp.getInvariantDeviceProfile(), workspaceItems);
-
-            bindWorkspaceItems(workspaceItems);
-            bindAppWidgets(appWidgets);
-
-            executeCallbacksTask(c -> c.finishBindingItems(mCurrentScreenIds), mUiExecutor);
-            mUiExecutor.execute(() -> {
-                MODEL_EXECUTOR.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT);
-                ItemInstallQueue.INSTANCE.get(mApp.getContext())
-                        .resumeModelPush(FLAG_LOADER_RUNNING);
-            });
-
-            StringCache cacheClone = mBgDataModel.stringCache.clone();
-            executeCallbacksTask(c -> c.bindStringCache(cacheClone), mUiExecutor);
-        }
-
-        private void bindWorkspaceItems(final ArrayList<ItemInfo> workspaceItems) {
-            // Bind the workspace items
-            int count = workspaceItems.size();
-            for (int i = 0; i < count; i += ITEMS_CHUNK) {
-                final int start = i;
-                final int chunkSize = (i + ITEMS_CHUNK <= count) ? ITEMS_CHUNK : (count - i);
-                executeCallbacksTask(
-                        c -> c.bindItems(workspaceItems.subList(start, start + chunkSize), false),
-                        mUiExecutor);
-            }
-        }
-
-        private void bindAppWidgets(List<LauncherAppWidgetInfo> appWidgets) {
-            // Bind the widgets, one at a time
-            int count = appWidgets.size();
-            for (int i = 0; i < count; i++) {
-                final ItemInfo widget = appWidgets.get(i);
-                executeCallbacksTask(
-                        c -> c.bindItems(Collections.singletonList(widget), false),
-                        mUiExecutor);
-            }
-        }
-    }
 }
diff --git a/src/com/android/launcher3/model/BgDataModel.java b/src/com/android/launcher3/model/BgDataModel.java
index 9a9fa5b..b9b1e98 100644
--- a/src/com/android/launcher3/model/BgDataModel.java
+++ b/src/com/android/launcher3/model/BgDataModel.java
@@ -537,7 +537,13 @@
         default void bindWidgetsRestored(ArrayList<LauncherAppWidgetInfo> widgets) { }
         default void bindRestoreItemsChange(HashSet<ItemInfo> updates) { }
         default void bindWorkspaceComponentsRemoved(Predicate<ItemInfo> matcher) { }
-        default void bindAllWidgets(List<WidgetsListBaseEntry> widgets) { }
+
+        /**
+         * Binds the app widgets to the providers that share widgets with the UI.
+         */
+        default void bindAllWidgets(@NonNull List<WidgetsListBaseEntry> widgets,
+                @NonNull List<WidgetsListBaseEntry> defaultWidgets) {
+        }
         default void bindSmartspaceWidget() { }
 
         /** Called when workspace has been bound. */
diff --git a/src/com/android/launcher3/model/DbEntry.kt b/src/com/android/launcher3/model/DbEntry.kt
new file mode 100644
index 0000000..b79d312
--- /dev/null
+++ b/src/com/android/launcher3/model/DbEntry.kt
@@ -0,0 +1,141 @@
+/*
+ * 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
+
+import android.content.ContentValues
+import android.content.Intent
+import android.util.Log
+import com.android.launcher3.LauncherSettings
+import com.android.launcher3.LauncherSettings.Favorites.CELLX
+import com.android.launcher3.LauncherSettings.Favorites.CELLY
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_FOLDER
+import com.android.launcher3.LauncherSettings.Favorites.SCREEN
+import com.android.launcher3.LauncherSettings.Favorites.SPANX
+import com.android.launcher3.LauncherSettings.Favorites.SPANY
+import com.android.launcher3.model.data.ItemInfo
+import com.android.launcher3.util.ContentWriter
+import java.net.URISyntaxException
+import java.util.Objects
+
+class DbEntry : ItemInfo(), Comparable<DbEntry> {
+    @JvmField var mIntent: String? = null
+    @JvmField var mProvider: String? = null
+    @JvmField var mFolderItems: MutableMap<String, Set<Int>> = HashMap()
+
+    /** Id of the specific widget. */
+    @JvmField var appWidgetId: Int = NO_ID
+
+    /** Comparator according to the reading order */
+    override fun compareTo(other: DbEntry): Int {
+        if (screenId != other.screenId) {
+            return screenId.compareTo(other.screenId)
+        }
+        if (cellY != other.cellY) {
+            return cellY.compareTo(other.cellY)
+        }
+        return cellX.compareTo(other.cellX)
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is DbEntry) return false
+        return getEntryMigrationId() == other.getEntryMigrationId()
+    }
+
+    override fun hashCode(): Int = Objects.hash(getEntryMigrationId())
+
+    /**
+     * Puts the updated DbEntry values into ContentValues which we then use to insert the entry to
+     * the DB.
+     */
+    fun updateContentValues(values: ContentValues) =
+        values.apply {
+            put(SCREEN, screenId)
+            put(CELLX, cellX)
+            put(CELLY, cellY)
+            put(SPANX, spanX)
+            put(SPANY, spanY)
+        }
+
+    override fun writeToValues(writer: ContentWriter) {
+        super.writeToValues(writer)
+        writer.put(LauncherSettings.Favorites.APPWIDGET_ID, appWidgetId)
+    }
+
+    override fun readFromValues(values: ContentValues) {
+        super.readFromValues(values)
+        appWidgetId = values.getAsInteger(LauncherSettings.Favorites.APPWIDGET_ID)
+    }
+
+    /**
+     * This id is not used in the DB is only used while doing the migration and it identifies an
+     * entry on each workspace. For example two calculator icons would have the same migration id
+     * even thought they have different database ids.
+     */
+    private fun getEntryMigrationId(): String? {
+        when (itemType) {
+            ITEM_TYPE_FOLDER,
+            ITEM_TYPE_APP_PAIR -> return getFolderMigrationId()
+            ITEM_TYPE_APPWIDGET ->
+                // mProvider is the app the widget belongs to and appWidgetId it's the unique
+                // is of the widget, we need both because if you remove a widget and then add it
+                // again, then it can change and the WidgetProvider would not know the widget.
+                return mProvider + appWidgetId
+            ITEM_TYPE_APPLICATION -> {
+                val intentStr = mIntent?.let { cleanIntentString(it) }
+                try {
+                    val i = Intent.parseUri(intentStr, 0)
+                    return Objects.requireNonNull(i.component).toString()
+                } catch (e: Exception) {
+                    return intentStr
+                }
+            }
+
+            else -> return mIntent?.let { cleanIntentString(it) }
+        }
+    }
+
+    /**
+     * This method should return an id that should be the same for two folders containing the same
+     * elements.
+     */
+    private fun getFolderMigrationId(): String =
+        mFolderItems.keys
+            .map { intentString: String ->
+                mFolderItems[intentString]?.size.toString() + cleanIntentString(intentString)
+            }
+            .sorted()
+            .joinToString(",")
+
+    /**
+     * This is needed because sourceBounds can change and make the id of two equal items different.
+     */
+    private fun cleanIntentString(intentStr: String): String {
+        try {
+            return Intent.parseUri(intentStr, 0).apply { sourceBounds = null }.toURI()
+        } catch (e: URISyntaxException) {
+            Log.e(TAG, "Unable to parse Intent string", e)
+            return intentStr
+        }
+    }
+
+    companion object {
+        private const val TAG = "DbEntry"
+    }
+}
diff --git a/src/com/android/launcher3/model/DeviceGridState.java b/src/com/android/launcher3/model/DeviceGridState.java
index 729b381..90af215 100644
--- a/src/com/android/launcher3/model/DeviceGridState.java
+++ b/src/com/android/launcher3/model/DeviceGridState.java
@@ -156,10 +156,16 @@
     }
 
     public Integer getColumns() {
+        if (TextUtils.isEmpty(mGridSizeString)) {
+            return -1;
+        }
         return Integer.parseInt(String.valueOf(mGridSizeString.split(",")[0]));
     }
 
     public Integer getRows() {
+        if (TextUtils.isEmpty(mGridSizeString)) {
+            return -1;
+        }
         return Integer.parseInt(String.valueOf(mGridSizeString.split(",")[1]));
     }
 
diff --git a/src/com/android/launcher3/model/GridSizeMigrationUtil.java b/src/com/android/launcher3/model/GridSizeMigrationDBController.java
similarity index 81%
rename from src/com/android/launcher3/model/GridSizeMigrationUtil.java
rename to src/com/android/launcher3/model/GridSizeMigrationDBController.java
index 942b97c..617cac7 100644
--- a/src/com/android/launcher3/model/GridSizeMigrationUtil.java
+++ b/src/com/android/launcher3/model/GridSizeMigrationDBController.java
@@ -27,7 +27,6 @@
 import android.content.ComponentName;
 import android.content.ContentValues;
 import android.content.Context;
-import android.content.Intent;
 import android.database.Cursor;
 import android.database.DatabaseUtils;
 import android.database.sqlite.SQLiteDatabase;
@@ -38,21 +37,17 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.VisibleForTesting;
 
-import com.android.launcher3.Flags;
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherPrefs;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction;
-import com.android.launcher3.util.ContentWriter;
 import com.android.launcher3.util.GridOccupancy;
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.WidgetManagerHelper;
 
-import java.net.URISyntaxException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
@@ -60,7 +55,6 @@
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
-import java.util.Objects;
 import java.util.Set;
 import java.util.stream.Collectors;
 
@@ -68,12 +62,12 @@
  * This class takes care of shrinking the workspace (by maximum of one row and one column), as a
  * result of restoring from a larger device or device density change.
  */
-public class GridSizeMigrationUtil {
+public class GridSizeMigrationDBController {
 
-    private static final String TAG = "GridSizeMigrationUtil";
+    private static final String TAG = "GridSizeMigrationDBController";
     private static final boolean DEBUG = true;
 
-    private GridSizeMigrationUtil() {
+    private GridSizeMigrationDBController() {
         // Util class should not be instantiated
     }
 
@@ -84,16 +78,22 @@
         return needsToMigrate(new DeviceGridState(context), new DeviceGridState(idp));
     }
 
-    private static boolean needsToMigrate(
+    static boolean needsToMigrate(
             DeviceGridState srcDeviceState, DeviceGridState destDeviceState) {
         boolean needsToMigrate = !destDeviceState.isCompatible(srcDeviceState);
         if (needsToMigrate) {
             Log.i(TAG, "Migration is needed. destDeviceState: " + destDeviceState
                     + ", srcDeviceState: " + srcDeviceState);
+        } else {
+            Log.i(TAG, "Migration is not needed. destDeviceState: " + destDeviceState
+                    + ", srcDeviceState: " + srcDeviceState);
         }
         return needsToMigrate;
     }
 
+    /**
+     * @return all the workspace and hotseat entries in the db.
+     */
     @VisibleForTesting
     public static List<DbEntry> readAllEntries(SQLiteDatabase db, String tableName,
             Context context) {
@@ -117,22 +117,16 @@
             @NonNull DeviceGridState srcDeviceState,
             @NonNull DeviceGridState destDeviceState,
             @NonNull DatabaseHelper target,
-            @NonNull SQLiteDatabase source) {
-
-        Log.i("b/360462379", "Going from " + srcDeviceState.getColumns() + "x"
-                + srcDeviceState.getRows());
-        Log.i("b/360462379", "Going to " + destDeviceState.getColumns() + "x"
-                + destDeviceState.getRows());
+            @NonNull SQLiteDatabase source,
+            boolean isDestNewDb) {
 
         if (!needsToMigrate(srcDeviceState, destDeviceState)) {
-            Log.i("b/360462379", "Does not need to migrate.");
             return true;
         }
 
-        if (Flags.enableGridMigrationFix()
+        if (isDestNewDb
                 && srcDeviceState.getColumns().equals(destDeviceState.getColumns())
                 && srcDeviceState.getRows() < destDeviceState.getRows()) {
-            Log.i("b/360462379", "Grid migration fix entry point.");
             // Only use this strategy when comparing the previous grid to the new grid and the
             // columns are the same and the destination has more rows
             copyTable(source, TABLE_NAME, target.getWritableDatabase(), TABLE_NAME, context);
@@ -196,7 +190,7 @@
                     Collectors.joining(",\n", "[", "]"))
                     + "\n Removing Items:"
                     + dstWorkspaceItems.stream().filter(entry ->
-                            toBeRemoved.contains(entry.id)).map(DbEntry::toString).collect(
+                    toBeRemoved.contains(entry.id)).map(DbEntry::toString).collect(
                     Collectors.joining(",\n", "[", "]"))
                     + "\n Adding Workspace Items:"
                     + workspaceToBeAdded.stream().map(DbEntry::toString).collect(
@@ -289,7 +283,7 @@
         });
     }
 
-    private static void insertEntryInDb(DatabaseHelper helper, DbEntry entry,
+    static void insertEntryInDb(DatabaseHelper helper, DbEntry entry,
             String srcTableName, String destTableName, List<Integer> idsInUse) {
         int id = copyEntryAndUpdate(helper, entry, srcTableName, destTableName, idsInUse);
         if (entry.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER
@@ -328,10 +322,9 @@
             } else {
                 values.put(LauncherSettings.Favorites.CONTAINER, folderId);
             }
-            newId = helper.generateNewItemId();
-            while (idsInUse.contains(newId)) {
+            do {
                 newId = helper.generateNewItemId();
-            }
+            } while (idsInUse.contains(newId));
             values.put(LauncherSettings.Favorites._ID, newId);
             helper.getWritableDatabase().insert(destTableName, null, values);
         }
@@ -339,7 +332,7 @@
         return newId;
     }
 
-    private static void removeEntryFromDb(SQLiteDatabase db, String tableName, IntArray entryIds) {
+    static void removeEntryFromDb(SQLiteDatabase db, String tableName, IntArray entryIds) {
         db.delete(tableName,
                 Utilities.createDbSelectionQuery(LauncherSettings.Favorites._ID, entryIds), null);
     }
@@ -385,7 +378,7 @@
     private static boolean findPlacementForEntry(@NonNull final DbEntry entry,
             @NonNull final Point next, @NonNull final Point trg,
             @NonNull final GridOccupancy occupied, final int screenId) {
-        for (int y = next.y; y <  trg.y; y++) {
+        for (int y = next.y; y < trg.y; y++) {
             for (int x = next.x; x < trg.x; x++) {
                 boolean fits = occupied.isRegionVacant(x, y, entry.spanX, entry.spanY);
                 boolean minFits = occupied.isRegionVacant(x, y, entry.minSpanX,
@@ -411,7 +404,7 @@
     private static void solveHotseatPlacement(
             @NonNull final DatabaseHelper helper, final int hotseatSize,
             @NonNull final DbReader srcReader, @NonNull final DbReader destReader,
-            @NonNull final  List<DbEntry> placedHotseatItems,
+            @NonNull final List<DbEntry> placedHotseatItems,
             @NonNull final List<DbEntry> itemsToPlace, List<Integer> idsInUse) {
 
         final boolean[] occupied = new boolean[hotseatSize];
@@ -434,15 +427,26 @@
         }
     }
 
-    @VisibleForTesting
+    static void copyCurrentGridToNewGrid(
+            @NonNull Context context,
+            @NonNull DeviceGridState destDeviceState,
+            @NonNull DatabaseHelper target,
+            @NonNull SQLiteDatabase source) {
+        // Only use this strategy when comparing the previous grid to the new grid and the
+        // columns are the same and the destination has more rows
+        copyTable(source, TABLE_NAME, target.getWritableDatabase(), TABLE_NAME, context);
+        destDeviceState.writeToPrefs(context);
+    }
+
+    @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
     public static class DbReader {
 
-        private final SQLiteDatabase mDb;
-        private final String mTableName;
-        private final Context mContext;
-        private int mLastScreenId = -1;
+        final SQLiteDatabase mDb;
+        final String mTableName;
+        final Context mContext;
+        int mLastScreenId = -1;
 
-        private final Map<Integer, ArrayList<DbEntry>> mWorkspaceEntriesByScreenId =
+        final Map<Integer, List<DbEntry>> mWorkspaceEntriesByScreenId =
                 new ArrayMap<>();
 
         public DbReader(SQLiteDatabase db, String tableName, Context context) {
@@ -527,7 +531,7 @@
                             LauncherSettings.Favorites.INTENT,               // 7
                             LauncherSettings.Favorites.APPWIDGET_PROVIDER,   // 8
                             LauncherSettings.Favorites.APPWIDGET_ID},        // 9
-                        LauncherSettings.Favorites.CONTAINER + " = "
+                    LauncherSettings.Favorites.CONTAINER + " = "
                             + LauncherSettings.Favorites.CONTAINER_DESKTOP);
             final int indexId = c.getColumnIndexOrThrow(LauncherSettings.Favorites._ID);
             final int indexItemType = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ITEM_TYPE);
@@ -646,118 +650,4 @@
             return mDb.query(mTableName, columns, where, null, null, null, null);
         }
     }
-
-    public static class DbEntry extends ItemInfo implements Comparable<DbEntry> {
-
-        private String mIntent;
-        private String mProvider;
-        private Map<String, Set<Integer>> mFolderItems = new HashMap<>();
-
-        /**
-         * Id of the specific widget.
-         */
-        public int appWidgetId = NO_ID;
-
-        /** Comparator according to the reading order */
-        @Override
-        public int compareTo(DbEntry another) {
-            if (screenId != another.screenId) {
-                return Integer.compare(screenId, another.screenId);
-            }
-            if (cellY != another.cellY) {
-                return Integer.compare(cellY, another.cellY);
-            }
-            return Integer.compare(cellX, another.cellX);
-        }
-
-        @Override
-        public boolean equals(Object o) {
-            if (this == o) return true;
-            if (o == null || getClass() != o.getClass()) return false;
-            DbEntry entry = (DbEntry) o;
-            return Objects.equals(getEntryMigrationId(), entry.getEntryMigrationId());
-        }
-
-        @Override
-        public int hashCode() {
-            return Objects.hash(getEntryMigrationId());
-        }
-
-        public void updateContentValues(ContentValues values) {
-            values.put(LauncherSettings.Favorites.SCREEN, screenId);
-            values.put(LauncherSettings.Favorites.CELLX, cellX);
-            values.put(LauncherSettings.Favorites.CELLY, cellY);
-            values.put(LauncherSettings.Favorites.SPANX, spanX);
-            values.put(LauncherSettings.Favorites.SPANY, spanY);
-        }
-
-        @Override
-        public void writeToValues(@NonNull ContentWriter writer) {
-            super.writeToValues(writer);
-            writer.put(LauncherSettings.Favorites.APPWIDGET_ID, appWidgetId);
-        }
-
-        @Override
-        public void readFromValues(@NonNull ContentValues values) {
-            super.readFromValues(values);
-            appWidgetId = values.getAsInteger(LauncherSettings.Favorites.APPWIDGET_ID);
-        }
-
-        /** This id is not used in the DB is only used while doing the migration and it identifies
-         * an entry on each workspace. For example two calculator icons would have the same
-         * migration id even thought they have different database ids.
-         */
-        public String getEntryMigrationId() {
-            switch (itemType) {
-                case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
-                case LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR:
-                    return getFolderMigrationId();
-                case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET:
-                    // mProvider is the app the widget belongs to and appWidgetId it's the unique
-                    // is of the widget, we need both because if you remove a widget and then add it
-                    // again, then it can change and the WidgetProvider would not know the widget.
-                    return mProvider + appWidgetId;
-                case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
-                    final String intentStr = cleanIntentString(mIntent);
-                    try {
-                        Intent i = Intent.parseUri(intentStr, 0);
-                        return Objects.requireNonNull(i.getComponent()).toString();
-                    } catch (Exception e) {
-                        return intentStr;
-                    }
-                default:
-                    return cleanIntentString(mIntent);
-            }
-        }
-
-        /**
-         * This method should return an id that should be the same for two folders containing the
-         * same elements.
-         */
-        @NonNull
-        private String getFolderMigrationId() {
-            return mFolderItems.keySet().stream()
-                    .map(intentString -> mFolderItems.get(intentString).size()
-                            + cleanIntentString(intentString))
-                    .sorted()
-                    .collect(Collectors.joining(","));
-        }
-
-        /**
-         * This is needed because sourceBounds can change and make the id of two equal items
-         * different.
-         */
-        @NonNull
-        private String cleanIntentString(@NonNull String intentStr) {
-            try {
-                Intent i = Intent.parseUri(intentStr, 0);
-                i.setSourceBounds(null);
-                return i.toURI();
-            } catch (URISyntaxException e) {
-                Log.e(TAG, "Unable to parse Intent string", e);
-                return intentStr;
-            }
-
-        }
-    }
 }
diff --git a/src/com/android/launcher3/model/GridSizeMigrationLogic.kt b/src/com/android/launcher3/model/GridSizeMigrationLogic.kt
new file mode 100644
index 0000000..c856d4b
--- /dev/null
+++ b/src/com/android/launcher3/model/GridSizeMigrationLogic.kt
@@ -0,0 +1,525 @@
+/*
+ * 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
+
+import android.content.Context
+import android.database.sqlite.SQLiteDatabase
+import android.graphics.Point
+import android.util.Log
+import androidx.annotation.VisibleForTesting
+import com.android.launcher3.Flags
+import com.android.launcher3.LauncherPrefs
+import com.android.launcher3.LauncherPrefs.Companion.get
+import com.android.launcher3.LauncherPrefs.Companion.getPrefs
+import com.android.launcher3.LauncherSettings
+import com.android.launcher3.Utilities
+import com.android.launcher3.config.FeatureFlags
+import com.android.launcher3.model.GridSizeMigrationDBController.DbReader
+import com.android.launcher3.provider.LauncherDbUtils
+import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction
+import com.android.launcher3.util.CellAndSpan
+import com.android.launcher3.util.GridOccupancy
+import com.android.launcher3.util.IntArray
+
+class GridSizeMigrationLogic {
+    /**
+     * Migrates the grid size from srcDeviceState to destDeviceState and make those changes in the
+     * target DB, using the source DB to determine what to add/remove/move/resize in the destination
+     * DB.
+     */
+    fun migrateGrid(
+        context: Context,
+        srcDeviceState: DeviceGridState,
+        destDeviceState: DeviceGridState,
+        target: DatabaseHelper,
+        source: SQLiteDatabase,
+        isDestNewDb: Boolean,
+    ) {
+        if (!GridSizeMigrationDBController.needsToMigrate(srcDeviceState, destDeviceState)) {
+            return
+        }
+
+        val isFirstLoad = get(context).get(LauncherPrefs.IS_FIRST_LOAD_AFTER_RESTORE)
+        Log.d(TAG, "Begin grid migration. First load: $isFirstLoad")
+
+        // This is a special case where if the grid is the same amount of columns but a larger
+        // amount of rows we simply copy over the source grid to the destination grid, rather
+        // than undergoing the general grid migration.
+        if (shouldMigrateToStrictlyTallerGrid(isDestNewDb, srcDeviceState, destDeviceState)) {
+            GridSizeMigrationDBController.copyCurrentGridToNewGrid(
+                context,
+                destDeviceState,
+                target,
+                source,
+            )
+            return
+        }
+        LauncherDbUtils.copyTable(
+            source,
+            LauncherSettings.Favorites.TABLE_NAME,
+            target.writableDatabase,
+            LauncherSettings.Favorites.TMP_TABLE,
+            context,
+        )
+
+        val migrationStartTime = System.currentTimeMillis()
+        try {
+            SQLiteTransaction(target.writableDatabase).use { t ->
+                val srcReader = DbReader(t.db, LauncherSettings.Favorites.TMP_TABLE, context)
+                val destReader = DbReader(t.db, LauncherSettings.Favorites.TABLE_NAME, context)
+
+                val targetSize = Point(destDeviceState.columns, destDeviceState.rows)
+
+                // Here we keep all the DB ids we have in the destination DB such that we don't
+                // assign
+                // an item that we want to add to the destination DB the same id as an already
+                // existing
+                // item.
+                val idsInUse = mutableListOf<Int>()
+
+                // Migrate hotseat.
+                migrateHotseat(destDeviceState.numHotseat, srcReader, destReader, target, idsInUse)
+                // Migrate workspace.
+                migrateWorkspace(srcReader, destReader, target, targetSize, idsInUse)
+
+                LauncherDbUtils.dropTable(t.db, LauncherSettings.Favorites.TMP_TABLE)
+                t.commit()
+            }
+        } catch (e: Exception) {
+            Log.e(TAG, "Error during grid migration", e)
+        } finally {
+            Log.v(
+                TAG,
+                "Workspace migration completed in " +
+                    (System.currentTimeMillis() - migrationStartTime),
+            )
+
+            // Save current configuration, so that the migration does not run again.
+            destDeviceState.writeToPrefs(context)
+        }
+    }
+
+    /** Handles hotseat migration. */
+    @VisibleForTesting
+    fun migrateHotseat(
+        destHotseatSize: Int,
+        srcReader: DbReader,
+        destReader: DbReader,
+        helper: DatabaseHelper,
+        idsInUse: MutableList<Int>,
+    ) {
+        val srcHotseatItems = srcReader.loadHotseatEntries()
+        val dstHotseatItems = destReader.loadHotseatEntries()
+
+        val hotseatToBeAdded = getItemsToBeAdded(srcHotseatItems, dstHotseatItems)
+        val toBeRemoved = IntArray()
+        toBeRemoved.addAll(getItemsToBeRemoved(srcHotseatItems, dstHotseatItems))
+
+        if (DEBUG) {
+            Log.d(
+                TAG,
+                """Start hotseat migration:
+            |Removing Hotseat Items: [${dstHotseatItems.filter { toBeRemoved.contains(it.id) }
+                .joinToString(",\n") { it.toString() }}]
+            |Adding Hotseat Items: [${hotseatToBeAdded
+                .joinToString(",\n") { it.toString() }}]
+            |"""
+                    .trimMargin(),
+            )
+        }
+
+        // Removes the items that we need to remove from the destination DB.
+        if (!toBeRemoved.isEmpty) {
+            GridSizeMigrationDBController.removeEntryFromDb(
+                destReader.mDb,
+                destReader.mTableName,
+                toBeRemoved,
+            )
+        }
+
+        placeHotseatItems(
+            hotseatToBeAdded,
+            dstHotseatItems,
+            destHotseatSize,
+            helper,
+            srcReader,
+            destReader,
+            idsInUse,
+        )
+    }
+
+    private fun placeHotseatItems(
+        hotseatToBeAdded: MutableList<DbEntry>,
+        dstHotseatItems: List<DbEntry>,
+        destHotseatSize: Int,
+        helper: DatabaseHelper,
+        srcReader: DbReader,
+        destReader: DbReader,
+        idsInUse: MutableList<Int>,
+    ) {
+        if (hotseatToBeAdded.isEmpty()) {
+            return
+        }
+
+        idsInUse.addAll(dstHotseatItems.map { entry: DbEntry -> entry.id })
+
+        hotseatToBeAdded.sort()
+
+        val placementSolutionHotseat =
+            solveHotseatPlacement(destHotseatSize, dstHotseatItems, hotseatToBeAdded)
+        for (entryToPlace in placementSolutionHotseat) {
+            GridSizeMigrationDBController.insertEntryInDb(
+                helper,
+                entryToPlace,
+                srcReader.mTableName,
+                destReader.mTableName,
+                idsInUse,
+            )
+        }
+    }
+
+    @VisibleForTesting
+    fun migrateWorkspace(
+        srcReader: DbReader,
+        destReader: DbReader,
+        helper: DatabaseHelper,
+        targetSize: Point,
+        idsInUse: MutableList<Int>,
+    ) {
+        val srcWorkspaceItems = srcReader.loadAllWorkspaceEntries()
+
+        val dstWorkspaceItems = destReader.loadAllWorkspaceEntries()
+
+        val toBeRemoved = IntArray()
+
+        val workspaceToBeAdded = getItemsToBeAdded(srcWorkspaceItems, dstWorkspaceItems)
+        toBeRemoved.addAll(getItemsToBeRemoved(srcWorkspaceItems, dstWorkspaceItems))
+
+        if (DEBUG) {
+            Log.d(
+                TAG,
+                """Start workspace migration:
+            |Source Device: [${srcWorkspaceItems.joinToString(",\n") { it.toString() }}]
+            |Target Device: [${dstWorkspaceItems.joinToString(",\n") { it.toString() }}]
+            |Removing Workspace Items: [${dstWorkspaceItems.filter { toBeRemoved.contains(it.id) }
+                .joinToString(",\n") { it.toString() }}]
+            |Adding Workspace Items: [${workspaceToBeAdded
+                .joinToString(",\n") { it.toString() }}]
+            |"""
+                    .trimMargin(),
+            )
+        }
+
+        // Removes the items that we need to remove from the destination DB.
+        if (!toBeRemoved.isEmpty) {
+            GridSizeMigrationDBController.removeEntryFromDb(
+                destReader.mDb,
+                destReader.mTableName,
+                toBeRemoved,
+            )
+        }
+
+        placeWorkspaceItems(
+            workspaceToBeAdded,
+            dstWorkspaceItems,
+            targetSize.x,
+            targetSize.y,
+            helper,
+            srcReader,
+            destReader,
+            idsInUse,
+        )
+    }
+
+    private fun placeWorkspaceItems(
+        workspaceToBeAdded: MutableList<DbEntry>,
+        dstWorkspaceItems: List<DbEntry>,
+        trgX: Int,
+        trgY: Int,
+        helper: DatabaseHelper,
+        srcReader: DbReader,
+        destReader: DbReader,
+        idsInUse: MutableList<Int>,
+    ) {
+        if (workspaceToBeAdded.isEmpty()) {
+            return
+        }
+
+        idsInUse.addAll(dstWorkspaceItems.map { entry: DbEntry -> entry.id })
+
+        workspaceToBeAdded.sort()
+
+        // First we create a collection of the screens
+        val screens: MutableList<Int> = ArrayList()
+        for (screenId in 0..destReader.mLastScreenId) {
+            screens.add(screenId)
+        }
+
+        // Then we place the items on the screens
+        var itemsToPlace = WorkspaceItemsToPlace(workspaceToBeAdded, mutableListOf())
+        for (screenId in screens) {
+            if (DEBUG) {
+                Log.d(TAG, "Migrating $screenId")
+            }
+            itemsToPlace =
+                solveGridPlacement(
+                    destReader.mContext,
+                    screenId,
+                    trgX,
+                    trgY,
+                    itemsToPlace.mRemainingItemsToPlace,
+                    destReader.mWorkspaceEntriesByScreenId[screenId],
+                )
+            placeItems(itemsToPlace, helper, srcReader, destReader, idsInUse)
+            while (itemsToPlace.mPlacementSolution.isNotEmpty()) {
+                GridSizeMigrationDBController.insertEntryInDb(
+                    helper,
+                    itemsToPlace.mPlacementSolution.removeAt(0),
+                    srcReader.mTableName,
+                    destReader.mTableName,
+                    idsInUse,
+                )
+            }
+            if (itemsToPlace.mRemainingItemsToPlace.isEmpty()) {
+                break
+            }
+        }
+
+        // In case the new grid is smaller, there might be some leftover items that don't fit on
+        // any of the screens, in this case we add them to new screens until all of them are placed.
+        var screenId = destReader.mLastScreenId + 1
+        while (itemsToPlace.mRemainingItemsToPlace.isNotEmpty()) {
+            itemsToPlace =
+                solveGridPlacement(
+                    destReader.mContext,
+                    screenId,
+                    trgX,
+                    trgY,
+                    itemsToPlace.mRemainingItemsToPlace,
+                    destReader.mWorkspaceEntriesByScreenId[screenId],
+                )
+            placeItems(itemsToPlace, helper, srcReader, destReader, idsInUse)
+            screenId++
+        }
+    }
+
+    private fun placeItems(
+        itemsToPlace: WorkspaceItemsToPlace,
+        helper: DatabaseHelper,
+        srcReader: DbReader,
+        destReader: DbReader,
+        idsInUse: List<Int>,
+    ) {
+        while (itemsToPlace.mPlacementSolution.isNotEmpty()) {
+            GridSizeMigrationDBController.insertEntryInDb(
+                helper,
+                itemsToPlace.mPlacementSolution.removeAt(0),
+                srcReader.mTableName,
+                destReader.mTableName,
+                idsInUse,
+            )
+        }
+    }
+
+    /** Only migrate the grid in this manner if the target grid is taller and not wider. */
+    private fun shouldMigrateToStrictlyTallerGrid(
+        isDestNewDb: Boolean,
+        srcDeviceState: DeviceGridState,
+        destDeviceState: DeviceGridState,
+    ): Boolean {
+        return isDestNewDb &&
+            srcDeviceState.columns == destDeviceState.columns &&
+            srcDeviceState.rows < destDeviceState.rows
+    }
+
+    /**
+     * Finds all the items that are in the old grid which aren't in the new grid, meaning they need
+     * to be added to the new grid.
+     *
+     * @return a list of DbEntry's which we need to add.
+     */
+    private fun getItemsToBeAdded(src: List<DbEntry>, dest: List<DbEntry>): MutableList<DbEntry> {
+        val entryCountDiff = calcDiff(src, dest)
+        val toBeAdded: MutableList<DbEntry> = ArrayList()
+        src.forEach { entry ->
+            entryCountDiff[entry]?.let { entryDiff ->
+                if (entryDiff > 0) {
+                    toBeAdded.add(entry)
+                    entryCountDiff[entry] = entryDiff - 1
+                }
+            }
+        }
+        return toBeAdded
+    }
+
+    /**
+     * Finds all the items that are in the new grid which aren't in the old grid, meaning they need
+     * to be removed from the new grid.
+     *
+     * @return an IntArray of item id's which we need to remove.
+     */
+    private fun getItemsToBeRemoved(src: List<DbEntry>, dest: List<DbEntry>): IntArray {
+        val entryCountDiff = calcDiff(src, dest)
+        val toBeRemoved =
+            IntArray().apply {
+                dest.forEach { entry ->
+                    entryCountDiff[entry]?.let { entryDiff ->
+                        if (entryDiff < 0) {
+                            add(entry.id)
+                            if (entry.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) {
+                                entry.mFolderItems.values.forEach { ids ->
+                                    ids.forEach { value -> add(value) }
+                                }
+                            }
+                        }
+                        entryCountDiff[entry] = entryDiff.plus(1)
+                    }
+                }
+            }
+        return toBeRemoved
+    }
+
+    /**
+     * Calculates the difference between the old and new grid items in terms of how many of each
+     * item there are. E.g. if the old grid had 2 Calculator icons but the new grid has 0, then the
+     * difference there would be 2. While if the old grid has 0 Calculator icons and the new grid
+     * has 1, then the difference would be -1.
+     *
+     * @return a Map with each DbEntry as a key and the count of said entry as the value.
+     */
+    private fun calcDiff(src: List<DbEntry>, dest: List<DbEntry>): MutableMap<DbEntry, Int> {
+        val entryCountDiff: MutableMap<DbEntry, Int> = HashMap()
+        src.forEach { entry -> entryCountDiff[entry] = entryCountDiff.getOrDefault(entry, 0) + 1 }
+        dest.forEach { entry -> entryCountDiff[entry] = entryCountDiff.getOrDefault(entry, 0) - 1 }
+        return entryCountDiff
+    }
+
+    private fun solveHotseatPlacement(
+        hotseatSize: Int,
+        placedHotseatItems: List<DbEntry>,
+        itemsToPlace: List<DbEntry>,
+    ): List<DbEntry> {
+        val placementSolution: MutableList<DbEntry> = ArrayList()
+        val remainingItemsToPlace: MutableList<DbEntry> = ArrayList(itemsToPlace)
+        val occupied = BooleanArray(hotseatSize)
+        for (entry in placedHotseatItems) {
+            occupied[entry.screenId] = true
+        }
+
+        for (i in occupied.indices) {
+            if (!occupied[i] && remainingItemsToPlace.isNotEmpty()) {
+                val entry: DbEntry =
+                    remainingItemsToPlace.removeAt(0).apply {
+                        screenId = i
+                        // These values does not affect the item position, but we should set them
+                        // to something other than -1.
+                        cellX = i
+                        cellY = 0
+                    }
+                placementSolution.add(entry)
+                occupied[entry.screenId] = true
+            }
+        }
+        return placementSolution
+    }
+
+    private fun solveGridPlacement(
+        context: Context,
+        screenId: Int,
+        trgX: Int,
+        trgY: Int,
+        sortedItemsToPlace: MutableList<DbEntry>,
+        existedEntries: MutableList<DbEntry>?,
+    ): WorkspaceItemsToPlace {
+        val itemsToPlace = WorkspaceItemsToPlace(sortedItemsToPlace, mutableListOf())
+        val occupied = GridOccupancy(trgX, trgY)
+        val trg = Point(trgX, trgY)
+        val next: Point =
+            if (
+                screenId == 0 &&
+                    (FeatureFlags.QSB_ON_FIRST_SCREEN &&
+                        (!Flags.enableSmartspaceRemovalToggle() ||
+                            getPrefs(context)
+                                .getBoolean(LoaderTask.SMARTSPACE_ON_HOME_SCREEN, true)) &&
+                        !Utilities.SHOULD_SHOW_FIRST_PAGE_WIDGET)
+            ) {
+                Point(0, 1 /* smartspace */)
+            } else {
+                Point(0, 0)
+            }
+        if (existedEntries != null) {
+            for (entry in existedEntries) {
+                occupied.markCells(entry, true)
+            }
+        }
+        val iterator = itemsToPlace.mRemainingItemsToPlace.iterator()
+        while (iterator.hasNext()) {
+            val entry = iterator.next()
+            if (entry.minSpanX > trgX || entry.minSpanY > trgY) {
+                iterator.remove()
+                continue
+            }
+            findPlacementForEntry(entry, next.x, next.y, trg, occupied)?.let {
+                entry.screenId = screenId
+                entry.cellX = it.cellX
+                entry.cellY = it.cellY
+                entry.spanX = it.spanX
+                entry.spanY = it.spanY
+                occupied.markCells(entry, true)
+                next[entry.cellX + entry.spanX] = entry.cellY
+                itemsToPlace.mPlacementSolution.add(entry)
+                iterator.remove()
+            }
+        }
+        return itemsToPlace
+    }
+
+    /**
+     * Search for the next possible placement of an item. (mNextStartX, mNextStartY) serves as a
+     * memoization of last placement, we can start our search for next placement from there to speed
+     * up the search.
+     *
+     * @return NewEntryPlacement object if we found a valid placement, null if we didn't.
+     */
+    private fun findPlacementForEntry(
+        entry: DbEntry,
+        startPosX: Int,
+        startPosY: Int,
+        trg: Point,
+        occupied: GridOccupancy,
+    ): CellAndSpan? {
+        var newStartPosX = startPosX
+        for (y in startPosY until trg.y) {
+            for (x in newStartPosX until trg.x) {
+                if (occupied.isRegionVacant(x, y, entry.minSpanX, entry.minSpanY)) {
+                    return (CellAndSpan(x, y, entry.minSpanX, entry.minSpanY))
+                }
+            }
+            newStartPosX = 0
+        }
+        return null
+    }
+
+    private data class WorkspaceItemsToPlace(
+        val mRemainingItemsToPlace: MutableList<DbEntry>,
+        val mPlacementSolution: MutableList<DbEntry>,
+    )
+
+    companion object {
+        private const val TAG = "GridSizeMigrationLogic"
+        private const val DEBUG = true
+    }
+}
diff --git a/src/com/android/launcher3/model/ItemInstallQueue.java b/src/com/android/launcher3/model/ItemInstallQueue.java
index 59d1d00..f9c6e96 100644
--- a/src/com/android/launcher3/model/ItemInstallQueue.java
+++ b/src/com/android/launcher3/model/ItemInstallQueue.java
@@ -45,16 +45,18 @@
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherSettings.Favorites;
+import com.android.launcher3.dagger.ApplicationContext;
+import com.android.launcher3.dagger.LauncherAppSingleton;
+import com.android.launcher3.dagger.LauncherBaseAppComponent;
 import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.shortcuts.ShortcutKey;
 import com.android.launcher3.shortcuts.ShortcutRequest;
-import com.android.launcher3.util.MainThreadInitializedObject;
+import com.android.launcher3.util.DaggerSingletonObject;
 import com.android.launcher3.util.PersistedItemArray;
 import com.android.launcher3.util.Preconditions;
-import com.android.launcher3.util.SafeCloseable;
 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 
 import java.util.HashSet;
@@ -62,10 +64,13 @@
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
+import javax.inject.Inject;
+
 /**
  * Class to maintain a queue of pending items to be added to the workspace.
  */
-public class ItemInstallQueue implements SafeCloseable {
+@LauncherAppSingleton
+public class ItemInstallQueue {
 
     private static final String LOG = "ItemInstallQueue";
 
@@ -81,9 +86,8 @@
     public static final int NEW_SHORTCUT_BOUNCE_DURATION = 450;
     public static final int NEW_SHORTCUT_STAGGER_DELAY = 85;
 
-    public static MainThreadInitializedObject<ItemInstallQueue> INSTANCE =
-            new MainThreadInitializedObject<>(ItemInstallQueue::new);
-
+    public static DaggerSingletonObject<ItemInstallQueue> INSTANCE =
+            new DaggerSingletonObject<>(LauncherBaseAppComponent::getItemInstallQueue);
     private final PersistedItemArray<PendingInstallShortcutInfo> mStorage =
             new PersistedItemArray<>(APPS_PENDING_INSTALL);
     private final Context mContext;
@@ -95,13 +99,11 @@
     // Only accessed on worker thread
     private List<PendingInstallShortcutInfo> mItems;
 
-    private ItemInstallQueue(Context context) {
+    @Inject
+    public ItemInstallQueue(@ApplicationContext Context context) {
         mContext = context;
     }
 
-    @Override
-    public void close() {}
-
     @WorkerThread
     private void ensureQueueLoaded() {
         Preconditions.assertWorkerThread();
@@ -121,7 +123,7 @@
 
     @WorkerThread
     private void flushQueueInBackground() {
-        Launcher launcher = Launcher.ACTIVITY_TRACKER.getCreatedActivity();
+        Launcher launcher = Launcher.ACTIVITY_TRACKER.getCreatedContext();
         if (launcher == null) {
             // Launcher not loaded
             return;
diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java
index 609846f..a830c96 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -20,9 +20,11 @@
 import static com.android.launcher3.Flags.enableLauncherBrMetricsFixed;
 import static com.android.launcher3.Flags.enableSmartspaceAsAWidget;
 import static com.android.launcher3.Flags.enableSmartspaceRemovalToggle;
+import static com.android.launcher3.Flags.enableTieredWidgetsByDefaultInPicker;
 import static com.android.launcher3.LauncherPrefs.IS_FIRST_LOAD_AFTER_RESTORE;
 import static com.android.launcher3.LauncherPrefs.SHOULD_SHOW_SMARTSPACE;
 import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME;
+import static com.android.launcher3.icons.CacheableShortcutInfo.convertShortcutsToCacheableShortcuts;
 import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_HAS_SHORTCUT_PERMISSION;
 import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_PRIVATE_PROFILE_QUIET_MODE_ENABLED;
 import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_QUIET_MODE_CHANGE_PERMISSION;
@@ -69,9 +71,10 @@
 import com.android.launcher3.folder.FolderGridOrganizer;
 import com.android.launcher3.folder.FolderNameInfos;
 import com.android.launcher3.folder.FolderNameProvider;
-import com.android.launcher3.icons.ComponentWithLabelAndIcon;
+import com.android.launcher3.icons.CacheableShortcutCachingLogic;
+import com.android.launcher3.icons.CacheableShortcutInfo;
 import com.android.launcher3.icons.IconCache;
-import com.android.launcher3.icons.ShortcutCachingLogic;
+import com.android.launcher3.icons.cache.CachedObject;
 import com.android.launcher3.icons.cache.CachedObjectCachingLogic;
 import com.android.launcher3.icons.cache.IconCacheUpdateHandler;
 import com.android.launcher3.icons.cache.LauncherActivityCachingLogic;
@@ -140,6 +143,7 @@
     private final UserManager mUserManager;
     private final UserCache mUserCache;
     private final PackageManagerHelper mPmHelper;
+    private final WidgetsFilterDataProvider mWidgetsFilterDataProvider;
 
     private final InstallSessionHelper mSessionHelper;
     private final IconCache mIconCache;
@@ -156,13 +160,16 @@
     private String mDbName;
 
     public LoaderTask(@NonNull LauncherAppState app, AllAppsList bgAllAppsList, BgDataModel bgModel,
-            ModelDelegate modelDelegate, @NonNull BaseLauncherBinder launcherBinder) {
-        this(app, bgAllAppsList, bgModel, modelDelegate, launcherBinder, new UserManagerState());
+            ModelDelegate modelDelegate, @NonNull BaseLauncherBinder launcherBinder,
+            @NonNull WidgetsFilterDataProvider widgetsFilterDataProvider) {
+        this(app, bgAllAppsList, bgModel, modelDelegate, launcherBinder, widgetsFilterDataProvider,
+                new UserManagerState());
     }
 
     @VisibleForTesting
     LoaderTask(@NonNull LauncherAppState app, AllAppsList bgAllAppsList, BgDataModel bgModel,
             ModelDelegate modelDelegate, @NonNull BaseLauncherBinder launcherBinder,
+            WidgetsFilterDataProvider widgetsFilterDataProvider,
             UserManagerState userManagerState) {
         mApp = app;
         mBgAllAppsList = bgAllAppsList;
@@ -177,6 +184,7 @@
         mIconCache = mApp.getIconCache();
         mUserManagerState = userManagerState;
         mInstallingPkgsCached = null;
+        mWidgetsFilterDataProvider = widgetsFilterDataProvider;
     }
 
     protected synchronized void waitForIdle() {
@@ -246,7 +254,7 @@
         }
         try (LauncherModel.LoaderTransaction transaction = mApp.getModel().beginLoader(this)) {
 
-            List<ShortcutInfo> allShortcuts = new ArrayList<>();
+            List<CacheableShortcutInfo> allShortcuts = new ArrayList<>();
             loadWorkspace(allShortcuts, "", memoryLogger, restoreEventLogger);
 
             // Sanitize data re-syncs widgets/shortcuts based on the workspace loaded from db.
@@ -285,11 +293,6 @@
             }
             logASplit("loadAllApps");
 
-            if (FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) {
-                mModelDelegate.loadAndBindAllAppsItems(mUserManagerState,
-                        mLauncherBinder.mCallbacksList, mShortcutKeyToPinnedShortcuts);
-                logASplit("allAppsDelegateItems");
-            }
             verifyNotStopped();
             mLauncherBinder.bindAllApps();
             logASplit("bindAllApps");
@@ -304,7 +307,7 @@
 
             verifyNotStopped();
             logASplit("save shortcuts in icon cache");
-            updateHandler.updateIcons(allShortcuts, new ShortcutCachingLogic(),
+            updateHandler.updateIcons(allShortcuts, CacheableShortcutCachingLogic.INSTANCE,
                     mApp.getModel()::onPackageIconsUpdated);
 
             // Take a break
@@ -322,8 +325,10 @@
 
             verifyNotStopped();
             logASplit("save deep shortcuts in icon cache");
-            updateHandler.updateIcons(allDeepShortcuts,
-                    new ShortcutCachingLogic(), (pkgs, user) -> { });
+            updateHandler.updateIcons(
+                    convertShortcutsToCacheableShortcuts(allDeepShortcuts, allActivityList),
+                    CacheableShortcutCachingLogic.INSTANCE,
+                    (pkgs, user) -> { });
 
             // Take a break
             waitForIdle();
@@ -331,8 +336,15 @@
             verifyNotStopped();
 
             // fourth step
-            List<ComponentWithLabelAndIcon> allWidgetsList =
-                    mBgDataModel.widgetsModel.update(mApp, null);
+            WidgetsModel widgetsModel = mBgDataModel.widgetsModel;
+            if (enableTieredWidgetsByDefaultInPicker()) {
+                // Begin periodic refresh of filters
+                mWidgetsFilterDataProvider.initPeriodicDataRefresh(
+                        mApp.getModel()::onWidgetFiltersLoaded);
+                // And, update model with currently cached data.
+                widgetsModel.updateWidgetFilters(mWidgetsFilterDataProvider);
+            }
+            List<CachedObject> allWidgetsList = widgetsModel.update(mApp, /*packageUser=*/null);
             logASplit("load widgets");
 
             verifyNotStopped();
@@ -353,14 +365,8 @@
                 prefs.putSync(SHOULD_SHOW_SMARTSPACE.to(true));
             }
 
-            if (FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) {
-                mModelDelegate.loadAndBindOtherItems(mLauncherBinder.mCallbacksList);
-                logASplit("otherDelegateItems");
-                verifyNotStopped();
-            }
-
             updateHandler.updateIcons(allWidgetsList,
-                    new CachedObjectCachingLogic(mApp.getContext()),
+                    CachedObjectCachingLogic.INSTANCE,
                     mApp.getModel()::onWidgetLabelsUpdated);
             logASplit("save widgets in icon cache");
 
@@ -397,7 +403,7 @@
     }
 
     protected void loadWorkspace(
-            List<ShortcutInfo> allDeepShortcuts,
+            List<CacheableShortcutInfo> allDeepShortcuts,
             String selection,
             LoaderMemoryLogger memoryLogger,
             @Nullable LauncherRestoreEventLogger restoreEventLogger
@@ -410,20 +416,13 @@
         }
         logASplit("loadWorkspace");
 
-        if (FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) {
-            verifyNotStopped();
-            mModelDelegate.loadAndBindWorkspaceItems(mUserManagerState,
-                    mLauncherBinder.mCallbacksList, mShortcutKeyToPinnedShortcuts);
-            mModelDelegate.markActive();
-            logASplit("workspaceDelegateItems");
-        }
         mBgDataModel.isFirstPagePinnedItemEnabled = FeatureFlags.QSB_ON_FIRST_SCREEN
                 && (!enableSmartspaceRemovalToggle() || LauncherPrefs.getPrefs(
                 mApp.getContext()).getBoolean(SMARTSPACE_ON_HOME_SCREEN, true));
     }
 
     private void loadWorkspaceImpl(
-            List<ShortcutInfo> allDeepShortcuts,
+            List<CacheableShortcutInfo> allDeepShortcuts,
             String selection,
             @Nullable LoaderMemoryLogger memoryLogger,
             @Nullable LauncherRestoreEventLogger restoreEventLogger) {
@@ -432,7 +431,15 @@
         final WidgetInflater widgetInflater = new WidgetInflater(context);
 
         ModelDbController dbController = mApp.getModel().getModelDbController();
-        dbController.tryMigrateDB(restoreEventLogger);
+        if (Flags.gridMigrationRefactor()) {
+            try {
+                dbController.attemptMigrateDb(restoreEventLogger);
+            } catch (Exception e) {
+                FileLog.e(TAG, "Failed to migrate grid", e);
+            }
+        } else {
+            dbController.tryMigrateDB(restoreEventLogger);
+        }
         Log.d(TAG, "loadWorkspace: loading default favorites");
         dbController.loadDefaultFavoritesIfNecessary();
 
@@ -479,14 +486,12 @@
                 IOUtils.closeSilently(c);
             }
 
-            if (!FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) {
-                mModelDelegate.loadAndBindWorkspaceItems(mUserManagerState,
-                        mLauncherBinder.mCallbacksList, mShortcutKeyToPinnedShortcuts);
-                mModelDelegate.loadAndBindAllAppsItems(mUserManagerState,
-                        mLauncherBinder.mCallbacksList, mShortcutKeyToPinnedShortcuts);
-                mModelDelegate.loadAndBindOtherItems(mLauncherBinder.mCallbacksList);
-                mModelDelegate.markActive();
-            }
+            mModelDelegate.loadAndBindWorkspaceItems(mUserManagerState,
+                    mLauncherBinder.mCallbacksList, mShortcutKeyToPinnedShortcuts);
+            mModelDelegate.loadAndBindAllAppsItems(mUserManagerState,
+                    mLauncherBinder.mCallbacksList, mShortcutKeyToPinnedShortcuts);
+            mModelDelegate.loadAndBindOtherItems(mLauncherBinder.mCallbacksList);
+            mModelDelegate.markActive();
 
             // Break early if we've stopped loading
             if (mStopped) {
diff --git a/src/com/android/launcher3/model/ModelDbController.java b/src/com/android/launcher3/model/ModelDbController.java
index da1a221..6ff8547 100644
--- a/src/com/android/launcher3/model/ModelDbController.java
+++ b/src/com/android/launcher3/model/ModelDbController.java
@@ -20,14 +20,17 @@
 import static android.util.Base64.NO_WRAP;
 
 import static com.android.launcher3.DefaultLayoutParser.RES_PARTNER_DEFAULT_LAYOUT;
+import static com.android.launcher3.LauncherPrefs.DB_FILE;
+import static com.android.launcher3.LauncherPrefs.NO_DB_FILES_RESTORED;
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER;
 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE;
 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR;
 import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME;
 import static com.android.launcher3.LauncherSettings.Favorites.addTableToDb;
-import static com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_KEY;
+import static com.android.launcher3.LauncherSettings.Settings.BLOB_KEY_PREFIX;
 import static com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_LABEL;
 import static com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_TAG;
+import static com.android.launcher3.LauncherSettings.Settings.LAYOUT_PROVIDER_KEY;
 import static com.android.launcher3.provider.LauncherDbUtils.tableExists;
 
 import android.app.blob.BlobHandle;
@@ -86,6 +89,7 @@
 import java.io.File;
 import java.io.InputStream;
 import java.io.StringReader;
+import java.util.List;
 
 /**
  * Utility class which maintains an instance of Launcher database and provides utility methods
@@ -96,6 +100,7 @@
 
     private static final String EMPTY_DATABASE_CREATED = "EMPTY_DATABASE_CREATED";
     public static final String EXTRA_DB_NAME = "db_name";
+    public static final String DATA_TYPE_DB_FILE = "database_file";
 
     protected DatabaseHelper mOpenHelper;
 
@@ -125,16 +130,20 @@
 
     private synchronized void createDbIfNotExists() {
         if (mOpenHelper == null) {
-            mOpenHelper = createDatabaseHelper(false /* forMigration */);
+            String dbFile = LauncherPrefs.get(mContext).get(DB_FILE);
+            if (dbFile.isEmpty()) {
+                dbFile = InvariantDeviceProfile.INSTANCE.get(mContext).dbFile;
+            }
+            mOpenHelper = createDatabaseHelper(false /* forMigration */, dbFile);
             printDBs("before: ");
             RestoreDbTask.restoreIfNeeded(mContext, this);
             printDBs("after: ");
         }
     }
 
-    protected DatabaseHelper createDatabaseHelper(boolean forMigration) {
+    protected DatabaseHelper createDatabaseHelper(boolean forMigration, String dbFile) {
         boolean isSandbox = mContext instanceof SandboxContext;
-        String dbName = isSandbox ? null : InvariantDeviceProfile.INSTANCE.get(mContext).dbFile;
+        String dbName = isSandbox ? null : dbFile;
 
         // Set the flag for empty DB
         Runnable onEmptyDbCreateCallback = forMigration ? () -> { }
@@ -288,13 +297,123 @@
 
 
     /**
+     * Resets the launcher DB if we should reset it.
+     */
+    public void resetLauncherDb(@Nullable LauncherRestoreEventLogger restoreEventLogger) {
+        if (restoreEventLogger != null) {
+            sendMetricsForFailedMigration(restoreEventLogger, getDb());
+        }
+        FileLog.d(TAG, "Migration failed: resetting launcher database");
+        createEmptyDB();
+        LauncherPrefs.get(mContext).putSync(
+                getEmptyDbCreatedKey(mOpenHelper.getDatabaseName()).to(true));
+
+        // Write the grid state to avoid another migration
+        new DeviceGridState(LauncherAppState.getIDP(mContext)).writeToPrefs(mContext);
+    }
+
+    /**
+     * Determines if we should reset the DB.
+     */
+    private boolean shouldResetDb() {
+        if (isThereExistingDb()) {
+            return true;
+        }
+        if (!isGridMigrationNecessary()) {
+            return false;
+        }
+        if (isCurrentDbSameAsTarget()) {
+            return true;
+        }
+        return false;
+    }
+
+    private boolean isThereExistingDb() {
+        if (LauncherPrefs.get(mContext).get(getEmptyDbCreatedKey())) {
+            // If we already have a new DB, ignore migration
+            FileLog.d(TAG, "migrateGridIfNeeded: new DB already created, skipping migration");
+            return true;
+        }
+        return false;
+    }
+
+    private boolean isGridMigrationNecessary() {
+        InvariantDeviceProfile idp = LauncherAppState.getIDP(mContext);
+        if (GridSizeMigrationDBController.needsToMigrate(mContext, idp)) {
+            return true;
+        }
+        FileLog.d(TAG, "migrateGridIfNeeded: no grid migration needed");
+        return false;
+    }
+
+    private boolean isCurrentDbSameAsTarget() {
+        InvariantDeviceProfile idp = LauncherAppState.getIDP(mContext);
+        String targetDbName = new DeviceGridState(idp).getDbFile();
+        if (TextUtils.equals(targetDbName, mOpenHelper.getDatabaseName())) {
+            FileLog.e(TAG, "migrateGridIfNeeded: target db is same as current: " + targetDbName);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Migrates the DB. If the migration failed, it clears the DB.
+     */
+    public void attemptMigrateDb(LauncherRestoreEventLogger restoreEventLogger) throws Exception {
+        createDbIfNotExists();
+
+        if (shouldResetDb()) {
+            resetLauncherDb(restoreEventLogger);
+            return;
+        }
+
+        InvariantDeviceProfile idp = LauncherAppState.getIDP(mContext);
+        DatabaseHelper oldHelper = mOpenHelper;
+
+        // We save the existing db's before creating the destination db helper so we know what logic
+        // to run in grid migration based on if that grid already existed before migration or not.
+        List<String> existingDBs = LauncherFiles.GRID_DB_FILES.stream()
+                .filter(dbName -> mContext.getDatabasePath(dbName).exists())
+                .toList();
+
+        mOpenHelper = (mContext instanceof SandboxContext) ? oldHelper
+                : createDatabaseHelper(true, new DeviceGridState(idp).getDbFile());
+        try {
+            // This is the current grid we have, given by the mContext
+            DeviceGridState srcDeviceState = new DeviceGridState(mContext);
+            // This is the state we want to migrate to that is given by the idp
+            DeviceGridState destDeviceState = new DeviceGridState(idp);
+
+            boolean isDestNewDb = !existingDBs.contains(destDeviceState.getDbFile());
+
+            GridSizeMigrationLogic gridSizeMigrationLogic = new GridSizeMigrationLogic();
+            gridSizeMigrationLogic.migrateGrid(mContext, srcDeviceState, destDeviceState,
+                    mOpenHelper, oldHelper.getWritableDatabase(), isDestNewDb);
+        } catch (Exception e) {
+            resetLauncherDb(restoreEventLogger);
+            throw new Exception("Failed to migrate grid", e);
+        } finally {
+            if (mOpenHelper != oldHelper) {
+                oldHelper.close();
+            }
+        }
+    }
+
+    /**
      * Migrates the DB if needed. If the migration failed, it clears the DB.
      */
     public void tryMigrateDB(@Nullable LauncherRestoreEventLogger restoreEventLogger) {
-
         if (!migrateGridIfNeeded()) {
             if (restoreEventLogger != null) {
-                sendMetricsForFailedMigration(restoreEventLogger, getDb());
+                if (LauncherPrefs.get(mContext).get(NO_DB_FILES_RESTORED)) {
+                    restoreEventLogger.logLauncherItemsRestoreFailed(DATA_TYPE_DB_FILE, 1,
+                            RestoreError.DATABASE_FILE_NOT_RESTORED);
+                    LauncherPrefs.get(mContext).put(NO_DB_FILES_RESTORED, false);
+                    FileLog.d(TAG, "There is no data to migrate: resetting launcher database");
+                } else {
+                    restoreEventLogger.logLauncherItemsRestored(DATA_TYPE_DB_FILE, 1);
+                    sendMetricsForFailedMigration(restoreEventLogger, getDb());
+                }
             }
             FileLog.d(TAG, "Migration failed: resetting launcher database");
             createEmptyDB();
@@ -303,6 +422,8 @@
 
             // Write the grid state to avoid another migration
             new DeviceGridState(LauncherAppState.getIDP(mContext)).writeToPrefs(mContext);
+        } else if (restoreEventLogger != null) {
+            restoreEventLogger.logLauncherItemsRestored(DATA_TYPE_DB_FILE, 1);
         }
     }
 
@@ -316,29 +437,39 @@
         createDbIfNotExists();
         if (LauncherPrefs.get(mContext).get(getEmptyDbCreatedKey())) {
             // If we have already create a new DB, ignore migration
-            Log.d(TAG, "migrateGridIfNeeded: new DB already created, skipping migration");
+            FileLog.d(TAG, "migrateGridIfNeeded: new DB already created, skipping migration");
             return false;
         }
         InvariantDeviceProfile idp = LauncherAppState.getIDP(mContext);
-        if (!GridSizeMigrationUtil.needsToMigrate(mContext, idp)) {
-            Log.d(TAG, "migrateGridIfNeeded: no grid migration needed");
+        if (!GridSizeMigrationDBController.needsToMigrate(mContext, idp)) {
+            FileLog.d(TAG, "migrateGridIfNeeded: no grid migration needed");
             return true;
         }
         String targetDbName = new DeviceGridState(idp).getDbFile();
         if (TextUtils.equals(targetDbName, mOpenHelper.getDatabaseName())) {
-            Log.e(TAG, "migrateGridIfNeeded: target db is same as current: " + targetDbName);
+            FileLog.e(TAG, "migrateGridIfNeeded: target db is same as current: " + targetDbName);
             return false;
         }
         DatabaseHelper oldHelper = mOpenHelper;
+
+        // We save the existing db's before creating the destination db helper so we know what logic
+        // to run in grid migration based on if that grid already existed before migration or not.
+        List<String> existingDBs = LauncherFiles.GRID_DB_FILES.stream()
+                .filter(dbName -> mContext.getDatabasePath(dbName).exists())
+                .toList();
+
         mOpenHelper = (mContext instanceof SandboxContext) ? oldHelper
-                : createDatabaseHelper(true /* forMigration */);
+                : createDatabaseHelper(true /* forMigration */, targetDbName);
         try {
             // This is the current grid we have, given by the mContext
             DeviceGridState srcDeviceState = new DeviceGridState(mContext);
             // This is the state we want to migrate to that is given by the idp
             DeviceGridState destDeviceState = new DeviceGridState(idp);
-            return GridSizeMigrationUtil.migrateGridIfNeeded(mContext, srcDeviceState,
-                    destDeviceState, mOpenHelper, oldHelper.getWritableDatabase());
+
+            boolean isDestNewDb = !existingDBs.contains(destDeviceState.getDbFile());
+
+            return GridSizeMigrationDBController.migrateGridIfNeeded(mContext, srcDeviceState,
+                    destDeviceState, mOpenHelper, oldHelper.getWritableDatabase(), isDestNewDb);
         } catch (Exception e) {
             FileLog.e(TAG, "Failed to migrate grid", e);
             return false;
@@ -548,9 +679,15 @@
     private AutoInstallsLayout createWorkspaceLoaderFromAppRestriction(
             LauncherWidgetHolder widgetHolder) {
         ContentResolver cr = mContext.getContentResolver();
-        String blobHandlerDigest = Settings.Secure.getString(cr, LAYOUT_DIGEST_KEY);
-        if (!TextUtils.isEmpty(blobHandlerDigest)) {
+        String systemLayoutProvider = Settings.Secure.getString(cr, LAYOUT_PROVIDER_KEY);
+        if (TextUtils.isEmpty(systemLayoutProvider)) {
+            return null;
+        }
+
+        // Try the blob store first
+        if (systemLayoutProvider.startsWith(BLOB_KEY_PREFIX)) {
             BlobStoreManager blobManager = mContext.getSystemService(BlobStoreManager.class);
+            String blobHandlerDigest = systemLayoutProvider.substring(BLOB_KEY_PREFIX.length());
             try (InputStream in = new ParcelFileDescriptor.AutoCloseInputStream(
                     blobManager.openBlob(BlobHandle.createWithSha256(
                             Base64.decode(blobHandlerDigest, NO_WRAP | NO_PADDING),
@@ -562,25 +699,21 @@
             }
         }
 
-        String authority = Settings.Secure.getString(cr, "launcher3.layout.provider");
-        if (TextUtils.isEmpty(authority)) {
-            return null;
-        }
-
+        // Try contentProvider based provider
         PackageManager pm = mContext.getPackageManager();
-        ProviderInfo pi = pm.resolveContentProvider(authority, 0);
+        ProviderInfo pi = pm.resolveContentProvider(systemLayoutProvider, 0);
         if (pi == null) {
-            Log.e(TAG, "No provider found for authority " + authority);
+            Log.e(TAG, "No provider found for authority " + systemLayoutProvider);
             return null;
         }
-        Uri uri = getLayoutUri(authority, mContext);
+        Uri uri = getLayoutUri(systemLayoutProvider, mContext);
         try (InputStream in = cr.openInputStream(uri)) {
-            Log.d(TAG, "Loading layout from " + authority);
+            Log.d(TAG, "Loading layout from " + systemLayoutProvider);
 
             Resources res = pm.getResourcesForApplication(pi.applicationInfo);
             return getAutoInstallsLayoutFromIS(in, widgetHolder, SourceResources.wrap(res));
         } catch (Exception e) {
-            Log.e(TAG, "Error getting layout stream from: " + authority , e);
+            Log.e(TAG, "Error getting layout stream from: " + systemLayoutProvider , e);
             return null;
         }
     }
diff --git a/src/com/android/launcher3/model/ModelLauncherCallbacks.kt b/src/com/android/launcher3/model/ModelLauncherCallbacks.kt
index 2ee5b80..ba4908c 100644
--- a/src/com/android/launcher3/model/ModelLauncherCallbacks.kt
+++ b/src/com/android/launcher3/model/ModelLauncherCallbacks.kt
@@ -16,11 +16,15 @@
 
 package com.android.launcher3.model
 
+import android.annotation.SuppressLint
 import android.content.pm.LauncherApps
+import android.content.pm.LauncherUserInfo
+import android.content.pm.PackageInstaller.SessionInfo
 import android.content.pm.ShortcutInfo
 import android.os.UserHandle
 import android.text.TextUtils
 import com.android.launcher3.LauncherModel.ModelUpdateTask
+import com.android.launcher3.config.FeatureFlags
 import com.android.launcher3.logging.FileLog
 import com.android.launcher3.model.PackageUpdatedTask.OP_ADD
 import com.android.launcher3.model.PackageUpdatedTask.OP_REMOVE
@@ -28,6 +32,10 @@
 import com.android.launcher3.model.PackageUpdatedTask.OP_UNAVAILABLE
 import com.android.launcher3.model.PackageUpdatedTask.OP_UNSUSPEND
 import com.android.launcher3.model.PackageUpdatedTask.OP_UPDATE
+import com.android.launcher3.pm.InstallSessionTracker
+import com.android.launcher3.pm.PackageInstallInfo
+import com.android.launcher3.pm.UserCache
+import com.android.launcher3.util.PackageUserKey
 import java.util.function.Consumer
 
 /**
@@ -35,7 +43,7 @@
  * model tasks
  */
 class ModelLauncherCallbacks(private var taskExecutor: Consumer<ModelUpdateTask>) :
-    LauncherApps.Callback() {
+    LauncherApps.Callback(), InstallSessionTracker.Callback {
 
     override fun onPackageAdded(packageName: String, user: UserHandle) {
         FileLog.d(TAG, "onPackageAdded triggered for packageName=$packageName, user=$user")
@@ -49,7 +57,7 @@
     override fun onPackageLoadingProgressChanged(
         packageName: String,
         user: UserHandle,
-        progress: Float
+        progress: Float,
     ) {
         taskExecutor.accept(PackageIncrementalDownloadUpdatedTask(packageName, user, progress))
     }
@@ -62,7 +70,7 @@
     override fun onPackagesAvailable(
         vararg packageNames: String,
         user: UserHandle,
-        replacing: Boolean
+        replacing: Boolean,
     ) {
         taskExecutor.accept(PackageUpdatedTask(OP_UPDATE, user, *packageNames))
     }
@@ -74,7 +82,7 @@
     override fun onPackagesUnavailable(
         packageNames: Array<String>,
         user: UserHandle,
-        replacing: Boolean
+        replacing: Boolean,
     ) {
         if (!replacing) {
             taskExecutor.accept(PackageUpdatedTask(OP_UNAVAILABLE, user, *packageNames))
@@ -88,7 +96,7 @@
     override fun onShortcutsChanged(
         packageName: String,
         shortcuts: MutableList<ShortcutInfo>,
-        user: UserHandle
+        user: UserHandle,
     ) {
         taskExecutor.accept(ShortcutsChangedTask(packageName, shortcuts, user, true))
     }
@@ -98,6 +106,48 @@
         taskExecutor.accept(PackageUpdatedTask(OP_REMOVE, user, *packages.toTypedArray()))
     }
 
+    override fun onSessionFailure(packageName: String, user: UserHandle) {
+        taskExecutor.accept(SessionFailureTask(packageName, user))
+    }
+
+    override fun onPackageStateChanged(installInfo: PackageInstallInfo) {
+        taskExecutor.accept(PackageInstallStateChangedTask(installInfo))
+    }
+
+    override fun onUpdateSessionDisplay(key: PackageUserKey, info: SessionInfo) {
+        /** Updates the icons and label of all pending icons for the provided package name. */
+        taskExecutor.accept { controller, _, _ ->
+            controller.app.iconCache.updateSessionCache(key, info)
+        }
+        taskExecutor.accept(
+            CacheDataUpdatedTask(
+                CacheDataUpdatedTask.OP_SESSION_UPDATE,
+                key.mUser,
+                hashSetOf(key.mPackageName),
+            )
+        )
+    }
+
+    override fun onInstallSessionCreated(sessionInfo: PackageInstallInfo) {
+        if (FeatureFlags.PROMISE_APPS_IN_ALL_APPS.get()) {
+            taskExecutor.accept { taskController, _, apps ->
+                apps.addPromiseApp(taskController.app.context, sessionInfo)
+                taskController.bindApplicationsIfNeeded()
+            }
+        }
+    }
+
+    @SuppressLint("NewApi")
+    override fun onUserConfigChanged(launcherUserInfo: LauncherUserInfo) {
+        FileLog.d(TAG, "onUserConfigChanged for user ${launcherUserInfo.userType}")
+        if (android.multiuser.Flags.addLauncherUserConfig()) {
+            taskExecutor.accept { taskController, _, _ ->
+                UserCache.INSTANCE.get(taskController.app.context).updateCache()
+                taskController.app.model.forceReload()
+            }
+        }
+    }
+
     companion object {
         private const val TAG = "LauncherAppsCallbackImpl"
     }
diff --git a/src/com/android/launcher3/model/ModelTaskController.kt b/src/com/android/launcher3/model/ModelTaskController.kt
index cf2cadc..fc53343 100644
--- a/src/com/android/launcher3/model/ModelTaskController.kt
+++ b/src/com/android/launcher3/model/ModelTaskController.kt
@@ -35,7 +35,7 @@
     val dataModel: BgDataModel,
     val allAppsList: AllAppsList,
     private val model: LauncherModel,
-    private val uiExecutor: Executor
+    private val uiExecutor: Executor,
 ) {
 
     /** Schedules a {@param task} to be executed on the current callbacks. */
@@ -79,10 +79,19 @@
     }
 
     fun bindUpdatedWidgets(dataModel: BgDataModel) {
-        val widgets =
-            WidgetsListBaseEntriesBuilder(app.context)
-                .build(dataModel.widgetsModel.widgetsByPackageItem)
-        scheduleCallbackTask { it.bindAllWidgets(widgets) }
+        val widgetsByPackageItem = dataModel.widgetsModel.widgetsByPackageItem
+        val allWidgets = WidgetsListBaseEntriesBuilder(app.context).build(widgetsByPackageItem)
+
+        val defaultWidgetsFilter = dataModel.widgetsModel.defaultWidgetsFilter
+        val defaultWidgets =
+            if (defaultWidgetsFilter != null) {
+                WidgetsListBaseEntriesBuilder(app.context)
+                    .build(widgetsByPackageItem, defaultWidgetsFilter)
+            } else {
+                emptyList()
+            }
+
+        scheduleCallbackTask { it.bindAllWidgets(allWidgets, defaultWidgets) }
     }
 
     fun deleteAndBindComponentsRemoved(matcher: Predicate<ItemInfo?>, reason: String?) {
@@ -99,7 +108,7 @@
             val packageUserKeyToUidMap =
                 apps.associateBy(
                     keySelector = { PackageUserKey(it.componentName!!.packageName, it.user) },
-                    valueTransform = { it.uid }
+                    valueTransform = { it.uid },
                 )
             scheduleCallbackTask { it.bindAllApplications(apps, flags, packageUserKeyToUidMap) }
         }
diff --git a/src/com/android/launcher3/model/SdCardAvailableReceiver.java b/src/com/android/launcher3/model/SdCardAvailableReceiver.java
index 5293316..9e3f0e1 100644
--- a/src/com/android/launcher3/model/SdCardAvailableReceiver.java
+++ b/src/com/android/launcher3/model/SdCardAvailableReceiver.java
@@ -24,7 +24,7 @@
 
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherModel;
-import com.android.launcher3.util.PackageManagerHelper;
+import com.android.launcher3.util.ApplicationInfoWrapper;
 import com.android.launcher3.util.PackageUserKey;
 
 import java.util.ArrayList;
@@ -52,7 +52,6 @@
     @Override
     public void onReceive(Context context, Intent intent) {
         final LauncherApps launcherApps = context.getSystemService(LauncherApps.class);
-        final PackageManagerHelper pmHelper = PackageManagerHelper.INSTANCE.get(context);
         for (PackageUserKey puk : mPackages) {
             UserHandle user = puk.mUser;
 
@@ -60,7 +59,7 @@
             final ArrayList<String> packagesUnavailable = new ArrayList<>();
 
             if (!launcherApps.isPackageEnabled(puk.mPackageName, user)) {
-                if (pmHelper.isAppOnSdcard(puk.mPackageName, user)) {
+                if (new ApplicationInfoWrapper(context, puk.mPackageName, user).isOnSdCard()) {
                     packagesUnavailable.add(puk.mPackageName);
                 } else {
                     packagesRemoved.add(puk.mPackageName);
diff --git a/src/com/android/launcher3/model/SessionFailureTask.kt b/src/com/android/launcher3/model/SessionFailureTask.kt
new file mode 100644
index 0000000..0d006fa
--- /dev/null
+++ b/src/com/android/launcher3/model/SessionFailureTask.kt
@@ -0,0 +1,77 @@
+/*
+ * 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
+
+import android.content.ComponentName
+import android.os.UserHandle
+import android.text.TextUtils
+import com.android.launcher3.LauncherModel.ModelUpdateTask
+import com.android.launcher3.icons.cache.BaseIconCache
+import com.android.launcher3.model.data.WorkspaceItemInfo
+import com.android.launcher3.util.ApplicationInfoWrapper
+import com.android.launcher3.util.ItemInfoMatcher
+
+/** Model task run when there is a package install session failure */
+class SessionFailureTask(val packageName: String, val user: UserHandle) : ModelUpdateTask {
+
+    override fun execute(
+        taskController: ModelTaskController,
+        dataModel: BgDataModel,
+        apps: AllAppsList,
+    ) {
+        val iconCache = taskController.app.iconCache
+        val isAppArchived =
+            ApplicationInfoWrapper(taskController.app.context, packageName, user).isArchived()
+        synchronized(dataModel) {
+            if (isAppArchived) {
+                val updatedItems = mutableListOf<WorkspaceItemInfo>()
+                // Remove package icon cache entry for archived app in case of a session
+                // failure.
+                iconCache.remove(
+                    ComponentName(packageName, packageName + BaseIconCache.EMPTY_CLASS_NAME),
+                    user,
+                )
+                for (info in dataModel.itemsIdMap) {
+                    if (info is WorkspaceItemInfo && info.isArchived && user == info.user) {
+                        // Refresh icons on the workspace for archived apps.
+                        iconCache.getTitleAndIcon(info, info.usingLowResIcon())
+                        updatedItems.add(info)
+                    }
+                }
+
+                if (updatedItems.isNotEmpty()) {
+                    taskController.bindUpdatedWorkspaceItems(updatedItems)
+                }
+                apps.updateIconsAndLabels(hashSetOf(packageName), user)
+                taskController.bindApplicationsIfNeeded()
+            } else {
+                val removedItems =
+                    dataModel.itemsIdMap.filter { info ->
+                        (info is WorkspaceItemInfo && info.hasPromiseIconUi()) &&
+                            user == info.user &&
+                            TextUtils.equals(packageName, info.intent.getPackage())
+                    }
+                if (removedItems.isNotEmpty()) {
+                    taskController.deleteAndBindComponentsRemoved(
+                        ItemInfoMatcher.ofItems(removedItems),
+                        "removed because install session failed",
+                    )
+                }
+            }
+        }
+    }
+}
diff --git a/src/com/android/launcher3/model/ShortcutsChangedTask.java b/src/com/android/launcher3/model/ShortcutsChangedTask.java
index 1916d23..b5a7382 100644
--- a/src/com/android/launcher3/model/ShortcutsChangedTask.java
+++ b/src/com/android/launcher3/model/ShortcutsChangedTask.java
@@ -24,11 +24,12 @@
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherModel.ModelUpdateTask;
 import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.icons.CacheableShortcutInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.shortcuts.ShortcutKey;
 import com.android.launcher3.shortcuts.ShortcutRequest;
+import com.android.launcher3.util.ApplicationInfoWrapper;
 import com.android.launcher3.util.ItemInfoMatcher;
-import com.android.launcher3.util.PackageManagerHelper;
 
 import java.util.ArrayList;
 import java.util.HashSet;
@@ -79,12 +80,11 @@
         }
 
         if (!matchingWorkspaceItems.isEmpty()) {
+            ApplicationInfoWrapper infoWrapper =
+                    new ApplicationInfoWrapper(context, mPackageName, mUser);
             if (mShortcuts.isEmpty()) {
-                PackageManagerHelper packageManagerHelper =
-                        PackageManagerHelper.INSTANCE.get(context);
                 // Verify that the app is indeed installed.
-                if (!packageManagerHelper.isAppInstalled(mPackageName, mUser)
-                        && !packageManagerHelper.isAppArchivedForUser(mPackageName, mUser)) {
+                if (!infoWrapper.isInstalled() && !infoWrapper.isArchived()) {
                     // App is not installed or archived, ignoring package events
                     return;
                 }
@@ -104,7 +104,6 @@
                 if (!fullDetails.isPinned()) {
                     continue;
                 }
-
                 String sid = fullDetails.getId();
                 nonPinnedIds.remove(sid);
                 matchingWorkspaceItems
@@ -112,7 +111,8 @@
                         .filter(itemInfo -> sid.equals(itemInfo.getDeepShortcutId()))
                         .forEach(workspaceItemInfo -> {
                             workspaceItemInfo.updateFromDeepShortcutInfo(fullDetails, context);
-                            app.getIconCache().getShortcutIcon(workspaceItemInfo, fullDetails);
+                            app.getIconCache().getShortcutIcon(workspaceItemInfo,
+                                    new CacheableShortcutInfo(fullDetails, infoWrapper));
                             updatedWorkspaceItemInfos.add(workspaceItemInfo);
                         });
             }
diff --git a/src/com/android/launcher3/model/WidgetItem.java b/src/com/android/launcher3/model/WidgetItem.java
index ac9f2d6..e757a68 100644
--- a/src/com/android/launcher3/model/WidgetItem.java
+++ b/src/com/android/launcher3/model/WidgetItem.java
@@ -7,7 +7,6 @@
 import android.annotation.SuppressLint;
 import android.content.Context;
 import android.content.pm.ActivityInfo;
-import android.content.pm.PackageManager;
 import android.content.res.Resources;
 import android.util.SparseArray;
 import android.widget.RemoteViews;
@@ -75,10 +74,10 @@
         this(info, idp, iconCache, context, new WidgetManagerHelper(context));
     }
 
-    public WidgetItem(ShortcutConfigActivityInfo info, IconCache iconCache, PackageManager pm) {
+    public WidgetItem(ShortcutConfigActivityInfo info, IconCache iconCache) {
         super(info.getComponent(), info.getUser());
         label = info.isPersistable() ? iconCache.getTitleNoCache(info) :
-                Utilities.trim(info.getLabel(pm));
+                Utilities.trim(info.getLabel());
         description = null;
         widgetInfo = null;
         activityInfo = info;
diff --git a/src/com/android/launcher3/model/WidgetsFilterDataProvider.kt b/src/com/android/launcher3/model/WidgetsFilterDataProvider.kt
new file mode 100644
index 0000000..0571de3
--- /dev/null
+++ b/src/com/android/launcher3/model/WidgetsFilterDataProvider.kt
@@ -0,0 +1,70 @@
+/*
+ * 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
+
+import android.content.Context
+import androidx.annotation.WorkerThread
+import com.android.launcher3.R
+import com.android.launcher3.util.ResourceBasedOverride
+import java.util.function.Predicate
+
+/** Helper for the widgets model to load the filters that can be applied to available widgets. */
+open class WidgetsFilterDataProvider(val context: Context) : ResourceBasedOverride {
+    /**
+     * Start regular periodic refresh of widget filtering data starting now (if not started
+     * already).
+     */
+    @WorkerThread
+    open fun initPeriodicDataRefresh(callback: WidgetsFilterLoadedCallback? = null) {
+        // no-op
+    }
+
+    /**
+     * Returns a filter that should be applied to the widget predictions.
+     *
+     * @return null if no filter needs to be applied
+     */
+    @WorkerThread open fun getPredictedWidgetsFilter(): Predicate<WidgetItem>? = null
+
+    /**
+     * Returns a filter that should be applied to the widgets list to see which widgets can be shown
+     * by default.
+     *
+     * @return null if no separate "default" list is supported
+     */
+    @WorkerThread open fun getDefaultWidgetsFilter(): Predicate<WidgetItem>? = null
+
+    /** Called when filter data provider is no longer needed. */
+    open fun destroy() {}
+
+    companion object {
+        /** Returns a new instance of the [WidgetsFilterDataProvider] based on resource override. */
+        fun newInstance(context: Context?): WidgetsFilterDataProvider {
+            return ResourceBasedOverride.Overrides.getObject(
+                WidgetsFilterDataProvider::class.java,
+                context,
+                R.string.widgets_filter_data_provider_class,
+            )
+        }
+    }
+}
+
+/** Interface for the model callback to be invoked when filters are loaded. */
+interface WidgetsFilterLoadedCallback {
+    /** Method called back when widget filters are loaded */
+    fun onWidgetsFilterLoaded()
+}
diff --git a/src/com/android/launcher3/model/WidgetsModel.java b/src/com/android/launcher3/model/WidgetsModel.java
index c949ce6..01d4996 100644
--- a/src/com/android/launcher3/model/WidgetsModel.java
+++ b/src/com/android/launcher3/model/WidgetsModel.java
@@ -14,11 +14,12 @@
 import android.appwidget.AppWidgetProviderInfo;
 import android.content.ComponentName;
 import android.content.Context;
-import android.content.pm.PackageManager;
 import android.os.UserHandle;
 import android.util.Log;
 import android.util.Pair;
 
+import androidx.annotation.AnyThread;
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.collection.ArrayMap;
 
@@ -27,8 +28,8 @@
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.icons.ComponentWithLabelAndIcon;
 import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.icons.cache.CachedObject;
 import com.android.launcher3.model.data.PackageItemInfo;
 import com.android.launcher3.pm.ShortcutConfigActivityInfo;
 import com.android.launcher3.util.ComponentKey;
@@ -66,6 +67,8 @@
 
     /* Map of widgets and shortcuts that are tracked per package. */
     private final Map<PackageItemInfo, List<WidgetItem>> mWidgetsByPackageItem = new HashMap<>();
+    @Nullable private Predicate<WidgetItem> mDefaultWidgetsFilter = null;
+    @Nullable private Predicate<WidgetItem> mPredictedWidgetsFilter = null;
 
     /**
      * Returns all widgets keyed by their component key.
@@ -93,23 +96,52 @@
     }
 
     /**
+     * Returns widget filter that can be applied to {@link WidgetItem}s to check if they can be
+     * shown in the default widgets list.
+     * <p>Returns null if filtering isn't available</p>
+     */
+    @AnyThread
+    public @Nullable Predicate<WidgetItem> getDefaultWidgetsFilter() {
+        return mDefaultWidgetsFilter;
+    }
+
+    /**
+     * Returns widget filter that can be applied to {@link WidgetItem}s to check if they can be
+     * part of widget predictions.
+     * <p>Returns null if filter isn't available</p>
+     */
+    @AnyThread
+    public @Nullable  Predicate<WidgetItem> getPredictedWidgetsFilter() {
+        return mPredictedWidgetsFilter;
+    }
+
+    /**
+     * Updates model with latest filter data in cache.
+     */
+    public void updateWidgetFilters(@NonNull WidgetsFilterDataProvider widgetsFilterDataProvider) {
+        if (!WIDGETS_ENABLED) {
+            return;
+        }
+        mDefaultWidgetsFilter = widgetsFilterDataProvider.getDefaultWidgetsFilter();
+        mPredictedWidgetsFilter = widgetsFilterDataProvider.getPredictedWidgetsFilter();
+    }
+
+    /**
      * @param packageUser If null, all widgets and shortcuts are updated and returned, otherwise
      *                    only widgets and shortcuts associated with the package/user are.
      */
-    public List<ComponentWithLabelAndIcon> update(
+    public List<CachedObject> update(
             LauncherAppState app, @Nullable PackageUserKey packageUser) {
         if (!WIDGETS_ENABLED) {
-            return Collections.emptyList();
+            return new ArrayList<>();
         }
         Preconditions.assertWorkerThread();
 
         Context context = app.getContext();
         final ArrayList<WidgetItem> widgetsAndShortcuts = new ArrayList<>();
-        List<ComponentWithLabelAndIcon> updatedItems = new ArrayList<>();
+        List<CachedObject> updatedItems = new ArrayList<>();
         try {
             InvariantDeviceProfile idp = app.getInvariantDeviceProfile();
-            PackageManager pm = app.getContext().getPackageManager();
-
             // Widgets
             WidgetManagerHelper widgetManager = new WidgetManagerHelper(context);
             for (AppWidgetProviderInfo widgetInfo : widgetManager.getAllProviders(packageUser)) {
@@ -125,7 +157,7 @@
             // Shortcuts
             for (ShortcutConfigActivityInfo info :
                     queryList(context, packageUser)) {
-                widgetsAndShortcuts.add(new WidgetItem(info, app.getIconCache(), pm));
+                widgetsAndShortcuts.add(new WidgetItem(info, app.getIconCache()));
                 updatedItems.add(info);
             }
             setWidgetsAndShortcuts(widgetsAndShortcuts, app, packageUser);
@@ -190,8 +222,7 @@
                     WidgetItem item = items.get(i);
                     if (item.user.equals(user)) {
                         if (item.activityInfo != null) {
-                            items.set(i, new WidgetItem(item.activityInfo, app.getIconCache(),
-                                    app.getContext().getPackageManager()));
+                            items.set(i, new WidgetItem(item.activityInfo, app.getIconCache()));
                         } else {
                             items.set(i, new WidgetItem(item.widgetInfo,
                                     app.getInvariantDeviceProfile(), app.getIconCache(),
@@ -303,7 +334,7 @@
             if (pInfo == null) {
                 pInfo = new PackageItemInfo(key.mPackageName, key.mWidgetCategory, key.mUser);
                 pInfo.user = key.mUser;
-                mMap.put(key,  pInfo);
+                mMap.put(key, pInfo);
             }
             return pInfo;
         }
diff --git a/src/com/android/launcher3/model/WorkspaceItemProcessor.kt b/src/com/android/launcher3/model/WorkspaceItemProcessor.kt
index 1f1e514..c02336e 100644
--- a/src/com/android/launcher3/model/WorkspaceItemProcessor.kt
+++ b/src/com/android/launcher3/model/WorkspaceItemProcessor.kt
@@ -32,6 +32,7 @@
 import com.android.launcher3.LauncherSettings.Favorites
 import com.android.launcher3.backuprestore.LauncherRestoreEventLogger.RestoreError
 import com.android.launcher3.config.FeatureFlags
+import com.android.launcher3.icons.CacheableShortcutInfo
 import com.android.launcher3.logging.FileLog
 import com.android.launcher3.model.data.AppInfo
 import com.android.launcher3.model.data.AppPairInfo
@@ -44,6 +45,7 @@
 import com.android.launcher3.pm.UserCache
 import com.android.launcher3.shortcuts.ShortcutKey
 import com.android.launcher3.util.ApiWrapper
+import com.android.launcher3.util.ApplicationInfoWrapper
 import com.android.launcher3.util.ComponentKey
 import com.android.launcher3.util.PackageManagerHelper
 import com.android.launcher3.util.PackageUserKey
@@ -75,7 +77,7 @@
     private val pmHelper: PackageManagerHelper,
     private val iconRequestInfos: MutableList<IconRequestInfo<WorkspaceItemInfo>>,
     private val unlockedUsers: LongSparseArray<Boolean>,
-    private val allDeepShortcuts: MutableList<ShortcutInfo>,
+    private val allDeepShortcuts: MutableList<CacheableShortcutInfo>,
 ) {
 
     private val isSafeMode = app.isSafeModeEnabled
@@ -152,6 +154,7 @@
             c.markDeleted("No target package for item id=${c.id}", RestoreError.MISSING_INFO)
             return
         }
+        val appInfoWrapper = ApplicationInfoWrapper(app.context, targetPkg, c.user)
         var validTarget = launcherApps.isPackageEnabled(targetPkg, c.user)
 
         // If it's a deep shortcut, we'll use pinned shortcuts to restore it
@@ -218,7 +221,7 @@
                             }
                         }
                     }
-                    pmHelper.isAppOnSdcard(targetPkg, c.user) -> {
+                    appInfoWrapper.isOnSdCard() -> {
                         // Package is present but not available.
                         disabledState =
                             disabledState or WorkspaceItemInfo.FLAG_DISABLED_NOT_AVAILABLE
@@ -276,13 +279,14 @@
                     info = WorkspaceItemInfo(pinnedShortcut, app.context)
                     // If the pinned deep shortcut is no longer published,
                     // use the last saved icon instead of the default.
-                    iconCache.getShortcutIcon(info, pinnedShortcut, c::loadIcon)
-                    if (pmHelper.isAppSuspended(pinnedShortcut.getPackage(), info.user)) {
+                    val csi = CacheableShortcutInfo(pinnedShortcut, appInfoWrapper)
+                    iconCache.getShortcutIcon(info, csi, c::loadIcon)
+                    if (appInfoWrapper.isSuspended()) {
                         info.runtimeStatusFlags =
                             info.runtimeStatusFlags or ItemInfoWithIcon.FLAG_DISABLED_SUSPENDED
                     }
                     intent = info.getIntent()
-                    allDeepShortcuts.add(pinnedShortcut)
+                    allDeepShortcuts.add(csi)
                 } else {
                     // Create a shortcut info in disabled mode for now.
                     info = c.loadSimpleWorkspaceItem()
@@ -294,7 +298,7 @@
                 info = c.loadSimpleWorkspaceItem()
 
                 // Shortcuts are only available on the primary profile
-                if (!TextUtils.isEmpty(targetPkg) && pmHelper.isAppSuspended(targetPkg, c.user)) {
+                if (appInfoWrapper.isSuspended()) {
                     disabledState = disabledState or ItemInfoWithIcon.FLAG_DISABLED_SUSPENDED
                 }
                 info.options = c.options
@@ -325,7 +329,7 @@
             info.spanX = 1
             info.spanY = 1
             info.runtimeStatusFlags = info.runtimeStatusFlags or disabledState
-            if (isSafeMode && !PackageManagerHelper.isSystemApp(app.context, intent)) {
+            if (isSafeMode && !appInfoWrapper.isSystem()) {
                 info.runtimeStatusFlags =
                     info.runtimeStatusFlags or ItemInfoWithIcon.FLAG_DISABLED_SAFEMODE
             }
@@ -486,7 +490,8 @@
                         (si == null) &&
                         (lapi == null) &&
                         !(Flags.enableSupportForArchiving() &&
-                            pmHelper.isAppArchived(component.packageName))
+                            ApplicationInfoWrapper(app.context, component.packageName, c.user)
+                                .isArchived())
                 ) {
                     // Restore never started
                     c.markDeleted(
diff --git a/src/com/android/launcher3/model/data/AppInfo.java b/src/com/android/launcher3/model/data/AppInfo.java
index a4281f8..97b62b4 100644
--- a/src/com/android/launcher3/model/data/AppInfo.java
+++ b/src/com/android/launcher3/model/data/AppInfo.java
@@ -21,7 +21,6 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
-import android.content.pm.ApplicationInfo;
 import android.content.pm.LauncherActivityInfo;
 import android.os.UserHandle;
 import android.os.UserManager;
@@ -36,6 +35,7 @@
 import com.android.launcher3.pm.PackageInstallInfo;
 import com.android.launcher3.pm.UserCache;
 import com.android.launcher3.util.ApiWrapper;
+import com.android.launcher3.util.ApplicationInfoWrapper;
 import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.util.UserIconInfo;
 
@@ -187,8 +187,8 @@
             ApiWrapper apiWrapper, PackageManagerHelper pmHelper) {
         final int oldProgressLevel = info.getProgressLevel();
         final int oldRuntimeStatusFlags = info.runtimeStatusFlags;
-        ApplicationInfo appInfo = lai.getApplicationInfo();
-        if (PackageManagerHelper.isAppSuspended(appInfo)) {
+        ApplicationInfoWrapper appInfo = new ApplicationInfoWrapper(lai.getApplicationInfo());
+        if (appInfo.isSuspended()) {
             info.runtimeStatusFlags |= FLAG_DISABLED_SUSPENDED;
         } else {
             info.runtimeStatusFlags &= ~FLAG_DISABLED_SUSPENDED;
@@ -200,8 +200,7 @@
                 info.runtimeStatusFlags &= ~FLAG_ARCHIVED;
             }
         }
-        info.runtimeStatusFlags |= (appInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0
-                ? FLAG_SYSTEM_NO : FLAG_SYSTEM_YES;
+        info.runtimeStatusFlags |= appInfo.isSystem() ? FLAG_SYSTEM_YES : FLAG_SYSTEM_NO;
 
         if (Flags.privateSpaceRestrictAccessibilityDrag()) {
             if (userIconInfo.isPrivate()) {
diff --git a/src/com/android/launcher3/model/data/AppPairInfo.kt b/src/com/android/launcher3/model/data/AppPairInfo.kt
index 2eb6154..3496c17 100644
--- a/src/com/android/launcher3/model/data/AppPairInfo.kt
+++ b/src/com/android/launcher3/model/data/AppPairInfo.kt
@@ -74,7 +74,7 @@
             (ActivityContext.lookupContext(context) as ActivityContext).getDeviceProfile().isTablet
         return Pair(
             isTablet || !getFirstApp().isNonResizeable(),
-            isTablet || !getSecondApp().isNonResizeable()
+            isTablet || !getSecondApp().isNonResizeable(),
         )
     }
 
@@ -105,10 +105,10 @@
     }
 
     /** Generates an ItemInfo for logging. */
-    override fun buildProto(cInfo: CollectionInfo?): LauncherAtom.ItemInfo {
+    override fun buildProto(cInfo: CollectionInfo?, context: Context): LauncherAtom.ItemInfo {
         val appPairIcon = LauncherAtom.FolderIcon.newBuilder().setCardinality(contents.size)
         appPairIcon.setLabelInfo(title.toString())
-        return getDefaultItemInfoBuilder()
+        return getDefaultItemInfoBuilder(context)
             .setFolderIcon(appPairIcon)
             .setRank(rank)
             .setContainerInfo(getContainerInfo())
diff --git a/src/com/android/launcher3/model/data/CollectionInfo.kt b/src/com/android/launcher3/model/data/CollectionInfo.kt
index 4f5e12f..12ba164 100644
--- a/src/com/android/launcher3/model/data/CollectionInfo.kt
+++ b/src/com/android/launcher3/model/data/CollectionInfo.kt
@@ -17,7 +17,6 @@
 package com.android.launcher3.model.data
 
 import com.android.launcher3.LauncherSettings
-import com.android.launcher3.logger.LauncherAtom
 import com.android.launcher3.util.ContentWriter
 import java.util.function.Predicate
 
@@ -42,9 +41,4 @@
         super.onAddToDatabase(writer)
         writer.put(LauncherSettings.Favorites.TITLE, title)
     }
-
-    /** Returns the collection wrapped as {@link LauncherAtom.ItemInfo} for logging. */
-    override fun buildProto(): LauncherAtom.ItemInfo {
-        return buildProto(null)
-    }
 }
diff --git a/src/com/android/launcher3/model/data/FolderInfo.java b/src/com/android/launcher3/model/data/FolderInfo.java
index 18d2b85..f0f2892 100644
--- a/src/com/android/launcher3/model/data/FolderInfo.java
+++ b/src/com/android/launcher3/model/data/FolderInfo.java
@@ -24,6 +24,8 @@
 import static com.android.launcher3.logger.LauncherAtom.Attribute.MANUAL_LABEL;
 import static com.android.launcher3.logger.LauncherAtom.Attribute.SUGGESTED_LABEL;
 
+import android.content.Context;
+
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
@@ -245,13 +247,13 @@
 
     @NonNull
     @Override
-    public LauncherAtom.ItemInfo buildProto(@Nullable CollectionInfo cInfo) {
+    public LauncherAtom.ItemInfo buildProto(@Nullable CollectionInfo cInfo, Context context) {
         FolderIcon.Builder folderIcon = FolderIcon.newBuilder()
                 .setCardinality(getContents().size());
         if (LabelState.SUGGESTED.equals(getLabelState())) {
             folderIcon.setLabelInfo(title.toString());
         }
-        return getDefaultItemInfoBuilder()
+        return getDefaultItemInfoBuilder(context)
                 .setFolderIcon(folderIcon)
                 .setRank(rank)
                 .addItemAttributes(getLabelState().mLogAttribute)
diff --git a/src/com/android/launcher3/model/data/ItemInfo.java b/src/com/android/launcher3/model/data/ItemInfo.java
index b706d24..c22a8a5 100644
--- a/src/com/android/launcher3/model/data/ItemInfo.java
+++ b/src/com/android/launcher3/model/data/ItemInfo.java
@@ -36,6 +36,7 @@
 
 import android.content.ComponentName;
 import android.content.ContentValues;
+import android.content.Context;
 import android.content.Intent;
 import android.net.Uri;
 import android.os.Process;
@@ -352,16 +353,16 @@
      * Creates {@link LauncherAtom.ItemInfo} with important fields and parent container info.
      */
     @NonNull
-    public LauncherAtom.ItemInfo buildProto() {
-        return buildProto(null);
+    public LauncherAtom.ItemInfo buildProto(Context context) {
+        return buildProto(null, context);
     }
 
     /**
      * Creates {@link LauncherAtom.ItemInfo} with important fields and parent container info.
      */
     @NonNull
-    public LauncherAtom.ItemInfo buildProto(@Nullable final CollectionInfo cInfo) {
-        LauncherAtom.ItemInfo.Builder itemBuilder = getDefaultItemInfoBuilder();
+    public LauncherAtom.ItemInfo buildProto(@Nullable final CollectionInfo cInfo, Context context) {
+        LauncherAtom.ItemInfo.Builder itemBuilder = getDefaultItemInfoBuilder(context);
         Optional<ComponentName> nullableComponent = Optional.ofNullable(getTargetComponent());
         switch (itemType) {
             case ITEM_TYPE_APPLICATION:
@@ -434,10 +435,10 @@
     }
 
     @NonNull
-    protected LauncherAtom.ItemInfo.Builder getDefaultItemInfoBuilder() {
+    protected LauncherAtom.ItemInfo.Builder getDefaultItemInfoBuilder(Context context) {
         LauncherAtom.ItemInfo.Builder itemBuilder = LauncherAtom.ItemInfo.newBuilder();
-        SettingsCache.INSTANCE.executeIfCreated(cache ->
-                itemBuilder.setIsKidsMode(cache.getValue(NAV_BAR_KIDS_MODE, 0)));
+        itemBuilder.setIsKidsMode(
+                SettingsCache.INSTANCE.get(context).getValue(NAV_BAR_KIDS_MODE, 0));
         UserCache.INSTANCE.executeIfCreated(cache ->
                 itemBuilder.setUserType(getUserType(cache.getUserInfo(user))));
         itemBuilder.setRank(rank);
diff --git a/src/com/android/launcher3/model/data/LauncherAppWidgetInfo.java b/src/com/android/launcher3/model/data/LauncherAppWidgetInfo.java
index 361f09d..7569ed5 100644
--- a/src/com/android/launcher3/model/data/LauncherAppWidgetInfo.java
+++ b/src/com/android/launcher3/model/data/LauncherAppWidgetInfo.java
@@ -24,6 +24,7 @@
 
 import android.appwidget.AppWidgetHostView;
 import android.content.ComponentName;
+import android.content.Context;
 import android.content.Intent;
 import android.content.res.Resources;
 import android.os.Process;
@@ -270,8 +271,9 @@
 
     @NonNull
     @Override
-    public LauncherAtom.ItemInfo buildProto(@Nullable CollectionInfo collectionInfo) {
-        LauncherAtom.ItemInfo info = super.buildProto(collectionInfo);
+    public LauncherAtom.ItemInfo buildProto(
+            @Nullable CollectionInfo collectionInfo, Context context) {
+        LauncherAtom.ItemInfo info = super.buildProto(collectionInfo, context);
         return info.toBuilder()
                 .setWidget(info.getWidget().toBuilder().setWidgetFeatures(widgetFeatures))
                 .addItemAttributes(getAttribute(sourceContainer))
diff --git a/src/com/android/launcher3/model/data/WorkspaceItemInfo.java b/src/com/android/launcher3/model/data/WorkspaceItemInfo.java
index f31bf1e..9af61f0 100644
--- a/src/com/android/launcher3/model/data/WorkspaceItemInfo.java
+++ b/src/com/android/launcher3/model/data/WorkspaceItemInfo.java
@@ -189,7 +189,13 @@
         if (TextUtils.isEmpty(label)) {
             label = shortcutInfo.getShortLabel();
         }
-        contentDescription = context.getPackageManager().getUserBadgedLabel(label, user);
+        try {
+            contentDescription = context.getPackageManager().getUserBadgedLabel(label, user);
+        } catch (SecurityException e) {
+            contentDescription = null;
+            Log.e(TAG, "Failed to get content description", e);
+        }
+
         if (shortcutInfo.isEnabled()) {
             runtimeStatusFlags &= ~FLAG_DISABLED_BY_PUBLISHER;
         } else {
diff --git a/src/com/android/launcher3/pm/InstallSessionHelper.java b/src/com/android/launcher3/pm/InstallSessionHelper.java
index e66f496..afc5117 100644
--- a/src/com/android/launcher3/pm/InstallSessionHelper.java
+++ b/src/com/android/launcher3/pm/InstallSessionHelper.java
@@ -17,7 +17,6 @@
 package com.android.launcher3.pm;
 
 import android.content.Context;
-import android.content.pm.ApplicationInfo;
 import android.content.pm.LauncherApps;
 import android.content.pm.PackageInstaller;
 import android.content.pm.PackageInstaller.SessionInfo;
@@ -32,15 +31,17 @@
 import com.android.launcher3.Flags;
 import com.android.launcher3.LauncherPrefs;
 import com.android.launcher3.SessionCommitReceiver;
+import com.android.launcher3.dagger.ApplicationContext;
+import com.android.launcher3.dagger.LauncherAppSingleton;
+import com.android.launcher3.dagger.LauncherBaseAppComponent;
 import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.model.ItemInstallQueue;
+import com.android.launcher3.util.ApplicationInfoWrapper;
+import com.android.launcher3.util.DaggerSingletonObject;
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.IntSet;
-import com.android.launcher3.util.MainThreadInitializedObject;
-import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.util.Preconditions;
-import com.android.launcher3.util.SafeCloseable;
 
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -48,11 +49,14 @@
 import java.util.List;
 import java.util.Objects;
 
+import javax.inject.Inject;
+
 /**
  * Utility class to tracking install sessions
  */
 @SuppressWarnings("NewApi")
-public class InstallSessionHelper implements SafeCloseable {
+@LauncherAppSingleton
+public class InstallSessionHelper {
 
     @NonNull
     private static final String LOG = "InstallSessionHelper";
@@ -65,8 +69,8 @@
     private static final boolean DEBUG = false;
 
     @NonNull
-    public static final MainThreadInitializedObject<InstallSessionHelper> INSTANCE =
-            new MainThreadInitializedObject<>(InstallSessionHelper::new);
+    public static final DaggerSingletonObject<InstallSessionHelper> INSTANCE =
+            new DaggerSingletonObject<>(LauncherBaseAppComponent::getInstallSessionHelper);
 
     @Nullable
     private final LauncherApps mLauncherApps;
@@ -83,15 +87,13 @@
     @Nullable
     private IntSet mPromiseIconIds;
 
-    public InstallSessionHelper(@NonNull final Context context) {
+    @Inject
+    public InstallSessionHelper(@NonNull @ApplicationContext final Context context) {
         mInstaller = context.getPackageManager().getPackageInstaller();
         mAppContext = context.getApplicationContext();
         mLauncherApps = context.getSystemService(LauncherApps.class);
     }
 
-    @Override
-    public void close() { }
-
     @WorkerThread
     @NonNull
     private IntSet getPromiseIconIds() {
@@ -171,8 +173,7 @@
         synchronized (mSessionVerifiedMap) {
             if (!mSessionVerifiedMap.containsKey(pkg)) {
                 boolean hasSystemFlag = DEBUG || mAppContext.getPackageName().equals(pkg)
-                        || PackageManagerHelper.INSTANCE.get(mAppContext)
-                                .getApplicationInfo(pkg, user, ApplicationInfo.FLAG_SYSTEM) != null;
+                        || new ApplicationInfoWrapper(mAppContext, pkg, user).isSystem();
                 mSessionVerifiedMap.put(pkg, hasSystemFlag);
             }
         }
@@ -245,8 +246,8 @@
                 && sessionInfo.getInstallReason() == PackageManager.INSTALL_REASON_USER
                 && sessionInfo.getAppIcon() != null
                 && !TextUtils.isEmpty(sessionInfo.getAppLabel())
-                && !PackageManagerHelper.INSTANCE.get(mAppContext).isAppInstalled(
-                        sessionInfo.getAppPackageName(), getUserHandle(sessionInfo));
+                && !new ApplicationInfoWrapper(mAppContext, sessionInfo.getAppPackageName(),
+                        getUserHandle(sessionInfo)).isInstalled();
     }
 
     public InstallSessionTracker registerInstallTracker(
diff --git a/src/com/android/launcher3/pm/PinRequestHelper.java b/src/com/android/launcher3/pm/PinRequestHelper.java
index 47afeef..2ed6591 100644
--- a/src/com/android/launcher3/pm/PinRequestHelper.java
+++ b/src/com/android/launcher3/pm/PinRequestHelper.java
@@ -32,7 +32,8 @@
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.icons.ShortcutCachingLogic;
+import com.android.launcher3.icons.CacheableShortcutCachingLogic;
+import com.android.launcher3.icons.CacheableShortcutInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 
 public class PinRequestHelper {
@@ -78,7 +79,8 @@
             // Apply the unbadged icon synchronously using the caching logic directly and
             // fetch the actual icon asynchronously.
             LauncherAppState app = LauncherAppState.getInstance(context);
-            info.bitmap = new ShortcutCachingLogic().loadIcon(context, app.getIconCache(), si);
+            info.bitmap = CacheableShortcutCachingLogic.INSTANCE.loadIcon(
+                    context, app.getIconCache(), new CacheableShortcutInfo(si, context));
             app.getModel().updateAndBindWorkspaceItem(info, si);
             return info;
         } else {
diff --git a/src/com/android/launcher3/pm/ShortcutConfigActivityInfo.java b/src/com/android/launcher3/pm/ShortcutConfigActivityInfo.java
index 351ebce..409174e 100644
--- a/src/com/android/launcher3/pm/ShortcutConfigActivityInfo.java
+++ b/src/com/android/launcher3/pm/ShortcutConfigActivityInfo.java
@@ -26,9 +26,9 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentSender;
+import android.content.pm.ApplicationInfo;
 import android.content.pm.LauncherActivityInfo;
 import android.content.pm.LauncherApps;
-import android.content.pm.PackageManager;
 import android.graphics.drawable.Drawable;
 import android.os.Build;
 import android.os.Process;
@@ -40,9 +40,10 @@
 
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.R;
-import com.android.launcher3.icons.ComponentWithLabelAndIcon;
-import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.icons.cache.BaseIconCache;
+import com.android.launcher3.icons.cache.CachedObject;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.util.ApplicationInfoWrapper;
 import com.android.launcher3.util.PackageUserKey;
 
 import java.util.ArrayList;
@@ -52,16 +53,26 @@
 /**
  * Wrapper class for representing a shortcut configure activity.
  */
-public abstract class ShortcutConfigActivityInfo implements ComponentWithLabelAndIcon {
+public abstract class ShortcutConfigActivityInfo implements CachedObject {
 
     private static final String TAG = "SCActivityInfo";
 
     private final ComponentName mCn;
     private final UserHandle mUser;
+    private final ApplicationInfoWrapper mInfoWrapper;
 
-    protected ShortcutConfigActivityInfo(ComponentName cn, UserHandle user) {
+    protected ShortcutConfigActivityInfo(
+            ComponentName cn, UserHandle user, ApplicationInfoWrapper infoWrapper) {
         mCn = cn;
         mUser = user;
+        mInfoWrapper = infoWrapper;
+    }
+
+    protected ShortcutConfigActivityInfo(
+            ComponentName cn, UserHandle user, Context context) {
+        mCn = cn;
+        mUser = user;
+        mInfoWrapper = new ApplicationInfoWrapper(context, cn.getPackageName(), user);
     }
 
     @Override
@@ -79,7 +90,7 @@
     }
 
     @Override
-    public abstract Drawable getFullResIcon(IconCache cache);
+    public abstract Drawable getFullResIcon(BaseIconCache cache);
 
     /**
      * Return a WorkspaceItemInfo, if it can be created directly on drop, without requiring any
@@ -89,6 +100,12 @@
         return null;
     }
 
+    @Nullable
+    @Override
+    public ApplicationInfo getApplicationInfo() {
+        return mInfoWrapper.getInfo();
+    }
+
     public boolean startConfigActivity(Activity activity, int requestCode) {
         Intent intent = new Intent(Intent.ACTION_CREATE_SHORTCUT)
                 .setComponent(getComponent());
@@ -107,7 +124,7 @@
     }
 
     /**
-     * Returns true if various properties ({@link #getLabel(PackageManager)},
+     * Returns true if various properties ({@link #getLabel()},
      * {@link #getFullResIcon}) can be safely persisted.
      */
     public boolean isPersistable() {
@@ -120,18 +137,19 @@
         private final LauncherActivityInfo mInfo;
 
         public ShortcutConfigActivityInfoVO(LauncherActivityInfo info) {
-            super(info.getComponentName(), info.getUser());
+            super(info.getComponentName(), info.getUser(),
+                    new ApplicationInfoWrapper(info.getApplicationInfo()));
             mInfo = info;
         }
 
         @Override
-        public CharSequence getLabel(PackageManager pm) {
+        public CharSequence getLabel() {
             return mInfo.getLabel();
         }
 
         @Override
-        public Drawable getFullResIcon(IconCache cache) {
-            return cache.getFullResIcon(mInfo);
+        public Drawable getFullResIcon(BaseIconCache cache) {
+            return cache.getFullResIcon(mInfo.getActivityInfo());
         }
 
         @Override
diff --git a/src/com/android/launcher3/pm/UserCache.java b/src/com/android/launcher3/pm/UserCache.java
index e861961..69f4469 100644
--- a/src/com/android/launcher3/pm/UserCache.java
+++ b/src/com/android/launcher3/pm/UserCache.java
@@ -26,7 +26,6 @@
 import android.os.UserManager;
 import android.util.ArrayMap;
 
-import androidx.annotation.AnyThread;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
@@ -111,7 +110,7 @@
         updateCache();
     }
 
-    @AnyThread
+    @WorkerThread
     private void onUsersChanged(Intent intent) {
         MODEL_EXECUTOR.execute(this::updateCache);
         UserHandle user = intent.getParcelableExtra(Intent.EXTRA_USER);
@@ -123,7 +122,7 @@
     }
 
     @WorkerThread
-    private void updateCache() {
+    public void updateCache() {
         mUserToSerialMap = ApiWrapper.INSTANCE.get(mContext).queryAllUsers();
         mUserToPreInstallAppMap = fetchPreInstallApps();
     }
diff --git a/src/com/android/launcher3/popup/PopupPopulator.java b/src/com/android/launcher3/popup/PopupPopulator.java
index aa24f60..755c3eb 100644
--- a/src/com/android/launcher3/popup/PopupPopulator.java
+++ b/src/com/android/launcher3/popup/PopupPopulator.java
@@ -27,11 +27,13 @@
 import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.icons.CacheableShortcutInfo;
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.shortcuts.DeepShortcutView;
 import com.android.launcher3.shortcuts.ShortcutRequest;
+import com.android.launcher3.util.ApplicationInfoWrapper;
 import com.android.launcher3.views.ActivityContext;
 
 import java.util.ArrayList;
@@ -113,6 +115,8 @@
         final ComponentName activity = originalInfo.getTargetComponent();
         final UserHandle user = originalInfo.user;
         return () -> {
+            ApplicationInfoWrapper infoWrapper =
+                    new ApplicationInfoWrapper(context, activity.getPackageName(), user);
             List<ShortcutInfo> shortcuts = new ShortcutRequest(context, user)
                     .withContainer(activity)
                     .query(ShortcutRequest.PUBLISHED);
@@ -121,7 +125,7 @@
             for (int i = 0; i < shortcuts.size() && i < shortcutViews.size(); i++) {
                 final ShortcutInfo shortcut = shortcuts.get(i);
                 final WorkspaceItemInfo si = new WorkspaceItemInfo(shortcut, context);
-                cache.getShortcutIcon(si, shortcut);
+                cache.getShortcutIcon(si, new CacheableShortcutInfo(shortcut, infoWrapper));
                 si.rank = i;
                 si.container = CONTAINER_SHORTCUTS;
 
diff --git a/src/com/android/launcher3/provider/LauncherDbUtils.java b/src/com/android/launcher3/provider/LauncherDbUtils.java
deleted file mode 100644
index 3ae643e..0000000
--- a/src/com/android/launcher3/provider/LauncherDbUtils.java
+++ /dev/null
@@ -1,210 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.provider;
-
-import static com.android.launcher3.LauncherSettings.Favorites.getColumns;
-import static com.android.launcher3.icons.IconCache.EXTRA_SHORTCUT_BADGE_OVERRIDE_PACKAGE;
-
-import android.content.ContentValues;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.ResolveInfo;
-import android.content.pm.ShortcutInfo;
-import android.database.Cursor;
-import android.database.sqlite.SQLiteDatabase;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.graphics.drawable.Icon;
-import android.os.PersistableBundle;
-import android.os.Process;
-import android.os.UserManager;
-import android.text.TextUtils;
-
-import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherSettings.Favorites;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.model.LoaderCursor;
-import com.android.launcher3.model.UserManagerState;
-import com.android.launcher3.pm.PinRequestHelper;
-import com.android.launcher3.pm.UserCache;
-import com.android.launcher3.shortcuts.ShortcutKey;
-import com.android.launcher3.util.IntArray;
-import com.android.launcher3.util.IntSet;
-import com.android.launcher3.util.PackageManagerHelper;
-
-/**
- * A set of utility methods for Launcher DB used for DB updates and migration.
- */
-public class LauncherDbUtils {
-    /**
-     * Returns a string which can be used as a where clause for DB query to match the given itemId
-     */
-    public static String itemIdMatch(int itemId) {
-        return "_id=" + itemId;
-    }
-
-    public static IntArray queryIntArray(boolean distinct, SQLiteDatabase db, String tableName,
-            String columnName, String selection, String groupBy, String orderBy) {
-        IntArray out = new IntArray();
-        try (Cursor c = db.query(distinct, tableName, new String[] { columnName }, selection, null,
-                groupBy, null, orderBy, null)) {
-            while (c.moveToNext()) {
-                out.add(c.getInt(0));
-            }
-        }
-        return out;
-    }
-
-    public static boolean tableExists(SQLiteDatabase db, String tableName) {
-        try (Cursor c = db.query(true, "sqlite_master", new String[] {"tbl_name"},
-                "tbl_name = ?", new String[] {tableName},
-                null, null, null, null, null)) {
-            return c.getCount() > 0;
-        }
-    }
-
-    public static void dropTable(SQLiteDatabase db, String tableName) {
-        db.execSQL("DROP TABLE IF EXISTS " + tableName);
-    }
-
-    /** Copy fromTable in fromDb to toTable in toDb. */
-    public static void copyTable(SQLiteDatabase fromDb, String fromTable, SQLiteDatabase toDb,
-            String toTable, Context context) {
-        long userSerial = UserCache.INSTANCE.get(context).getSerialNumberForUser(
-                Process.myUserHandle());
-        dropTable(toDb, toTable);
-        Favorites.addTableToDb(toDb, userSerial, false, toTable);
-        if (fromDb != toDb) {
-            toDb.execSQL("ATTACH DATABASE '" + fromDb.getPath() + "' AS from_db");
-            toDb.execSQL(
-                    "INSERT INTO " + toTable + " SELECT " + getColumns(userSerial)
-                        + " FROM from_db." + fromTable);
-            toDb.execSQL("DETACH DATABASE 'from_db'");
-        } else {
-            toDb.execSQL("INSERT INTO " + toTable + " SELECT " + getColumns(userSerial) + " FROM "
-                    + fromTable);
-        }
-    }
-
-    /**
-     * Migrates the legacy shortcuts to deep shortcuts pinned under Launcher.
-     * Removes any invalid shortcut or any shortcut which requires some permission to launch
-     */
-    public static void migrateLegacyShortcuts(Context context, SQLiteDatabase db) {
-        Cursor c = db.query(
-                Favorites.TABLE_NAME, null, "itemType = 1", null, null, null, null);
-        UserManagerState ums = new UserManagerState();
-        PackageManagerHelper pmHelper = PackageManagerHelper.INSTANCE.get(context);
-        ums.init(UserCache.INSTANCE.get(context),
-                context.getSystemService(UserManager.class));
-        LoaderCursor lc = new LoaderCursor(c, LauncherAppState.getInstance(context), ums, pmHelper,
-                null);
-        IntSet deletedShortcuts = new IntSet();
-
-        while (lc.moveToNext()) {
-            if (lc.user != Process.myUserHandle()) {
-                deletedShortcuts.add(lc.id);
-                continue;
-            }
-            Intent intent = lc.parseIntent();
-            if (intent == null) {
-                deletedShortcuts.add(lc.id);
-                continue;
-            }
-            if (TextUtils.isEmpty(lc.getTitle())) {
-                deletedShortcuts.add(lc.id);
-                continue;
-            }
-
-            // Make sure the target intent can be launched without any permissions. Otherwise remove
-            // the shortcut
-            ResolveInfo ri = context.getPackageManager().resolveActivity(intent, 0);
-            if (ri == null || !TextUtils.isEmpty(ri.activityInfo.permission)) {
-                deletedShortcuts.add(lc.id);
-                continue;
-            }
-            PersistableBundle extras = new PersistableBundle();
-            extras.putString(EXTRA_SHORTCUT_BADGE_OVERRIDE_PACKAGE, ri.activityInfo.packageName);
-            ShortcutInfo.Builder infoBuilder = new ShortcutInfo.Builder(
-                    context, "migrated_shortcut-" + lc.id)
-                    .setIntent(intent)
-                    .setExtras(extras)
-                    .setShortLabel(lc.getTitle());
-
-            Bitmap bitmap = null;
-            byte[] iconData = lc.getIconBlob();
-            if (iconData != null) {
-                bitmap = BitmapFactory.decodeByteArray(iconData, 0, iconData.length);
-            }
-            if (bitmap != null) {
-                infoBuilder.setIcon(Icon.createWithBitmap(bitmap));
-            }
-
-            ShortcutInfo info = infoBuilder.build();
-            try {
-                if (!PinRequestHelper.createRequestForShortcut(context, info).accept()) {
-                    deletedShortcuts.add(lc.id);
-                    continue;
-                }
-            } catch (Exception e) {
-                deletedShortcuts.add(lc.id);
-                continue;
-            }
-            ContentValues update = new ContentValues();
-            update.put(Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_DEEP_SHORTCUT);
-            update.put(Favorites.INTENT,
-                    ShortcutKey.makeIntent(info.getId(), context.getPackageName()).toUri(0));
-            db.update(Favorites.TABLE_NAME, update, "_id = ?",
-                    new String[] {Integer.toString(lc.id)});
-        }
-        lc.close();
-        if (!deletedShortcuts.isEmpty()) {
-            db.delete(Favorites.TABLE_NAME,
-                    Utilities.createDbSelectionQuery(Favorites._ID, deletedShortcuts.getArray()),
-                    null);
-        }
-
-        // Drop the unused columns
-        db.execSQL("ALTER TABLE " + Favorites.TABLE_NAME + " DROP COLUMN iconPackage;");
-        db.execSQL("ALTER TABLE " + Favorites.TABLE_NAME + " DROP COLUMN iconResource;");
-    }
-
-    /**
-     * Utility class to simplify managing sqlite transactions
-     */
-    public static class SQLiteTransaction implements AutoCloseable {
-        private final SQLiteDatabase mDb;
-
-        public SQLiteTransaction(SQLiteDatabase db) {
-            mDb = db;
-            db.beginTransaction();
-        }
-
-        public void commit() {
-            mDb.setTransactionSuccessful();
-        }
-
-        @Override
-        public void close() {
-            mDb.endTransaction();
-        }
-
-        public SQLiteDatabase getDb() {
-            return mDb;
-        }
-    }
-}
diff --git a/src/com/android/launcher3/provider/LauncherDbUtils.kt b/src/com/android/launcher3/provider/LauncherDbUtils.kt
new file mode 100644
index 0000000..3c68e46
--- /dev/null
+++ b/src/com/android/launcher3/provider/LauncherDbUtils.kt
@@ -0,0 +1,261 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.provider
+
+import android.content.ContentValues
+import android.content.Context
+import android.content.pm.ShortcutInfo
+import android.database.sqlite.SQLiteDatabase
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.graphics.drawable.Icon
+import android.os.PersistableBundle
+import android.os.Process
+import android.os.UserManager
+import android.text.TextUtils
+import com.android.launcher3.LauncherAppState
+import com.android.launcher3.LauncherSettings
+import com.android.launcher3.Utilities
+import com.android.launcher3.icons.IconCache
+import com.android.launcher3.model.LoaderCursor
+import com.android.launcher3.model.UserManagerState
+import com.android.launcher3.pm.PinRequestHelper
+import com.android.launcher3.pm.UserCache
+import com.android.launcher3.shortcuts.ShortcutKey
+import com.android.launcher3.util.IntArray
+import com.android.launcher3.util.IntSet
+import com.android.launcher3.util.PackageManagerHelper
+
+/** A set of utility methods for Launcher DB used for DB updates and migration. */
+object LauncherDbUtils {
+    /**
+     * Returns a string which can be used as a where clause for DB query to match the given itemId
+     */
+    @JvmStatic fun itemIdMatch(itemId: Int): String = "_id=$itemId"
+
+    @JvmStatic
+    fun queryIntArray(
+        distinct: Boolean,
+        db: SQLiteDatabase,
+        tableName: String,
+        columnName: String,
+        selection: String?,
+        groupBy: String?,
+        orderBy: String?,
+    ): IntArray {
+        val out = IntArray()
+        db.query(
+                distinct,
+                tableName,
+                arrayOf(columnName),
+                selection,
+                null,
+                groupBy,
+                null,
+                orderBy,
+                null,
+            )
+            .use { c ->
+                while (c.moveToNext()) {
+                    out.add(c.getInt(0))
+                }
+            }
+        return out
+    }
+
+    @JvmStatic
+    fun tableExists(db: SQLiteDatabase, tableName: String): Boolean =
+        db.query(
+                /* distinct = */ true,
+                /* table = */ "sqlite_master",
+                /* columns = */ arrayOf("tbl_name"),
+                /* selection = */ "tbl_name = ?",
+                /* selectionArgs = */ arrayOf(tableName),
+                /* groupBy = */ null,
+                /* having = */ null,
+                /* orderBy = */ null,
+                /* limit = */ null,
+                /* cancellationSignal = */ null,
+            )
+            .use { c ->
+                return c.count > 0
+            }
+
+    @JvmStatic
+    fun dropTable(db: SQLiteDatabase, tableName: String) =
+        db.execSQL("DROP TABLE IF EXISTS $tableName")
+
+    /** Copy fromTable in fromDb to toTable in toDb. */
+    @JvmStatic
+    fun copyTable(
+        fromDb: SQLiteDatabase,
+        fromTable: String,
+        toDb: SQLiteDatabase,
+        toTable: String,
+        context: Context,
+    ) {
+        val userSerial = UserCache.INSTANCE[context].getSerialNumberForUser(Process.myUserHandle())
+        dropTable(toDb, toTable)
+        LauncherSettings.Favorites.addTableToDb(toDb, userSerial, false, toTable)
+        if (fromDb != toDb) {
+            toDb.run {
+                execSQL("ATTACH DATABASE '${fromDb.path}' AS from_db")
+                execSQL(
+                    "INSERT INTO $toTable SELECT ${LauncherSettings.Favorites.getColumns(userSerial)} FROM from_db.$fromTable"
+                )
+                execSQL("DETACH DATABASE 'from_db'")
+            }
+        } else {
+            toDb.run {
+                execSQL(
+                    "INSERT INTO $toTable SELECT ${
+                        LauncherSettings.Favorites.getColumns(
+                            userSerial
+                        )
+                    } FROM $fromTable"
+                )
+            }
+        }
+    }
+
+    /**
+     * Migrates the legacy shortcuts to deep shortcuts pinned under Launcher. Removes any invalid
+     * shortcut or any shortcut which requires some permission to launch
+     */
+    @JvmStatic
+    fun migrateLegacyShortcuts(context: Context, db: SQLiteDatabase) {
+        val c =
+            db.query(
+                LauncherSettings.Favorites.TABLE_NAME,
+                null,
+                "itemType = 1",
+                null,
+                null,
+                null,
+                null,
+            )
+        val pmHelper = PackageManagerHelper.INSTANCE[context]
+        val ums = UserManagerState()
+        ums.run {
+            init(UserCache.INSTANCE[context], context.getSystemService(UserManager::class.java))
+        }
+        val lc = LoaderCursor(c, LauncherAppState.getInstance(context), ums, pmHelper, null)
+        val deletedShortcuts = IntSet()
+
+        while (lc.moveToNext()) {
+            if (lc.user !== Process.myUserHandle()) {
+                deletedShortcuts.add(lc.id)
+                continue
+            }
+            val intent = lc.parseIntent()
+            if (intent == null) {
+                deletedShortcuts.add(lc.id)
+                continue
+            }
+            if (TextUtils.isEmpty(lc.title)) {
+                deletedShortcuts.add(lc.id)
+                continue
+            }
+
+            // Make sure the target intent can be launched without any permissions. Otherwise remove
+            // the shortcut
+            val ri = context.packageManager.resolveActivity(intent, 0)
+            if (ri == null || !TextUtils.isEmpty(ri.activityInfo.permission)) {
+                deletedShortcuts.add(lc.id)
+                continue
+            }
+            val extras =
+                PersistableBundle().apply {
+                    putString(
+                        IconCache.EXTRA_SHORTCUT_BADGE_OVERRIDE_PACKAGE,
+                        ri.activityInfo.packageName,
+                    )
+                }
+            val infoBuilder =
+                ShortcutInfo.Builder(context, "migrated_shortcut-${lc.id}")
+                    .setIntent(intent)
+                    .setExtras(extras)
+                    .setShortLabel(lc.title)
+
+            var bitmap: Bitmap? = null
+            val iconData = lc.iconBlob
+            if (iconData != null) {
+                bitmap = BitmapFactory.decodeByteArray(iconData, 0, iconData.size)
+            }
+            if (bitmap != null) {
+                infoBuilder.setIcon(Icon.createWithBitmap(bitmap))
+            }
+
+            val info = infoBuilder.build()
+            try {
+                if (!PinRequestHelper.createRequestForShortcut(context, info).accept()) {
+                    deletedShortcuts.add(lc.id)
+                    continue
+                }
+            } catch (e: Exception) {
+                deletedShortcuts.add(lc.id)
+                continue
+            }
+            val update =
+                ContentValues().apply {
+                    put(
+                        LauncherSettings.Favorites.ITEM_TYPE,
+                        LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT,
+                    )
+                    put(
+                        LauncherSettings.Favorites.INTENT,
+                        ShortcutKey.makeIntent(info.id, context.packageName).toUri(0),
+                    )
+                }
+            db.update(
+                LauncherSettings.Favorites.TABLE_NAME,
+                update,
+                "_id = ?",
+                arrayOf(lc.id.toString()),
+            )
+        }
+        lc.close()
+        if (deletedShortcuts.isEmpty.not()) {
+            db.delete(
+                /* table = */ LauncherSettings.Favorites.TABLE_NAME,
+                /* whereClause = */ Utilities.createDbSelectionQuery(
+                    LauncherSettings.Favorites._ID,
+                    deletedShortcuts.array,
+                ),
+                /* whereArgs = */ null,
+            )
+        }
+
+        // Drop the unused columns
+        db.run {
+            execSQL("ALTER TABLE ${LauncherSettings.Favorites.TABLE_NAME} DROP COLUMN iconPackage;")
+            execSQL(
+                "ALTER TABLE ${LauncherSettings.Favorites.TABLE_NAME} DROP COLUMN iconResource;"
+            )
+        }
+    }
+
+    /** Utility class to simplify managing sqlite transactions */
+    class SQLiteTransaction(val db: SQLiteDatabase) : AutoCloseable {
+        init {
+            db.beginTransaction()
+        }
+
+        fun commit() = db.setTransactionSuccessful()
+
+        override fun close() = db.endTransaction()
+    }
+}
diff --git a/src/com/android/launcher3/provider/RestoreDbTask.java b/src/com/android/launcher3/provider/RestoreDbTask.java
index 21897bf..8db981f 100644
--- a/src/com/android/launcher3/provider/RestoreDbTask.java
+++ b/src/com/android/launcher3/provider/RestoreDbTask.java
@@ -51,7 +51,6 @@
 import androidx.annotation.VisibleForTesting;
 import androidx.annotation.WorkerThread;
 
-import com.android.launcher3.Flags;
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherFiles;
@@ -75,7 +74,6 @@
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.LogConfig;
 
-import java.io.File;
 import java.io.InvalidObjectException;
 import java.util.Arrays;
 import java.util.Collection;
@@ -125,17 +123,14 @@
         // executed again.
         LauncherPrefs.get(context).removeSync(RESTORE_DEVICE);
 
-        if (Flags.enableNarrowGridRestore()) {
-            String oldPhoneFileName = idp.dbFile;
-            List<String> previousDbs = existingDbs();
-            removeOldDBs(context, oldPhoneFileName);
-            // The idp before this contains data about the old phone, after this it becomes the idp
-            // of the current phone.
-            idp.reset(context);
-            trySettingPreviousGidAsCurrent(context, idp, oldPhoneFileName, previousDbs);
-        } else {
-            idp.reinitializeAfterRestore(context);
-        }
+        DeviceGridState deviceGridState = new DeviceGridState(context);
+        String oldPhoneFileName = deviceGridState.getDbFile();
+        List<String> previousDbs = existingDbs(context);
+        removeOldDBs(context, oldPhoneFileName);
+        // The idp before this contains data about the old phone, after this it becomes the idp
+        // of the current phone.
+        idp.reset(context);
+        trySettingPreviousGridAsCurrent(context, idp, oldPhoneFileName, previousDbs);
     }
 
 
@@ -143,12 +138,13 @@
      * Try setting the gird used in the previous phone to the new one. If the current device doesn't
      * support the previous grid option it will not be set.
      */
-    private static void trySettingPreviousGidAsCurrent(Context context, InvariantDeviceProfile idp,
+    private static void trySettingPreviousGridAsCurrent(Context context, InvariantDeviceProfile idp,
             String oldPhoneDbFileName, List<String> previousDbs) {
         InvariantDeviceProfile.GridOption oldPhoneGridOption = idp.getGridOptionFromFileName(
                 context, oldPhoneDbFileName);
         // The grid option could be null if current phone doesn't support the previous db.
         if (oldPhoneGridOption != null) {
+
             /* If the user only used the default db on the previous phone and the new default db is
              * bigger than or equal to the previous one, then keep the new default db */
             if (previousDbs.size() == 1 && oldPhoneGridOption.numColumns <= idp.numColumns
@@ -166,17 +162,19 @@
     /**
      * Returns a list of paths of the existing launcher dbs.
      */
-    private static List<String> existingDbs() {
+    @VisibleForTesting
+    public static List<String> existingDbs(Context context) {
         // At this point idp.dbFile contains the name of the dbFile from the previous phone
         return LauncherFiles.GRID_DB_FILES.stream()
-                .filter(dbName -> new File(dbName).exists())
+                .filter(dbName -> context.getDatabasePath(dbName).exists())
                 .toList();
     }
 
     /**
      * Only keep the last database used on the previous device.
      */
-    private static void removeOldDBs(Context context, String oldPhoneDbFileName) {
+    @VisibleForTesting
+    public static void removeOldDBs(Context context, String oldPhoneDbFileName) {
         // At this point idp.dbFile contains the name of the dbFile from the previous phone
         LauncherFiles.GRID_DB_FILES.stream()
                 .filter(dbName -> !dbName.equals(oldPhoneDbFileName))
diff --git a/src/com/android/launcher3/secondarydisplay/SecondaryDragLayer.java b/src/com/android/launcher3/secondarydisplay/SecondaryDragLayer.java
index 6e697d9..d5c87f4 100644
--- a/src/com/android/launcher3/secondarydisplay/SecondaryDragLayer.java
+++ b/src/com/android/launcher3/secondarydisplay/SecondaryDragLayer.java
@@ -65,7 +65,7 @@
     @Override
     public void recreateControllers() {
         mControllers = new TouchController[]{new CloseAllAppsTouchController(),
-                mActivity.getDragController()};
+                mContainer.getDragController()};
     }
 
     /**
@@ -79,10 +79,10 @@
         mAppsView = findViewById(R.id.apps_view);
         // Setup workspace
         mWorkspace = findViewById(R.id.workspace_grid);
-        mPinnedAppsAdapter = new PinnedAppsAdapter(mActivity, mAppsView.getAppsStore(),
+        mPinnedAppsAdapter = new PinnedAppsAdapter(mContainer, mAppsView.getAppsStore(),
                 this::onIconLongClicked);
         mWorkspace.setAdapter(mPinnedAppsAdapter);
-        mWorkspace.setNumColumns(mActivity.getDeviceProfile().inv.numColumns);
+        mWorkspace.setNumColumns(mContainer.getDeviceProfile().inv.numColumns);
     }
 
     /**
@@ -112,7 +112,7 @@
         int height = MeasureSpec.getSize(heightMeasureSpec);
         setMeasuredDimension(width, height);
 
-        DeviceProfile grid = mActivity.getDeviceProfile();
+        DeviceProfile grid = mContainer.getDeviceProfile();
         int count = getChildCount();
         for (int i = 0; i < count; i++) {
             final View child = getChildAt(i);
@@ -153,17 +153,17 @@
 
         @Override
         public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
-            if (!mActivity.isAppDrawerShown()) {
+            if (!mContainer.isAppDrawerShown()) {
                 return false;
             }
 
-            if (AbstractFloatingView.getTopOpenView(mActivity) != null) {
+            if (AbstractFloatingView.getTopOpenView(mContainer) != null) {
                 return false;
             }
 
             if (ev.getAction() == MotionEvent.ACTION_DOWN
-                    && !isEventOverView(mActivity.getAppsView(), ev)) {
-                mActivity.showAppDrawer(false);
+                    && !isEventOverView(mContainer.getAppsView(), ev)) {
+                mContainer.showAppDrawer(false);
                 return true;
             }
             return false;
@@ -178,7 +178,7 @@
         if (!(v instanceof BubbleTextView)) {
             return false;
         }
-        if (PopupContainerWithArrow.getOpen(mActivity) != null) {
+        if (PopupContainerWithArrow.getOpen(mContainer) != null) {
             // There is already an items container open, so don't open this one.
             v.clearFocus();
             return false;
@@ -187,32 +187,32 @@
         if (!ShortcutUtil.supportsShortcuts(item)) {
             return false;
         }
-        PopupDataProvider popupDataProvider = mActivity.getPopupDataProvider();
+        PopupDataProvider popupDataProvider = mContainer.getPopupDataProvider();
         if (popupDataProvider == null) {
             return false;
         }
 
         // order of this list will reflect in the popup
         List<SystemShortcut> systemShortcuts = new ArrayList<>();
-        systemShortcuts.add(APP_INFO.getShortcut(mActivity, item, v));
+        systemShortcuts.add(APP_INFO.getShortcut(mContainer, item, v));
         // Hide redundant pin shortcut for app drawer icons if drag-n-drop is enabled.
-        if (!FeatureFlags.SECONDARY_DRAG_N_DROP_TO_PIN.get() || !mActivity.isAppDrawerShown()) {
+        if (!FeatureFlags.SECONDARY_DRAG_N_DROP_TO_PIN.get() || !mContainer.isAppDrawerShown()) {
             systemShortcuts.add(mPinnedAppsAdapter.getSystemShortcut(item, v));
         }
         int deepShortcutCount = popupDataProvider.getShortcutCountForItem(item);
         final PopupContainerWithArrow<SecondaryDisplayLauncher> container;
-        container = (PopupContainerWithArrow) mActivity.getLayoutInflater().inflate(
-                R.layout.popup_container, mActivity.getDragLayer(), false);
+        container = (PopupContainerWithArrow) mContainer.getLayoutInflater().inflate(
+                R.layout.popup_container, mContainer.getDragLayer(), false);
         container.populateAndShowRows((BubbleTextView) v, deepShortcutCount,
                 systemShortcuts);
         container.requestFocus();
 
-        if (!FeatureFlags.SECONDARY_DRAG_N_DROP_TO_PIN.get() || !mActivity.isAppDrawerShown()) {
+        if (!FeatureFlags.SECONDARY_DRAG_N_DROP_TO_PIN.get() || !mContainer.isAppDrawerShown()) {
             return true;
         }
 
         DragOptions options = new DragOptions();
-        DeviceProfile grid = mActivity.getDeviceProfile();
+        DeviceProfile grid = mContainer.getDeviceProfile();
         options.intrinsicIconScaleFactor = (float) grid.allAppsIconSizePx / grid.iconSizePx;
         options.preDragCondition = container.createPreDragCondition(false);
         if (options.preDragCondition == null) {
@@ -229,7 +229,7 @@
                     mDragView = dragObject.dragView;
                     if (!shouldStartDrag(0)) {
                         mDragView.setOnScaleAnimEndCallback(() ->
-                                mActivity.beginDragShared(v, mActivity.getAppsView(), options));
+                                mContainer.beginDragShared(v, mContainer.getAppsView(), options));
                     }
                 }
 
@@ -239,7 +239,7 @@
                 }
             };
         }
-        mActivity.beginDragShared(v, mActivity.getAppsView(), options);
+        mContainer.beginDragShared(v, mContainer.getAppsView(), options);
         return true;
     }
 }
diff --git a/src/com/android/launcher3/settings/SettingsActivity.java b/src/com/android/launcher3/settings/SettingsActivity.java
index bd9298b..5851f62 100644
--- a/src/com/android/launcher3/settings/SettingsActivity.java
+++ b/src/com/android/launcher3/settings/SettingsActivity.java
@@ -26,6 +26,7 @@
 
 import android.app.Activity;
 import android.content.Intent;
+import android.content.pm.ActivityInfo;
 import android.net.Uri;
 import android.os.Bundle;
 import android.provider.Settings;
@@ -66,6 +67,8 @@
     @VisibleForTesting
     static final String DEVELOPER_OPTIONS_KEY = "pref_developer_options";
 
+    public static final String FIXED_LANDSCAPE_MODE = "pref_fixed_landscape_mode";
+
     private static final String NOTIFICATION_DOTS_PREFERENCE_KEY = "pref_icon_badging";
 
     public static final String EXTRA_FRAGMENT_ARGS = ":settings:fragment_args";
@@ -236,7 +239,7 @@
         /**
          * Finds the parent preference screen for the given target key.
          *
-         * @param parent the parent preference screen
+         * @param parent    the parent preference screen
          * @param targetKey the key of the preference to find
          * @return the parent preference screen that contains the target preference
          */
@@ -286,13 +289,14 @@
          * will remove that preference from the list.
          */
         protected boolean initPreference(Preference preference) {
+            DisplayController.Info info = DisplayController.INSTANCE.get(getContext()).getInfo();
             switch (preference.getKey()) {
                 case NOTIFICATION_DOTS_PREFERENCE_KEY:
                     return BuildConfig.NOTIFICATION_DOTS_ENABLED;
-
                 case ALLOW_ROTATION_PREFERENCE_KEY:
-                    DisplayController.Info info =
-                            DisplayController.INSTANCE.get(getContext()).getInfo();
+                    if (Flags.oneGridSpecs()) {
+                        return false;
+                    }
                     if (info.isTablet(info.realBounds)) {
                         // Launcher supports rotation by default. No need to show this setting.
                         return false;
@@ -300,14 +304,29 @@
                     // Initialize the UI once
                     preference.setDefaultValue(RotationHelper.getAllowRotationDefaultValue(info));
                     return true;
-
                 case DEVELOPER_OPTIONS_KEY:
                     if (IS_STUDIO_BUILD) {
                         preference.setOrder(0);
                     }
                     return mDeveloperOptionsEnabled;
+                case FIXED_LANDSCAPE_MODE:
+                    if (!Flags.oneGridSpecs()) {
+                        return false;
+                    }
+                    // When the setting changes rotate the screen accordingly to showcase the result
+                    // of the setting
+                    preference.setOnPreferenceChangeListener(
+                            (pref, newValue) -> {
+                                getActivity().setRequestedOrientation(
+                                        (boolean) newValue
+                                                ? ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
+                                                : ActivityInfo.SCREEN_ORIENTATION_USER
+                                );
+                                return true;
+                            }
+                    );
+                    return !info.isTablet(info.realBounds);
             }
-
             return true;
         }
 
diff --git a/tests/multivalentTests/src/com/android/launcher3/model/ModelTestRule.kt b/src/com/android/launcher3/shapes/AppShape.kt
similarity index 67%
rename from tests/multivalentTests/src/com/android/launcher3/model/ModelTestRule.kt
rename to src/com/android/launcher3/shapes/AppShape.kt
index ad2c2a4..68200a0 100644
--- a/tests/multivalentTests/src/com/android/launcher3/model/ModelTestRule.kt
+++ b/src/com/android/launcher3/shapes/AppShape.kt
@@ -14,14 +14,6 @@
  * limitations under the License.
  */
 
-package com.android.launcher3.model
+package com.android.launcher3.shapes
 
-import com.android.launcher3.util.RoboApiWrapper
-import org.junit.rules.TestWatcher
-import org.junit.runner.Description
-
-class ModelTestRule : TestWatcher() {
-    override fun starting(description: Description?) {
-        RoboApiWrapper.initialize()
-    }
-}
+class AppShape(val key: String, val title: String, val path: String)
diff --git a/src/com/android/launcher3/shapes/AppShapesProvider.kt b/src/com/android/launcher3/shapes/AppShapesProvider.kt
new file mode 100644
index 0000000..8c2f181
--- /dev/null
+++ b/src/com/android/launcher3/shapes/AppShapesProvider.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.launcher3.shapes
+
+import com.android.systemui.shared.Flags
+
+object AppShapesProvider {
+
+    val shapes =
+        if (Flags.newCustomizationPickerUi())
+            listOf(
+                AppShape(
+                    "arch",
+                    "arch",
+                    "M100 83.46C100 85.471 100 86.476 99.9 87.321 99.116 93.916 93.916 99.116 87.321 99.9 86.476 100 85.471 100 83.46 100H16.54C14.529 100 13.524 100 12.679 99.9 6.084 99.116.884 93.916.1 87.321 0 86.476 0 85.471 0 83.46L0 50C0 22.386 22.386 0 50 0 77.614 0 100 22.386 100 50V83.46Z",
+                ),
+                AppShape(
+                    "4_sided_cookie",
+                    "4 sided cookie",
+                    "M63.605 3C84.733-6.176 106.176 15.268 97 36.395L95.483 39.888C92.681 46.338 92.681 53.662 95.483 60.112L97 63.605C106.176 84.732 84.733 106.176 63.605 97L60.112 95.483C53.662 92.681 46.338 92.681 39.888 95.483L36.395 97C15.267 106.176-6.176 84.732 3 63.605L4.517 60.112C7.319 53.662 7.319 46.338 4.517 39.888L3 36.395C-6.176 15.268 15.267-6.176 36.395 3L39.888 4.517C46.338 7.319 53.662 7.319 60.112 4.517L63.605 3Z",
+                ),
+                AppShape(
+                    "seven_sided_cookie",
+                    "7 sided cookie",
+                    "M35.209 4.878C36.326 3.895 36.884 3.404 37.397 3.006 44.82-2.742 55.18-2.742 62.603 3.006 63.116 3.404 63.674 3.895 64.791 4.878 65.164 5.207 65.351 5.371 65.539 5.529 68.167 7.734 71.303 9.248 74.663 9.932 74.902 9.981 75.147 10.025 75.637 10.113 77.1 10.375 77.831 10.506 78.461 10.66 87.573 12.893 94.032 21.011 94.176 30.412 94.186 31.062 94.151 31.805 94.08 33.293 94.057 33.791 94.045 34.04 94.039 34.285 93.958 37.72 94.732 41.121 96.293 44.18 96.404 44.399 96.522 44.618 96.759 45.056 97.467 46.366 97.821 47.021 98.093 47.611 102.032 56.143 99.727 66.266 92.484 72.24 91.983 72.653 91.381 73.089 90.177 73.961 89.774 74.254 89.572 74.4 89.377 74.548 86.647 76.626 84.477 79.353 83.063 82.483 82.962 82.707 82.865 82.936 82.671 83.395 82.091 84.766 81.8 85.451 81.51 86.033 77.31 94.44 67.977 98.945 58.801 96.994 58.166 96.859 57.451 96.659 56.019 96.259 55.54 96.125 55.3 96.058 55.063 95.998 51.74 95.154 48.26 95.154 44.937 95.998 44.699 96.058 44.46 96.125 43.981 96.259 42.549 96.659 41.834 96.859 41.199 96.994 32.023 98.945 22.69 94.44 18.49 86.033 18.2 85.451 17.909 84.766 17.329 83.395 17.135 82.936 17.038 82.707 16.937 82.483 15.523 79.353 13.353 76.626 10.623 74.548 10.428 74.4 10.226 74.254 9.823 73.961 8.619 73.089 8.017 72.653 7.516 72.24.273 66.266-2.032 56.143 1.907 47.611 2.179 47.021 2.533 46.366 3.241 45.056 3.478 44.618 3.596 44.399 3.707 44.18 5.268 41.121 6.042 37.72 5.961 34.285 5.955 34.04 5.943 33.791 5.92 33.293 5.849 31.805 5.814 31.062 5.824 30.412 5.968 21.011 12.427 12.893 21.539 10.66 22.169 10.506 22.9 10.375 24.363 10.113 24.853 10.025 25.098 9.981 25.337 9.932 28.697 9.248 31.833 7.734 34.461 5.529 34.649 5.371 34.836 5.207 35.209 4.878Z",
+                ),
+                AppShape(
+                    "sunny",
+                    "sunny",
+                    "M42.846 4.873C46.084-.531 53.916-.531 57.154 4.873L60.796 10.951C62.685 14.103 66.414 15.647 69.978 14.754L76.851 13.032C82.962 11.5 88.5 17.038 86.968 23.149L85.246 30.022C84.353 33.586 85.897 37.315 89.049 39.204L95.127 42.846C100.531 46.084 100.531 53.916 95.127 57.154L89.049 60.796C85.897 62.685 84.353 66.414 85.246 69.978L86.968 76.851C88.5 82.962 82.962 88.5 76.851 86.968L69.978 85.246C66.414 84.353 62.685 85.898 60.796 89.049L57.154 95.127C53.916 100.531 46.084 100.531 42.846 95.127L39.204 89.049C37.315 85.898 33.586 84.353 30.022 85.246L23.149 86.968C17.038 88.5 11.5 82.962 13.032 76.851L14.754 69.978C15.647 66.414 14.103 62.685 10.951 60.796L4.873 57.154C-.531 53.916-.531 46.084 4.873 42.846L10.951 39.204C14.103 37.315 15.647 33.586 14.754 30.022L13.032 23.149C11.5 17.038 17.038 11.5 23.149 13.032L30.022 14.754C33.586 15.647 37.315 14.103 39.204 10.951L42.846 4.873Z",
+                ),
+                AppShape(
+                    "circle",
+                    "circle",
+                    "M99.18 50C99.18 77.162 77.162 99.18 50 99.18 22.838 99.18.82 77.162.82 50 .82 22.839 22.838.82 50 .82 77.162.82 99.18 22.839 99.18 50Z",
+                ),
+                AppShape(
+                    "square",
+                    "square",
+                    "M99.18 53.689C99.18 67.434 99.18 74.306 97.022 79.758 93.897 87.649 87.649 93.897 79.758 97.022 74.306 99.18 67.434 99.18 53.689 99.18H46.311C32.566 99.18 25.694 99.18 20.242 97.022 12.351 93.897 6.103 87.649 2.978 79.758.82 74.306.82 67.434.82 53.689L.82 46.311C.82 32.566.82 25.694 2.978 20.242 6.103 12.351 12.351 6.103 20.242 2.978 25.694.82 32.566.82 46.311.82L53.689.82C67.434.82 74.306.82 79.758 2.978 87.649 6.103 93.897 12.351 97.022 20.242 99.18 25.694 99.18 32.566 99.18 46.311V53.689Z\n",
+                ),
+            )
+        else emptyList()
+}
diff --git a/src/com/android/launcher3/statemanager/BaseState.java b/src/com/android/launcher3/statemanager/BaseState.java
index b81729a..f6b610c 100644
--- a/src/com/android/launcher3/statemanager/BaseState.java
+++ b/src/com/android/launcher3/statemanager/BaseState.java
@@ -72,6 +72,13 @@
     }
 
     /**
+     * For this state, whether fullscreen and desktop quickswitch carousel are detached.
+     */
+    default boolean detachDesktopCarousel() {
+        return true;
+    }
+
+    /**
      * For this state, whether member variables and other forms of data state should be preserved
      * or wiped when the state is reapplied. (See {@link StateManager#reapplyState()})
      */
diff --git a/src/com/android/launcher3/statemanager/StateManager.java b/src/com/android/launcher3/statemanager/StateManager.java
index 303290d..763f3ba 100644
--- a/src/com/android/launcher3/statemanager/StateManager.java
+++ b/src/com/android/launcher3/statemanager/StateManager.java
@@ -18,6 +18,7 @@
 
 import static android.animation.ValueAnimator.areAnimatorsEnabled;
 
+import static com.android.launcher3.Flags.enableStateManagerProtoLog;
 import static com.android.launcher3.anim.AnimatorPlaybackController.callListenerCommandRecursively;
 import static com.android.launcher3.states.StateAnimationConfig.HANDLE_STATE_APPLY;
 import static com.android.launcher3.states.StateAnimationConfig.SKIP_ALL_ANIMATIONS;
@@ -39,6 +40,7 @@
 import com.android.launcher3.states.StateAnimationConfig;
 import com.android.launcher3.states.StateAnimationConfig.AnimationFlags;
 import com.android.launcher3.states.StateAnimationConfig.AnimationPropertyFlags;
+import com.android.launcher3.util.StateManagerProtoLogProxy;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -243,7 +245,10 @@
 
     private void goToState(
             STATE_TYPE state, boolean animated, long delay, AnimatorListener listener) {
-        if (DEBUG) {
+        if (enableStateManagerProtoLog()) {
+            StateManagerProtoLogProxy.logGoToState(
+                    mState, state, getTrimmedStackTrace("StateManager.goToState"));
+        } else if (DEBUG) {
             Log.d(TAG, "goToState - fromState: " + mState + ", toState: " + state
                     + ", partial trace:\n" + getTrimmedStackTrace("StateManager.goToState"));
         }
@@ -331,7 +336,10 @@
      */
     public AnimatorSet createAtomicAnimation(
             STATE_TYPE fromState, STATE_TYPE toState, StateAnimationConfig config) {
-        if (DEBUG) {
+        if (enableStateManagerProtoLog()) {
+            StateManagerProtoLogProxy.logCreateAtomicAnimation(
+                    mState, toState, getTrimmedStackTrace("StateManager.createAtomicAnimation"));
+        } else if (DEBUG) {
             Log.d(TAG, "createAtomicAnimation - fromState: " + fromState + ", toState: " + toState
                     + ", partial trace:\n" + getTrimmedStackTrace(
                             "StateManager.createAtomicAnimation"));
@@ -408,7 +416,9 @@
         mState = state;
         mStatefulContainer.onStateSetStart(mState);
 
-        if (DEBUG) {
+        if (enableStateManagerProtoLog()) {
+            StateManagerProtoLogProxy.logOnStateTransitionStart(state);
+        } else if (DEBUG) {
             Log.d(TAG, "onStateTransitionStart - state: " + state);
         }
         for (int i = mListeners.size() - 1; i >= 0; i--) {
@@ -428,7 +438,9 @@
             setRestState(null);
         }
 
-        if (DEBUG) {
+        if (enableStateManagerProtoLog()) {
+            StateManagerProtoLogProxy.logOnStateTransitionEnd(state);
+        } else if (DEBUG) {
             Log.d(TAG, "onStateTransitionEnd - state: " + state);
         }
         for (int i = mListeners.size() - 1; i >= 0; i--) {
@@ -468,7 +480,11 @@
      * Cancels the current animation.
      */
     public void cancelAnimation() {
-        if (DEBUG && mConfig.currentAnimation != null) {
+        if (enableStateManagerProtoLog()) {
+            StateManagerProtoLogProxy.logCancelAnimation(
+                    mConfig.currentAnimation != null,
+                    getTrimmedStackTrace("StateManager.cancelAnimation"));
+        } else if (DEBUG && mConfig.currentAnimation != null) {
             Log.d(TAG, "cancelAnimation - with ongoing animation"
                     + ", partial trace:\n" + getTrimmedStackTrace("StateManager.cancelAnimation"));
         }
diff --git a/src/com/android/launcher3/statemanager/StatefulActivity.java b/src/com/android/launcher3/statemanager/StatefulActivity.java
index 28f2def..079191f 100644
--- a/src/com/android/launcher3/statemanager/StatefulActivity.java
+++ b/src/com/android/launcher3/statemanager/StatefulActivity.java
@@ -20,9 +20,11 @@
 
 import static com.android.launcher3.LauncherState.FLAG_NON_INTERACTIVE;
 
+import android.content.Context;
 import android.content.res.Configuration;
 import android.os.Bundle;
 import android.os.Handler;
+import android.os.Trace;
 import android.view.LayoutInflater;
 import android.view.View;
 
@@ -175,8 +177,10 @@
 
     @Override
     public void onConfigurationChanged(Configuration newConfig) {
+        Trace.beginSection("statefulActivity#onConfigurationChanged");
         handleConfigurationChanged(newConfig);
         super.onConfigurationChanged(newConfig);
+        Trace.endSection();
     }
 
     /**
@@ -195,15 +199,15 @@
         mOldRotation = rotation;
     }
 
+    @Override
+    public Context getContext() {
+        return this;
+    }
+
     /**
      * Logic for when device configuration changes (rotation, screen size change, multi-window,
      * etc.)
      */
     protected abstract void onHandleConfigurationChanged();
 
-    /**
-     * Enter staged split directly from the current running app.
-     * @param leftOrTop if the staged split will be positioned left or top.
-     */
-    public void enterStageSplitFromRunningApp(boolean leftOrTop) { }
 }
diff --git a/src/com/android/launcher3/statemanager/StatefulContainer.java b/src/com/android/launcher3/statemanager/StatefulContainer.java
index 0cf0a27..b10af0a 100644
--- a/src/com/android/launcher3/statemanager/StatefulContainer.java
+++ b/src/com/android/launcher3/statemanager/StatefulContainer.java
@@ -20,6 +20,10 @@
 import static com.android.launcher3.LauncherState.FLAG_CLOSE_POPUPS;
 import static com.android.launcher3.statemanager.BaseState.FLAG_NON_INTERACTIVE;
 
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.res.Configuration;
+
 import androidx.annotation.CallSuper;
 
 import com.android.launcher3.AbstractFloatingView;
@@ -36,6 +40,23 @@
         ActivityContext {
 
     /**
+     * Returns an instance of an implementation of StatefulContainer
+     *
+     * @param context will find instance of StatefulContainer from given context.
+     */
+    static <T extends StatefulContainer> T fromContext(Context context) {
+        if (context instanceof StatefulContainer) {
+            return (T) context;
+        } else if (context instanceof ContextWrapper) {
+            return fromContext(((ContextWrapper) context).getBaseContext());
+        } else {
+            throw new IllegalArgumentException("Cannot find StatefulContainer in parent tree");
+        }
+    }
+
+    Context getContext();
+
+    /**
      * Creates a factory for atomic state animations
      */
     default StateManager.AtomicAnimationFactory<STATE_TYPE> createAtomicAnimationFactory() {
@@ -54,12 +75,15 @@
 
     /**
      * Called when transition to state ends
+     *
      * @param state current state of State_Type
      */
-    default void onStateSetEnd(STATE_TYPE state) { }
+    default void onStateSetEnd(STATE_TYPE state) {
+    }
 
     /**
      * Called when transition to state starts
+     *
      * @param state current state of State_Type
      */
     @CallSuper
@@ -71,6 +95,7 @@
 
     /**
      * Returns true if the activity is in the provided state
+     *
      * @param state current state of State_Type
      */
     default boolean isInState(STATE_TYPE state) {
@@ -81,4 +106,8 @@
      * Returns true if state change should transition with animation
      */
     boolean shouldAnimateStateChange();
+
+    default void handleConfigurationChanged(Configuration configuration){
+        //no op
+    }
 }
diff --git a/src/com/android/launcher3/states/RotationHelper.java b/src/com/android/launcher3/states/RotationHelper.java
index fdb37f0..7d7ccd3 100644
--- a/src/com/android/launcher3/states/RotationHelper.java
+++ b/src/com/android/launcher3/states/RotationHelper.java
@@ -18,6 +18,7 @@
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LOCKED;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE;
 import static android.util.DisplayMetrics.DENSITY_DEVICE_STABLE;
 
 import static com.android.launcher3.LauncherPrefs.ALLOW_ROTATION;
@@ -26,8 +27,6 @@
 import static com.android.launcher3.util.window.WindowManagerProxy.MIN_TABLET_WIDTH;
 
 import android.content.Context;
-import android.content.SharedPreferences;
-import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
 import android.os.Handler;
 import android.os.Message;
 
@@ -36,13 +35,15 @@
 
 import com.android.launcher3.BaseActivity;
 import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.LauncherPrefChangeListener;
 import com.android.launcher3.LauncherPrefs;
+import com.android.launcher3.util.ContextTracker;
 import com.android.launcher3.util.DisplayController;
 
 /**
  * Utility class to manage launcher rotation
  */
-public class RotationHelper implements OnSharedPreferenceChangeListener,
+public class RotationHelper implements LauncherPrefChangeListener,
         DeviceProfile.OnDeviceProfileChangeListener,
         DisplayController.DisplayInfoChangeListener {
 
@@ -63,6 +64,8 @@
     public static final int REQUEST_ROTATE = 1;
     public static final int REQUEST_LOCK = 2;
 
+    private boolean mIsFixedLandscape = false;
+
     @NonNull
     private final BaseActivity mActivity;
     private final Handler mRequestOrientationHandler;
@@ -73,7 +76,7 @@
 
     /**
      * Rotation request made by
-     * {@link com.android.launcher3.util.ActivityTracker.SchedulerCallback}.
+     * {@link ContextTracker.SchedulerCallback}.
      * This supersedes any other request.
      */
     private int mStateHandlerRequest = REQUEST_NONE;
@@ -112,7 +115,7 @@
     }
 
     @Override
-    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String s) {
+    public void onPrefChanged(String s) {
         if (mDestroyed || mIgnoreAutoRotateSettings) return;
         boolean wasRotationEnabled = mHomeRotationEnabled;
         mHomeRotationEnabled = LauncherPrefs.get(mActivity).get(ALLOW_ROTATION);
@@ -163,6 +166,18 @@
         notifyChange();
     }
 
+    public boolean isFixedLandscape() {
+        return mIsFixedLandscape;
+    }
+
+    /**
+     * If fixedLandscape is true then the Launcher become landscape until set false..
+     */
+    public void setFixedLandscape(boolean fixedLandscape) {
+        mIsFixedLandscape = fixedLandscape;
+        notifyChange();
+    }
+
     // Used by tests only.
     public void forceAllowRotationForTesting(boolean allowRotation) {
         if (mDestroyed) return;
@@ -203,6 +218,8 @@
                     SCREEN_ORIENTATION_LOCKED : SCREEN_ORIENTATION_UNSPECIFIED;
         } else if (mCurrentStateRequest == REQUEST_LOCK) {
             activityFlags = SCREEN_ORIENTATION_LOCKED;
+        } else if (mIsFixedLandscape) {
+            activityFlags = SCREEN_ORIENTATION_USER_LANDSCAPE;
         } else if (mIgnoreAutoRotateSettings || mCurrentStateRequest == REQUEST_ROTATE
                 || mHomeRotationEnabled || mForceAllowRotationForTesting) {
             activityFlags = SCREEN_ORIENTATION_UNSPECIFIED;
diff --git a/src/com/android/launcher3/states/StateAnimationConfig.java b/src/com/android/launcher3/states/StateAnimationConfig.java
index 0ca5afd..2ffbbf4 100644
--- a/src/com/android/launcher3/states/StateAnimationConfig.java
+++ b/src/com/android/launcher3/states/StateAnimationConfig.java
@@ -77,7 +77,6 @@
             ANIM_WORKSPACE_PAGE_TRANSLATE_X,
             ANIM_OVERVIEW_SPLIT_SELECT_FLOATING_TASK_TRANSLATE_OFFSCREEN,
             ANIM_OVERVIEW_SPLIT_SELECT_INSTRUCTIONS_FADE,
-            ANIM_ALL_APPS_BOTTOM_SHEET_FADE,
             ANIM_ALL_APPS_KEYBOARD_FADE
     })
     @Retention(RetentionPolicy.SOURCE)
@@ -101,8 +100,7 @@
     public static final int ANIM_WORKSPACE_PAGE_TRANSLATE_X = 15;
     public static final int ANIM_OVERVIEW_SPLIT_SELECT_FLOATING_TASK_TRANSLATE_OFFSCREEN = 17;
     public static final int ANIM_OVERVIEW_SPLIT_SELECT_INSTRUCTIONS_FADE = 18;
-    public static final int ANIM_ALL_APPS_BOTTOM_SHEET_FADE = 19;
-    public static final int ANIM_ALL_APPS_KEYBOARD_FADE = 20;
+    public static final int ANIM_ALL_APPS_KEYBOARD_FADE = 19;
 
     private static final int ANIM_TYPES_COUNT = 21;
 
diff --git a/src/com/android/launcher3/testing/TestInformationHandler.java b/src/com/android/launcher3/testing/TestInformationHandler.java
index 6d9b891..aa3f2f2 100644
--- a/src/com/android/launcher3/testing/TestInformationHandler.java
+++ b/src/com/android/launcher3/testing/TestInformationHandler.java
@@ -168,21 +168,14 @@
             }
 
             case TestProtocol.REQUEST_TARGET_INSETS: {
-                return getUIProperty(Bundle::putParcelable, activity -> {
-                    WindowInsets insets = activity.getWindow()
-                            .getDecorView().getRootWindowInsets();
-                    return Insets.max(
-                            insets.getSystemGestureInsets(),
-                            insets.getSystemWindowInsets());
-                }, this::getCurrentActivity);
+                return getUIProperty(Bundle::putParcelable, insets -> Insets.max(
+                        insets.getSystemGestureInsets(),
+                        insets.getSystemWindowInsets()), this::getWindowInsets);
             }
 
             case TestProtocol.REQUEST_WINDOW_INSETS: {
-                return getUIProperty(Bundle::putParcelable, activity -> {
-                    WindowInsets insets = activity.getWindow()
-                            .getDecorView().getRootWindowInsets();
-                    return insets.getSystemWindowInsets();
-                }, this::getCurrentActivity);
+                return getUIProperty(Bundle::putParcelable,
+                        WindowInsets::getSystemWindowInsets, this::getWindowInsets);
             }
 
             case TestProtocol.REQUEST_CELL_LAYOUT_BOARDER_HEIGHT: {
@@ -192,13 +185,13 @@
             }
 
             case TestProtocol.REQUEST_SYSTEM_GESTURE_REGION: {
-                return getUIProperty(Bundle::putParcelable, activity -> {
-                    WindowInsetsCompat insets = WindowInsetsCompat.toWindowInsetsCompat(
-                            activity.getWindow().getDecorView().getRootWindowInsets());
+                return getUIProperty(Bundle::putParcelable, windowInsets -> {
+                    WindowInsetsCompat insets =
+                            WindowInsetsCompat.toWindowInsetsCompat(windowInsets);
                     return insets.getInsets(WindowInsetsCompat.Type.ime()
                             | WindowInsetsCompat.Type.systemGestures())
                             .toPlatformInsets();
-                }, this::getCurrentActivity);
+                }, this::getWindowInsets);
             }
 
             case TestProtocol.REQUEST_ICON_HEIGHT: {
@@ -253,12 +246,12 @@
 
             case TestProtocol.REQUEST_GET_SPLIT_SELECTION_ACTIVE:
                 response.putBoolean(TEST_INFO_RESPONSE_FIELD, enableSplitContextually()
-                        && Launcher.ACTIVITY_TRACKER.getCreatedActivity().isSplitSelectionActive());
+                        && Launcher.ACTIVITY_TRACKER.getCreatedContext().isSplitSelectionActive());
                 return response;
 
             case TestProtocol.REQUEST_ENABLE_ROTATION:
                 MAIN_EXECUTOR.submit(() ->
-                        Launcher.ACTIVITY_TRACKER.getCreatedActivity().getRotationHelper()
+                        Launcher.ACTIVITY_TRACKER.getCreatedContext().getRotationHelper()
                                 .forceAllowRotationForTesting(Boolean.parseBoolean(arg)));
                 return response;
 
@@ -482,12 +475,13 @@
     }
 
     protected boolean isLauncherInitialized() {
-        return Launcher.ACTIVITY_TRACKER.getCreatedActivity() == null
+        return Launcher.ACTIVITY_TRACKER.getCreatedContext() == null
                 || LauncherAppState.getInstance(mContext).getModel().isModelLoaded();
     }
 
-    protected Activity getCurrentActivity() {
-        return Launcher.ACTIVITY_TRACKER.getCreatedActivity();
+    protected WindowInsets getWindowInsets(){
+        return Launcher.ACTIVITY_TRACKER.getCreatedContext().getWindow().getDecorView()
+                .getRootWindowInsets();
     }
 
     /**
@@ -495,7 +489,7 @@
      */
     public static <T> Bundle getLauncherUIProperty(
             BundleSetter<T> bundleSetter, Function<Launcher, T> provider) {
-        return getUIProperty(bundleSetter, provider, Launcher.ACTIVITY_TRACKER::getCreatedActivity);
+        return getUIProperty(bundleSetter, provider, Launcher.ACTIVITY_TRACKER::getCreatedContext);
     }
 
     /**
diff --git a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
index efd1f0d..74a0966 100644
--- a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
+++ b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
@@ -407,10 +407,7 @@
     }
 
     private void updateContextualEduStats(LauncherState targetState) {
-        if (targetState == NORMAL) {
-            ContextualEduStatsManager.INSTANCE.get(
-                    mLauncher).updateEduStats(mDetector.isTrackpadGesture(), GestureType.HOME);
-        } else if (targetState == OVERVIEW) {
+        if (targetState == OVERVIEW) {
             ContextualEduStatsManager.INSTANCE.get(
                     mLauncher).updateEduStats(mDetector.isTrackpadGesture(), GestureType.OVERVIEW);
         } else if (targetState == ALL_APPS && !mDetector.isTrackpadGesture()) {
diff --git a/src/com/android/launcher3/touch/AllAppsSwipeController.java b/src/com/android/launcher3/touch/AllAppsSwipeController.java
index 9dcdf22..107bcc1 100644
--- a/src/com/android/launcher3/touch/AllAppsSwipeController.java
+++ b/src/com/android/launcher3/touch/AllAppsSwipeController.java
@@ -198,7 +198,7 @@
      * Applies Animation config values for transition from all apps to home.
      */
     public static void applyAllAppsToNormalConfig(Launcher launcher, StateAnimationConfig config) {
-        if (launcher.getDeviceProfile().isTablet) {
+        if (launcher.getDeviceProfile().shouldShowAllAppsOnSheet()) {
             config.setInterpolator(ANIM_SCRIM_FADE,
                     Interpolators.reverse(ALL_APPS_SCRIM_RESPONDER));
             config.setInterpolator(ANIM_ALL_APPS_FADE, FINAL_FRAME);
@@ -240,7 +240,7 @@
      */
     public static void applyNormalToAllAppsAnimConfig(
             Launcher launcher, StateAnimationConfig config) {
-        if (launcher.getDeviceProfile().isTablet) {
+        if (launcher.getDeviceProfile().shouldShowAllAppsOnSheet()) {
             config.setInterpolator(ANIM_ALL_APPS_FADE, INSTANT);
             config.setInterpolator(ANIM_SCRIM_FADE, ALL_APPS_SCRIM_RESPONDER);
             if (!config.isUserControlled()) {
diff --git a/src/com/android/launcher3/util/ActivityTracker.java b/src/com/android/launcher3/util/ActivityTracker.java
deleted file mode 100644
index b2d0d75..0000000
--- a/src/com/android/launcher3/util/ActivityTracker.java
+++ /dev/null
@@ -1,117 +0,0 @@
-/*
- * Copyright (C) 2019 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.util.Log;
-
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.BaseActivity;
-
-import java.io.PrintWriter;
-import java.lang.ref.WeakReference;
-import java.util.concurrent.CopyOnWriteArrayList;
-
-/**
- * Helper class to statically track activity creation
- * @param <T> The activity type to track
- */
-public final class ActivityTracker<T extends BaseActivity> {
-
-    private static final String TAG = "ActivityTracker";
-
-    private WeakReference<T> mCurrentActivity = new WeakReference<>(null);
-    private CopyOnWriteArrayList<SchedulerCallback<T>> mCallbacks = new CopyOnWriteArrayList<>();
-
-    @Nullable
-    public <R extends T> R getCreatedActivity() {
-        return (R) mCurrentActivity.get();
-    }
-
-    public void onActivityDestroyed(T activity) {
-        if (mCurrentActivity.get() == activity) {
-            mCurrentActivity.clear();
-        }
-    }
-
-    /**
-     * Call {@link SchedulerCallback#init(BaseActivity, boolean)} when the
-     * activity is ready. If the activity is already created, this is called immediately.
-     *
-     * The tracker maintains a strong ref to the callback, so it is up to the caller to return
-     * {@code false} in the callback OR to unregister the callback explicitly.
-     *
-     * @param callback The callback to call init() on when the activity is ready.
-     */
-    public void registerCallback(SchedulerCallback<T> callback, String reasonString) {
-        Log.d(TAG, "Registering callback: " + callback + ", reason=" + reasonString);
-        T activity = mCurrentActivity.get();
-        mCallbacks.add(callback);
-        if (activity != null) {
-            if (!callback.init(activity, activity.isStarted())) {
-                unregisterCallback(callback, "ActivityTracker.registerCallback: Intent handled");
-            }
-        }
-    }
-
-    /**
-     * Unregisters a registered callback.
-     */
-    public void unregisterCallback(SchedulerCallback<T> callback, String reasonString) {
-        Log.d(TAG, "Unregistering callback: " + callback + ", reason=" + reasonString);
-        mCallbacks.remove(callback);
-    }
-
-    public boolean handleCreate(T activity) {
-        mCurrentActivity = new WeakReference<>(activity);
-        return handleIntent(activity, false /* alreadyOnHome */);
-    }
-
-    public boolean handleNewIntent(T activity) {
-        return handleIntent(activity, activity.isStarted());
-    }
-
-    private boolean handleIntent(T activity, boolean alreadyOnHome) {
-        boolean handled = false;
-        if (!mCallbacks.isEmpty()) {
-            Log.d(TAG, "handleIntent: mCallbacks=" + mCallbacks);
-        }
-        for (SchedulerCallback<T> cb : mCallbacks) {
-            if (!cb.init(activity, alreadyOnHome)) {
-                // Callback doesn't want any more updates
-                unregisterCallback(cb, "ActivityTracker.handleIntent: Intent handled");
-            }
-            handled = true;
-        }
-        return handled;
-    }
-
-    public void dump(String prefix, PrintWriter writer) {
-        writer.println(prefix + "ActivityTracker:");
-        writer.println(prefix + "\tmCurrentActivity=" + mCurrentActivity.get());
-        writer.println(prefix + "\tmCallbacks=" + mCallbacks);
-    }
-
-    public interface SchedulerCallback<T extends BaseActivity> {
-
-        /**
-         * Called when the activity is ready.
-         * @param alreadyOnHome Whether the activity is already started.
-         * @return Whether to continue receiving callbacks (i.e. if the activity is recreated).
-         */
-        boolean init(T activity, boolean alreadyOnHome);
-    }
-}
diff --git a/src/com/android/launcher3/util/ApiWrapper.java b/src/com/android/launcher3/util/ApiWrapper.java
index 095518c..35fc9f5 100644
--- a/src/com/android/launcher3/util/ApiWrapper.java
+++ b/src/com/android/launcher3/util/ApiWrapper.java
@@ -16,44 +16,58 @@
 
 package com.android.launcher3.util;
 
-import static com.android.launcher3.LauncherConstants.ActivityCodes.REQUEST_HOME_ROLE;
-import static com.android.launcher3.util.MainThreadInitializedObject.forOverride;
+import static android.multiuser.Flags.addLauncherUserConfig;
 
+import static com.android.launcher3.LauncherConstants.ActivityCodes.REQUEST_HOME_ROLE;
+
+import android.annotation.SuppressLint;
 import android.app.ActivityOptions;
 import android.app.Person;
 import android.app.role.RoleManager;
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.ApplicationInfo;
 import android.content.pm.LauncherActivityInfo;
+import android.content.pm.LauncherApps;
+import android.content.pm.LauncherUserInfo;
 import android.content.pm.ShortcutInfo;
 import android.graphics.drawable.ColorDrawable;
 import android.net.Uri;
+import android.os.Build;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.util.ArrayMap;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.BuildConfig;
 import com.android.launcher3.Launcher;
-import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.dagger.ApplicationContext;
+import com.android.launcher3.dagger.LauncherAppComponent;
+import com.android.launcher3.dagger.LauncherAppSingleton;
 
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
+
+import javax.inject.Inject;
 
 /**
  * A wrapper for the hidden API calls
  */
-public class ApiWrapper implements ResourceBasedOverride, SafeCloseable {
+@LauncherAppSingleton
+public class ApiWrapper {
 
-    public static final MainThreadInitializedObject<ApiWrapper> INSTANCE =
-            forOverride(ApiWrapper.class, R.string.api_wrapper_class);
+    public static final DaggerSingletonObject<ApiWrapper> INSTANCE = new DaggerSingletonObject<>(
+            LauncherAppComponent::getApiWrapper);
 
     protected final Context mContext;
 
-    public ApiWrapper(Context context) {
+    @Inject
+    public ApiWrapper(@ApplicationContext Context context) {
         mContext = context;
     }
 
@@ -78,22 +92,32 @@
     /**
      * Returns a map of all users on the device to their corresponding UI properties
      */
+    @SuppressLint("NewApi")
     public Map<UserHandle, UserIconInfo> queryAllUsers() {
         UserManager um = mContext.getSystemService(UserManager.class);
         Map<UserHandle, UserIconInfo> users = new ArrayMap<>();
         List<UserHandle> usersActual = um.getUserProfiles();
         if (usersActual != null) {
             for (UserHandle user : usersActual) {
-                long serial = um.getSerialNumberForUser(user);
-
-                // Simple check to check if the provided user is work profile
-                // TODO: Migrate to a better platform API
                 NoopDrawable d = new NoopDrawable();
                 boolean isWork = (d != mContext.getPackageManager().getUserBadgedIcon(d, user));
-                UserIconInfo info = new UserIconInfo(
-                        user,
-                        isWork ? UserIconInfo.TYPE_WORK : UserIconInfo.TYPE_MAIN,
-                        serial);
+                UserIconInfo info;
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+                    LauncherUserInfo userInfo = Objects.requireNonNull(
+                            mContext.getSystemService(LauncherApps.class)).getLauncherUserInfo(
+                            user);
+                    long serial = Objects.requireNonNull(userInfo).getUserSerialNumber();
+                    info = addLauncherUserConfig() ? new UserIconInfo(user,
+                            isWork ? UserIconInfo.TYPE_WORK : UserIconInfo.TYPE_MAIN, serial,
+                            userInfo.getUserConfig()) : new UserIconInfo(user,
+                            isWork ? UserIconInfo.TYPE_WORK : UserIconInfo.TYPE_MAIN, serial);
+                } else {
+                    long serial = um.getSerialNumberForUser(user);
+                    // Simple check to check if the provided user is work profile
+                    // TODO: Migrate to a better platform API
+                    info = new UserIconInfo(user,
+                            isWork ? UserIconInfo.TYPE_WORK : UserIconInfo.TYPE_MAIN, serial);
+                }
                 users.put(user, info);
             }
         }
@@ -156,8 +180,13 @@
         }
     }
 
-    @Override
-    public void close() { }
+    /**
+     * Returns a hash to uniquely identify a particular version of appInfo
+     */
+    public String getApplicationInfoHash(@NonNull ApplicationInfo appInfo) {
+        // The hashString in source dir changes with every install
+        return appInfo.sourceDir;
+    }
 
     private static class NoopDrawable extends ColorDrawable {
         @Override
diff --git a/src/com/android/launcher3/util/ApplicationInfoWrapper.kt b/src/com/android/launcher3/util/ApplicationInfoWrapper.kt
new file mode 100644
index 0000000..e75b3bc
--- /dev/null
+++ b/src/com/android/launcher3/util/ApplicationInfoWrapper.kt
@@ -0,0 +1,115 @@
+/*
+ * 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.pm.ApplicationInfo
+import android.content.pm.ApplicationInfo.FLAG_EXTERNAL_STORAGE
+import android.content.pm.ApplicationInfo.FLAG_INSTALLED
+import android.content.pm.ApplicationInfo.FLAG_SUSPENDED
+import android.content.pm.ApplicationInfo.FLAG_SYSTEM
+import android.content.pm.LauncherApps
+import android.content.pm.PackageManager
+import android.content.pm.PackageManager.NameNotFoundException
+import android.os.UserHandle
+import com.android.launcher3.Flags.enableSupportForArchiving
+import com.android.launcher3.Utilities.ATLEAST_V
+import kotlin.LazyThreadSafetyMode.NONE
+
+/**
+ * A set of utility methods around ApplicationInfo with support for fetching the actual info lazily
+ */
+class ApplicationInfoWrapper private constructor(provider: () -> ApplicationInfo?) {
+
+    constructor(appInfo: ApplicationInfo?) : this({ appInfo })
+
+    constructor(
+        ctx: Context,
+        pkg: String,
+        user: UserHandle,
+    ) : this({
+        try {
+            ctx.getSystemService(LauncherApps::class.java)
+                ?.getApplicationInfo(pkg, PackageManager.MATCH_UNINSTALLED_PACKAGES, user)
+                ?.let { ai ->
+                    // its enabled and (either installed or archived)
+                    if (
+                        ai.enabled &&
+                            (ai.flags.and(FLAG_INSTALLED) != 0 ||
+                                (ATLEAST_V && enableSupportForArchiving() && ai.isArchived))
+                    ) {
+                        ai
+                    } else {
+                        null
+                    }
+                }
+        } catch (e: NameNotFoundException) {
+            null
+        }
+    })
+
+    constructor(
+        ctx: Context,
+        intent: Intent,
+    ) : this(
+        provider@{
+            try {
+                val pm = ctx.packageManager
+                val packageName: String =
+                    intent.component?.packageName
+                        ?: intent.getPackage()
+                        ?: return@provider pm.resolveActivity(
+                                intent,
+                                PackageManager.MATCH_DEFAULT_ONLY,
+                            )
+                            ?.activityInfo
+                            ?.applicationInfo
+                pm.getApplicationInfo(packageName, 0)
+            } catch (e: NameNotFoundException) {
+                null
+            }
+        }
+    )
+
+    private val appInfo: ApplicationInfo? by lazy(NONE, provider)
+
+    private fun hasFlag(flag: Int) = appInfo?.let { it.flags.and(flag) != 0 } ?: false
+
+    /**
+     * Returns true if the app can possibly be on the SDCard. This is just a workaround and doesn't
+     * guarantee that the app is on SD card.
+     */
+    fun isOnSdCard() = hasFlag(FLAG_EXTERNAL_STORAGE)
+
+    /** Returns whether the target app is installed for a given user */
+    fun isInstalled() = hasFlag(FLAG_INSTALLED)
+
+    /**
+     * Returns whether the target app is suspended for a given user as per
+     * [android.app.admin.DevicePolicyManager.isPackageSuspended].
+     */
+    fun isSuspended() = hasFlag(FLAG_INSTALLED) && hasFlag(FLAG_SUSPENDED)
+
+    /** Returns whether the target app is archived for a given user */
+    fun isArchived() = ATLEAST_V && enableSupportForArchiving() && appInfo?.isArchived ?: false
+
+    /** Returns whether the target app is a system app */
+    fun isSystem() = hasFlag(FLAG_SYSTEM)
+
+    fun getInfo(): ApplicationInfo? = appInfo
+}
diff --git a/src/com/android/launcher3/util/ContextTracker.java b/src/com/android/launcher3/util/ContextTracker.java
new file mode 100644
index 0000000..c729b4b
--- /dev/null
+++ b/src/com/android/launcher3/util/ContextTracker.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2019 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.util.Log;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.BaseActivity;
+import com.android.launcher3.views.ActivityContext;
+
+import java.io.PrintWriter;
+import java.lang.ref.WeakReference;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * Helper class to statically track activity creation
+ * @param <CONTEXT> The context type to track
+ */
+public abstract class ContextTracker<CONTEXT extends ActivityContext> {
+
+    private static final String TAG = "ContextTracker";
+
+    private WeakReference<CONTEXT> mCurrentContext = new WeakReference<>(null);
+    private CopyOnWriteArrayList<SchedulerCallback<CONTEXT>> mCallbacks =
+            new CopyOnWriteArrayList<>();
+
+    @Nullable
+    public <R extends CONTEXT> R getCreatedContext() {
+        return (R) mCurrentContext.get();
+    }
+
+    public void onContextDestroyed(CONTEXT context) {
+        if (mCurrentContext.get() == context) {
+            mCurrentContext.clear();
+        }
+    }
+
+    public abstract boolean isHomeStarted(CONTEXT context);
+
+    /**
+     * Call {@link SchedulerCallback#init(ActivityContext, boolean)} when the
+     * context is ready. If the context is already created, this is called immediately.
+     *
+     * The tracker maintains a strong ref to the callback, so it is up to the caller to return
+     * {@code false} in the callback OR to unregister the callback explicitly.
+     *
+     * @param callback The callback to call init() on when the context is ready.
+     */
+    public void registerCallback(SchedulerCallback<CONTEXT> callback, String reasonString) {
+        Log.d(TAG, "Registering callback: " + callback + ", reason=" + reasonString);
+        CONTEXT context = mCurrentContext.get();
+        mCallbacks.add(callback);
+        if (context != null) {
+            if (!callback.init(context, isHomeStarted(context))) {
+                unregisterCallback(callback, "ContextTracker.registerCallback: Intent handled");
+            }
+        }
+    }
+
+    /**
+     * Unregisters a registered callback.
+     */
+    public void unregisterCallback(SchedulerCallback<CONTEXT> callback, String reasonString) {
+        Log.d(TAG, "Unregistering callback: " + callback + ", reason=" + reasonString);
+        mCallbacks.remove(callback);
+    }
+
+    public boolean handleCreate(CONTEXT context) {
+        mCurrentContext = new WeakReference<>(context);
+        return handleCreate(context, /* alreadyOnHome= */ false);
+    }
+
+    public boolean handleNewIntent(CONTEXT context) {
+        return handleCreate(context, isHomeStarted(context));
+    }
+
+    private boolean handleCreate(CONTEXT context, boolean isHomeStarted) {
+        boolean handled = false;
+        if (!mCallbacks.isEmpty()) {
+            Log.d(TAG, "handleIntent: mCallbacks=" + mCallbacks);
+        }
+        for (SchedulerCallback<CONTEXT> cb : mCallbacks) {
+            if (!cb.init(context, isHomeStarted)) {
+                // Callback doesn't want any more updates
+                unregisterCallback(cb, "ContextTracker.handleIntent: Intent handled");
+            }
+            handled = true;
+        }
+        return handled;
+    }
+
+    public void dump(String prefix, PrintWriter writer) {
+        writer.println(prefix + "ContextTracker:");
+        writer.println(prefix + "\tmCurrentContext=" + mCurrentContext.get());
+        writer.println(prefix + "\tmCallbacks=" + mCallbacks);
+    }
+
+    public interface SchedulerCallback<T extends ActivityContext> {
+
+        /**
+         * Called when the context is ready.
+         * @param isHomeStarted Whether the home activity is already started.
+         * @return Whether to continue receiving callbacks (i.e. if the context is recreated).
+         */
+        boolean init(T context, boolean isHomeStarted);
+    }
+
+    public static final class ActivityTracker<T extends BaseActivity> extends ContextTracker<T> {
+
+        @Override
+        public boolean isHomeStarted(T context) {
+            return context.isStarted();
+        }
+    }
+}
diff --git a/src/com/android/launcher3/util/DaggerSingletonObject.java b/src/com/android/launcher3/util/DaggerSingletonObject.java
index b8cf2ae..febe6af 100644
--- a/src/com/android/launcher3/util/DaggerSingletonObject.java
+++ b/src/com/android/launcher3/util/DaggerSingletonObject.java
@@ -29,7 +29,7 @@
  * We should delete this class at the end and use @Inject to get dagger provided singletons.
  */
 
-public class DaggerSingletonObject<T extends SafeCloseable> {
+public class DaggerSingletonObject<T> {
     private final Function<LauncherAppComponent, T> mFunction;
 
     public DaggerSingletonObject(Function<LauncherAppComponent, T> function) {
diff --git a/src/com/android/launcher3/util/DaggerSingletonTracker.java b/src/com/android/launcher3/util/DaggerSingletonTracker.java
index 2946da1..b7a88db 100644
--- a/src/com/android/launcher3/util/DaggerSingletonTracker.java
+++ b/src/com/android/launcher3/util/DaggerSingletonTracker.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3.util;
 
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+
 import com.android.launcher3.dagger.LauncherAppSingleton;
 
 import java.util.ArrayList;
@@ -31,7 +33,9 @@
 @LauncherAppSingleton
 public class DaggerSingletonTracker implements SafeCloseable {
 
-    private final ArrayList<SafeCloseable> mLauncherAppSingletons = new ArrayList<>();
+    private final ArrayList<SafeCloseable> mCloseables = new ArrayList<>();
+
+    private boolean mClosed = false;
 
     @Inject
     DaggerSingletonTracker() {
@@ -44,14 +48,21 @@
      * {@link MainThreadInitializedObject.SandboxContext#onDestroy()}
      */
     public void addCloseable(SafeCloseable closeable) {
-        mLauncherAppSingletons.add(closeable);
+        MAIN_EXECUTOR.execute(() -> {
+            if (mClosed) {
+                closeable.close();
+            } else {
+                mCloseables.add(closeable);
+            }
+        });
     }
 
     @Override
     public void close() {
+        mClosed = true;
         // Destroy in reverse order
-        for (int i = mLauncherAppSingletons.size() - 1; i >= 0; i--) {
-            mLauncherAppSingletons.get(i).close();
+        for (int i = mCloseables.size() - 1; i >= 0; i--) {
+            mCloseables.get(i).close();
         }
     }
 }
diff --git a/src/com/android/launcher3/util/DisplayController.java b/src/com/android/launcher3/util/DisplayController.java
index c59cc81..26912eb 100644
--- a/src/com/android/launcher3/util/DisplayController.java
+++ b/src/com/android/launcher3/util/DisplayController.java
@@ -35,7 +35,6 @@
 import android.content.ComponentCallbacks;
 import android.content.Context;
 import android.content.Intent;
-import android.content.SharedPreferences;
 import android.content.res.Configuration;
 import android.graphics.Point;
 import android.graphics.Rect;
@@ -51,6 +50,7 @@
 import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.InvariantDeviceProfile.DeviceType;
+import com.android.launcher3.LauncherPrefChangeListener;
 import com.android.launcher3.LauncherPrefs;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.logging.FileLog;
@@ -116,8 +116,7 @@
     private Info mInfo;
     private boolean mDestroyed = false;
 
-    private SharedPreferences.OnSharedPreferenceChangeListener
-            mTaskbarPinningPreferenceChangeListener;
+    private LauncherPrefChangeListener mTaskbarPinningPreferenceChangeListener;
 
     @VisibleForTesting
     protected DisplayController(Context context) {
@@ -142,19 +141,18 @@
     }
 
     private void attachTaskbarPinningSharedPreferenceChangeListener(Context context) {
-        mTaskbarPinningPreferenceChangeListener =
-                (sharedPreferences, key) -> {
-                    LauncherPrefs prefs = LauncherPrefs.get(mContext);
-                    boolean isTaskbarPinningChanged = TASKBAR_PINNING_KEY.equals(key)
-                            && mInfo.mIsTaskbarPinned != prefs.get(TASKBAR_PINNING);
-                    boolean isTaskbarPinningDesktopModeChanged =
-                            TASKBAR_PINNING_DESKTOP_MODE_KEY.equals(key)
-                                    && mInfo.mIsTaskbarPinnedInDesktopMode != prefs.get(
-                                    TASKBAR_PINNING_IN_DESKTOP_MODE);
-                    if (isTaskbarPinningChanged || isTaskbarPinningDesktopModeChanged) {
-                        notifyConfigChange();
-                    }
-                };
+        mTaskbarPinningPreferenceChangeListener = key -> {
+            LauncherPrefs prefs = LauncherPrefs.get(mContext);
+            boolean isTaskbarPinningChanged = TASKBAR_PINNING_KEY.equals(key)
+                    && mInfo.mIsTaskbarPinned != prefs.get(TASKBAR_PINNING);
+            boolean isTaskbarPinningDesktopModeChanged =
+                    TASKBAR_PINNING_DESKTOP_MODE_KEY.equals(key)
+                            && mInfo.mIsTaskbarPinnedInDesktopMode != prefs.get(
+                            TASKBAR_PINNING_IN_DESKTOP_MODE);
+            if (isTaskbarPinningChanged || isTaskbarPinningDesktopModeChanged) {
+                notifyConfigChange();
+            }
+        };
 
         LauncherPrefs.get(context).addListener(
                 mTaskbarPinningPreferenceChangeListener, TASKBAR_PINNING);
@@ -257,7 +255,9 @@
                 || config.fontScale != mInfo.fontScale
                 || !mInfo.mScreenSizeDp.equals(
                         new PortraitSize(config.screenHeightDp, config.screenWidthDp))
-                || mWindowContext.getDisplay().getRotation() != mInfo.rotation) {
+                || mWindowContext.getDisplay().getRotation() != mInfo.rotation
+                || WindowManagerProxy.INSTANCE.get(mContext).showLockedTaskbarOnHome(mWindowContext)
+                        != mInfo.showLockedTaskbarOnHome()) {
             notifyConfigChange();
         }
     }
diff --git a/src/com/android/launcher3/util/ExecutorUtil.java b/src/com/android/launcher3/util/ExecutorUtil.java
deleted file mode 100644
index efc0eec..0000000
--- a/src/com/android/launcher3/util/ExecutorUtil.java
+++ /dev/null
@@ -1,37 +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.util;
-
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-
-import android.os.Looper;
-
-import java.util.concurrent.ExecutionException;
-
-public final class ExecutorUtil {
-
-    /**
-     * Executes runnable on {@link Looper#getMainLooper()}, otherwise fails with an exception.
-     */
-    public static void executeSyncOnMainOrFail(Runnable runnable) {
-        try {
-            MAIN_EXECUTOR.submit(runnable).get();
-        } catch (InterruptedException | ExecutionException e) {
-            throw new RuntimeException(e);
-        }
-    }
-}
diff --git a/src/com/android/launcher3/util/LockedUserState.kt b/src/com/android/launcher3/util/LockedUserState.kt
index 10559f3..c8d86d4 100644
--- a/src/com/android/launcher3/util/LockedUserState.kt
+++ b/src/com/android/launcher3/util/LockedUserState.kt
@@ -88,6 +88,13 @@
         mUserUnlockedActions.add(action)
     }
 
+    /**
+     * Removes a previously queued `Runnable` to be run when the user is unlocked.
+     */
+    fun removeOnUserUnlockedRunnable(action: Runnable) {
+        mUserUnlockedActions.remove(action)
+    }
+
     companion object {
         @VisibleForTesting
         @JvmField
diff --git a/src/com/android/launcher3/util/LogConfig.java b/src/com/android/launcher3/util/LogConfig.java
index f183f18..72e3e79 100644
--- a/src/com/android/launcher3/util/LogConfig.java
+++ b/src/com/android/launcher3/util/LogConfig.java
@@ -52,9 +52,9 @@
     public static final String WEB_APP_SEARCH_LOGGING = "WebAppSearchLogging";
 
     /**
-     * When turned on, we enable quick launch v2 related logging.
+     * When turned on, we enable quick launch related logging.
      */
-    public static final String QUICK_LAUNCH_V2 = "QuickLaunchV2";
+    public static final String QUICK_LAUNCH = "QuickLaunch";
 
     /**
      * When turned on, we enable Gms Play related logging.
diff --git a/src/com/android/launcher3/util/MSDLPlayerWrapper.java b/src/com/android/launcher3/util/MSDLPlayerWrapper.java
new file mode 100644
index 0000000..1e53ac1
--- /dev/null
+++ b/src/com/android/launcher3/util/MSDLPlayerWrapper.java
@@ -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.util;
+
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+
+import android.content.Context;
+import android.os.Vibrator;
+
+import com.android.launcher3.dagger.ApplicationContext;
+import com.android.launcher3.dagger.LauncherAppSingleton;
+import com.android.launcher3.dagger.LauncherBaseAppComponent;
+
+import com.google.android.msdl.data.model.MSDLToken;
+import com.google.android.msdl.domain.InteractionProperties;
+import com.google.android.msdl.domain.MSDLPlayer;
+
+import javax.inject.Inject;
+
+/**
+ * Wrapper around {@link com.google.android.msdl.domain.MSDLPlayer} to perform MSDL feedback.
+ */
+@LauncherAppSingleton
+public class MSDLPlayerWrapper {
+
+    public static final DaggerSingletonObject<MSDLPlayerWrapper> INSTANCE =
+            new DaggerSingletonObject<>(LauncherBaseAppComponent::getMSDLPlayerWrapper);
+
+    /** Internal player */
+    private final MSDLPlayer mMSDLPlayer;
+
+    @Inject
+    public MSDLPlayerWrapper(@ApplicationContext Context context) {
+        Vibrator vibrator = context.getSystemService(Vibrator.class);
+        mMSDLPlayer = MSDLPlayer.Companion.createPlayer(vibrator, UI_HELPER_EXECUTOR, null);
+    }
+
+    /** Perform MSDL feedback for a token with interaction properties */
+    public void playToken(MSDLToken token, InteractionProperties properties) {
+        mMSDLPlayer.playToken(token, properties);
+    }
+
+    /** Perform MSDL feedback for a token without properties */
+    public void playToken(MSDLToken token) {
+        mMSDLPlayer.playToken(token, null);
+    }
+}
diff --git a/src/com/android/launcher3/util/MainThreadInitializedObject.java b/src/com/android/launcher3/util/MainThreadInitializedObject.java
index e12ccbc..356a551 100644
--- a/src/com/android/launcher3/util/MainThreadInitializedObject.java
+++ b/src/com/android/launcher3/util/MainThreadInitializedObject.java
@@ -50,7 +50,7 @@
 
     public T get(Context context) {
         Context app = context.getApplicationContext();
-        if (app instanceof SandboxApplication sc) {
+        if (app instanceof ObjectSandbox sc) {
             return sc.getObject(this);
         }
 
@@ -100,7 +100,8 @@
         T get(Context context);
     }
 
-    public interface SandboxApplication {
+    /** Sandbox for isolating {@link MainThreadInitializedObject} instances from Launcher. */
+    public interface ObjectSandbox {
 
         /**
          * Find a cached object from mObjectMap if we have already created one. If not, generate
@@ -108,6 +109,25 @@
          */
         <T extends SafeCloseable> T getObject(MainThreadInitializedObject<T> object);
 
+
+        /**
+         * Put a value into cache, can be used to put mocked MainThreadInitializedObject
+         * instances.
+         */
+        <T extends SafeCloseable> void putObject(MainThreadInitializedObject<T> object, T value);
+
+        /**
+         * Returns whether this sandbox should cleanup all objects when its destroyed or leave it
+         * to the GC.
+         * These objects can have listeners attached to the system server and mey not be able to get
+         * GCed themselves when running on a device.
+         * Some environments like Robolectric tear down the whole system at the end of the test,
+         * so manual cleanup may not be required.
+         */
+        default boolean shouldCleanUpOnDestroy() {
+            return true;
+        }
+
         @UiThread
         default <T extends SafeCloseable> T createObject(MainThreadInitializedObject<T> object) {
             return object.mProvider.get((Context) this);
@@ -118,7 +138,7 @@
      * Abstract Context which allows custom implementations for
      * {@link MainThreadInitializedObject} providers
      */
-    public static class SandboxContext extends LauncherApplication implements SandboxApplication {
+    public static class SandboxContext extends LauncherApplication implements ObjectSandbox {
 
         private static final String TAG = "SandboxContext";
 
@@ -130,7 +150,6 @@
 
         public SandboxContext(Context base) {
             attachBaseContext(base);
-            initDagger();
         }
 
         @Override
@@ -138,7 +157,19 @@
             return this;
         }
 
+        @Override
+        public boolean shouldCleanUpOnDestroy() {
+            return (getBaseContext().getApplicationContext() instanceof ObjectSandbox os)
+                    ? os.shouldCleanUpOnDestroy() : true;
+        }
+
         public void onDestroy() {
+            if (shouldCleanUpOnDestroy()) {
+                cleanUpObjects();
+            }
+        }
+
+        protected void cleanUpObjects() {
             getAppComponent().getDaggerSingletonTracker().close();
             synchronized (mDestroyLock) {
                 // Destroy in reverse order
@@ -174,10 +205,7 @@
             }
         }
 
-        /**
-         * Put a value into mObjectMap, can be used to put mocked MainThreadInitializedObject
-         * instances into SandboxContext.
-         */
+        @Override
         public <T extends SafeCloseable> void putObject(
                 MainThreadInitializedObject<T> object, T value) {
             mObjectMap.put(object, value);
diff --git a/src/com/android/launcher3/util/PackageManagerHelper.java b/src/com/android/launcher3/util/PackageManagerHelper.java
index b1913c0..4b60d98 100644
--- a/src/com/android/launcher3/util/PackageManagerHelper.java
+++ b/src/com/android/launcher3/util/PackageManagerHelper.java
@@ -24,13 +24,10 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
-import android.content.pm.ApplicationInfo;
 import android.content.pm.LauncherActivityInfo;
 import android.content.pm.LauncherApps;
-import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
-import android.content.pm.ResolveInfo;
 import android.graphics.Rect;
 import android.os.Bundle;
 import android.os.Process;
@@ -42,10 +39,12 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
-import com.android.launcher3.Flags;
 import com.android.launcher3.PendingAddItemInfo;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.dagger.ApplicationContext;
+import com.android.launcher3.dagger.LauncherAppSingleton;
+import com.android.launcher3.dagger.LauncherBaseAppComponent;
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.ItemInfoWithIcon;
@@ -55,16 +54,19 @@
 import java.util.List;
 import java.util.Objects;
 
+import javax.inject.Inject;
+
 /**
  * Utility methods using package manager
  */
-public class PackageManagerHelper implements SafeCloseable{
+@LauncherAppSingleton
+public class PackageManagerHelper {
 
     private static final String TAG = "PackageManagerHelper";
 
     @NonNull
-    public static final MainThreadInitializedObject<PackageManagerHelper> INSTANCE =
-            new MainThreadInitializedObject<>(PackageManagerHelper::new);
+    public static DaggerSingletonObject<PackageManagerHelper> INSTANCE =
+            new DaggerSingletonObject<>(LauncherBaseAppComponent::getPackageManagerHelper);
 
     @NonNull
     private final Context mContext;
@@ -77,78 +79,13 @@
 
     private final String[] mLegacyMultiInstanceSupportedApps;
 
-    public PackageManagerHelper(@NonNull final Context context) {
+    @Inject
+    public PackageManagerHelper(@ApplicationContext final Context context) {
         mContext = context;
         mPm = context.getPackageManager();
         mLauncherApps = Objects.requireNonNull(context.getSystemService(LauncherApps.class));
         mLegacyMultiInstanceSupportedApps = mContext.getResources().getStringArray(
-                R.array.config_appsSupportMultiInstancesSplit);
-    }
-
-    @Override
-    public void close() { }
-
-    /**
-     * Returns true if the app can possibly be on the SDCard. This is just a workaround and doesn't
-     * guarantee that the app is on SD card.
-     */
-    public boolean isAppOnSdcard(@NonNull final String packageName,
-            @NonNull final UserHandle user) {
-        final ApplicationInfo info = getApplicationInfo(
-                packageName, user, PackageManager.MATCH_UNINSTALLED_PACKAGES);
-        return info != null && (info.flags & ApplicationInfo.FLAG_EXTERNAL_STORAGE) != 0;
-    }
-
-    /**
-     * Returns whether the target app is suspended for a given user as per
-     * {@link android.app.admin.DevicePolicyManager#isPackageSuspended}.
-     */
-    public boolean isAppSuspended(@NonNull final String packageName,
-            @NonNull final UserHandle user) {
-        final ApplicationInfo info = getApplicationInfo(packageName, user, 0);
-        return info != null && isAppSuspended(info);
-    }
-
-    /**
-     * Returns whether the target app is installed for a given user
-     */
-    public boolean isAppInstalled(@NonNull final String packageName,
-            @NonNull final UserHandle user) {
-        final ApplicationInfo info = getApplicationInfo(packageName, user, 0);
-        return info != null;
-    }
-
-    /**
-     * Returns whether the target app is archived for a given user
-     */
-    @SuppressWarnings("NewApi")
-    public boolean isAppArchivedForUser(@NonNull final String packageName,
-            @NonNull final UserHandle user) {
-        if (!Flags.enableSupportForArchiving()) {
-            return false;
-        }
-        final ApplicationInfo info = getApplicationInfo(
-                // LauncherApps does not support long flags currently. Since archived apps are
-                // subset of uninstalled apps, this filter also includes archived apps.
-                packageName, user, PackageManager.MATCH_UNINSTALLED_PACKAGES);
-        return info != null && info.isArchived;
-    }
-
-    /**
-     * Returns whether the target app is in archived state
-     */
-    @SuppressWarnings("NewApi")
-    public boolean isAppArchived(@NonNull final String packageName) {
-        final ApplicationInfo info;
-        try {
-            info = mPm.getPackageInfo(packageName,
-                    PackageManager.PackageInfoFlags.of(
-                            PackageManager.MATCH_ARCHIVED_PACKAGES)).applicationInfo;
-            return info.isArchived;
-        } catch (NameNotFoundException e) {
-            Log.e(TAG, "Failed to get applicationInfo for package: " + packageName, e);
-            return false;
-        }
+                    R.array.config_appsSupportMultiInstancesSplit);
     }
 
     /**
@@ -164,20 +101,6 @@
     }
 
     /**
-     * Returns the application info for the provided package or null
-     */
-    @Nullable
-    public ApplicationInfo getApplicationInfo(@NonNull final String packageName,
-            @NonNull final UserHandle user, final int flags) {
-        try {
-            ApplicationInfo info = mLauncherApps.getApplicationInfo(packageName, flags, user);
-            return !isPackageInstalledOrArchived(info) || !info.enabled ? null : info;
-        } catch (PackageManager.NameNotFoundException e) {
-            return null;
-        }
-    }
-
-    /**
      * Returns the preferred launch activity intent for a given package.
      */
     @Nullable
@@ -197,14 +120,6 @@
     }
 
     /**
-     * Returns whether an application is suspended as per
-     * {@link android.app.admin.DevicePolicyManager#isPackageSuspended}.
-     */
-    public static boolean isAppSuspended(ApplicationInfo info) {
-        return (info.flags & ApplicationInfo.FLAG_SUSPENDED) != 0;
-    }
-
-    /**
      * Starts the details activity for {@code info}
      */
     public static void startDetailsActivityForInfo(Context context, ItemInfo info,
@@ -236,35 +151,6 @@
         }
     }
 
-    public static boolean isSystemApp(@NonNull final Context context,
-            @NonNull final Intent intent) {
-        PackageManager pm = context.getPackageManager();
-        ComponentName cn = intent.getComponent();
-        String packageName = null;
-        if (cn == null) {
-            ResolveInfo info = pm.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY);
-            if ((info != null) && (info.activityInfo != null)) {
-                packageName = info.activityInfo.packageName;
-            }
-        } else {
-            packageName = cn.getPackageName();
-        }
-        if (packageName == null) {
-            packageName = intent.getPackage();
-        }
-        if (packageName != null) {
-            try {
-                PackageInfo info = pm.getPackageInfo(packageName, 0);
-                return (info != null) && (info.applicationInfo != null) &&
-                        ((info.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0);
-            } catch (NameNotFoundException e) {
-                return false;
-            }
-        } else {
-            return false;
-        }
-    }
-
     /**
      * Returns true if the intent is a valid launch intent for a launcher activity of an app.
      * This is used to identify shortcuts which are different from the ones exposed by the
@@ -306,13 +192,6 @@
         return (int) (100 * info.getLoadingProgress());
     }
 
-    /** Returns true in case app is installed on the device or in archived state. */
-    @SuppressWarnings("NewApi")
-    private boolean isPackageInstalledOrArchived(ApplicationInfo info) {
-        return (info.flags & ApplicationInfo.FLAG_INSTALLED) != 0 || (
-                Flags.enableSupportForArchiving() && info.isArchived);
-    }
-
     /**
      * Returns whether the given component or its application has the multi-instance property set.
      */
diff --git a/src/com/android/launcher3/util/PluginManagerWrapper.java b/src/com/android/launcher3/util/PluginManagerWrapper.java
index b27aa12..5b28570 100644
--- a/src/com/android/launcher3/util/PluginManagerWrapper.java
+++ b/src/com/android/launcher3/util/PluginManagerWrapper.java
@@ -15,32 +15,39 @@
  */
 package com.android.launcher3.util;
 
-import static com.android.launcher3.util.MainThreadInitializedObject.forOverride;
+import androidx.annotation.AnyThread;
 
-import com.android.launcher3.R;
+import com.android.launcher3.dagger.LauncherAppSingleton;
+import com.android.launcher3.dagger.LauncherBaseAppComponent;
 import com.android.systemui.plugins.Plugin;
 import com.android.systemui.plugins.PluginListener;
 
 import java.io.PrintWriter;
 
-public class PluginManagerWrapper implements ResourceBasedOverride, SafeCloseable {
+import javax.inject.Inject;
 
-    public static final MainThreadInitializedObject<PluginManagerWrapper> INSTANCE =
-            forOverride(PluginManagerWrapper.class, R.string.plugin_manager_wrapper_class);
+@LauncherAppSingleton
+public class PluginManagerWrapper{
 
+    public static final DaggerSingletonObject<PluginManagerWrapper> INSTANCE =
+            new DaggerSingletonObject<>(LauncherBaseAppComponent::getPluginManagerWrapper);
+
+    @Inject
+    public PluginManagerWrapper() { }
+
+    @AnyThread
     public <T extends Plugin> void addPluginListener(
             PluginListener<T> listener, Class<T> pluginClass) {
         addPluginListener(listener, pluginClass, false);
     }
 
+    @AnyThread
     public <T extends Plugin> void addPluginListener(
             PluginListener<T> listener, Class<T> pluginClass, boolean allowMultiple) {
     }
 
+    @AnyThread
     public void removePluginListener(PluginListener<? extends Plugin> listener) { }
 
-    @Override
-    public void close() { }
-
     public void dump(PrintWriter pw) { }
 }
diff --git a/src/com/android/launcher3/util/ScreenOnTracker.java b/src/com/android/launcher3/util/ScreenOnTracker.java
index 8ee799a..50be98b 100644
--- a/src/com/android/launcher3/util/ScreenOnTracker.java
+++ b/src/com/android/launcher3/util/ScreenOnTracker.java
@@ -26,16 +26,22 @@
 
 import androidx.annotation.VisibleForTesting;
 
+import com.android.launcher3.dagger.ApplicationContext;
+import com.android.launcher3.dagger.LauncherAppSingleton;
+import com.android.launcher3.dagger.LauncherBaseAppComponent;
+
 import java.util.concurrent.CopyOnWriteArrayList;
-import java.util.function.Consumer;
+
+import javax.inject.Inject;
 
 /**
  * Utility class for tracking if the screen is currently on or off
  */
+@LauncherAppSingleton
 public class ScreenOnTracker implements SafeCloseable {
 
-    public static final MainThreadInitializedObject<ScreenOnTracker> INSTANCE =
-            new MainThreadInitializedObject<>(ScreenOnTracker::new);
+    public static final DaggerSingletonObject<ScreenOnTracker> INSTANCE =
+            new DaggerSingletonObject<>(LauncherBaseAppComponent::getScreenOnTracker);
 
     private final SimpleBroadcastReceiver mReceiver;
     private final CopyOnWriteArrayList<ScreenOnListener> mListeners = new CopyOnWriteArrayList<>();
@@ -43,23 +49,26 @@
     private final Context mContext;
     private boolean mIsScreenOn;
 
-    private ScreenOnTracker(Context context) {
+    @Inject
+    ScreenOnTracker(@ApplicationContext Context context, DaggerSingletonTracker tracker) {
         // Assume that the screen is on to begin with
         mContext = context;
         mReceiver = new SimpleBroadcastReceiver(UI_HELPER_EXECUTOR, this::onReceive);
-        init();
+        init(tracker);
     }
 
     @VisibleForTesting
-    ScreenOnTracker(Context context, SimpleBroadcastReceiver receiver) {
+    ScreenOnTracker(@ApplicationContext Context context, SimpleBroadcastReceiver receiver,
+            DaggerSingletonTracker tracker) {
         mContext = context;
         mReceiver = receiver;
-        init();
+        init(tracker);
     }
 
-    private void init() {
+    private void init(DaggerSingletonTracker tracker) {
         mIsScreenOn = true;
         mReceiver.register(mContext, ACTION_SCREEN_ON, ACTION_SCREEN_OFF, ACTION_USER_PRESENT);
+        tracker.addCloseable(this);
     }
 
     @Override
diff --git a/src/com/android/launcher3/util/SettingsCache.java b/src/com/android/launcher3/util/SettingsCache.java
index cd6701d..8fe6e93 100644
--- a/src/com/android/launcher3/util/SettingsCache.java
+++ b/src/com/android/launcher3/util/SettingsCache.java
@@ -25,14 +25,23 @@
 import android.database.ContentObserver;
 import android.net.Uri;
 import android.os.Handler;
+import android.os.Looper;
 import android.provider.Settings;
 
+import androidx.annotation.UiThread;
+
+import com.android.launcher3.dagger.ApplicationContext;
+import com.android.launcher3.dagger.LauncherAppSingleton;
+import com.android.launcher3.dagger.LauncherBaseAppComponent;
+
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.CopyOnWriteArrayList;
 
+import javax.inject.Inject;
+
 /**
  * ContentObserver over Settings keys that also has a caching layer.
  * Consumers can register for callbacks via {@link #register(Uri, OnChangeListener)} and
@@ -47,6 +56,7 @@
  *
  * Cache will also be updated if a key queried is missing (even if it has no listeners registered).
  */
+@LauncherAppSingleton
 public class SettingsCache extends ContentObserver implements SafeCloseable {
 
     /** Hidden field Settings.Secure.NOTIFICATION_BADGING */
@@ -79,12 +89,14 @@
     /**
      * Singleton instance
      */
-    public static MainThreadInitializedObject<SettingsCache> INSTANCE =
-            new MainThreadInitializedObject<>(SettingsCache::new);
+    public static final DaggerSingletonObject<SettingsCache> INSTANCE =
+            new DaggerSingletonObject<>(LauncherBaseAppComponent::getSettingsCache);
 
-    private SettingsCache(Context context) {
-        super(new Handler());
+    @Inject
+    SettingsCache(@ApplicationContext Context context, DaggerSingletonTracker tracker) {
+        super(new Handler(Looper.getMainLooper()));
         mResolver = context.getContentResolver();
+        tracker.addCloseable(this);
     }
 
     @Override
@@ -130,7 +142,9 @@
      * Does not de-dupe if you add same listeners for the same key multiple times.
      * Unregister once complete using {@link #unregister(Uri, OnChangeListener)}
      */
+    @UiThread
     public void register(Uri uri, OnChangeListener changeListener) {
+        Preconditions.assertUIThread();
         if (mListenerMap.containsKey(uri)) {
             mListenerMap.get(uri).add(changeListener);
         } else {
diff --git a/src/com/android/launcher3/util/SystemUiController.java b/src/com/android/launcher3/util/SystemUiController.java
index df54fd7..368b267 100644
--- a/src/com/android/launcher3/util/SystemUiController.java
+++ b/src/com/android/launcher3/util/SystemUiController.java
@@ -17,7 +17,6 @@
 package com.android.launcher3.util;
 
 import android.view.View;
-import android.view.Window;
 
 import androidx.annotation.IntDef;
 
@@ -54,11 +53,11 @@
     })
     public @interface SystemUiControllerFlags {}
 
-    private final Window mWindow;
+    private final View mView;
     private final int[] mStates = new int[5];
 
-    public SystemUiController(Window window) {
-        mWindow = window;
+    public SystemUiController(View view) {
+        mView = view;
     }
 
     public void updateUiState(int uiState, boolean isLight) {
@@ -72,14 +71,14 @@
         }
         mStates[uiState] = flags;
 
-        int oldFlags = mWindow.getDecorView().getSystemUiVisibility();
+        int oldFlags = mView.getSystemUiVisibility();
         // Apply the state flags in priority order
         int newFlags = oldFlags;
         for (int stateFlag : mStates) {
             newFlags = getSysUiVisibilityFlags(stateFlag, newFlags);
         }
         if (newFlags != oldFlags) {
-            mWindow.getDecorView().setSystemUiVisibility(newFlags);
+            mView.setSystemUiVisibility(newFlags);
         }
     }
 
@@ -88,7 +87,7 @@
      */
     public int getBaseSysuiVisibility() {
         return getSysUiVisibilityFlags(
-                mStates[UI_STATE_BASE_WINDOW], mWindow.getDecorView().getSystemUiVisibility());
+                mStates[UI_STATE_BASE_WINDOW], mView.getSystemUiVisibility());
     }
 
     private int getSysUiVisibilityFlags(int stateFlag, int currentVisibility) {
diff --git a/src/com/android/launcher3/util/VibratorWrapper.java b/src/com/android/launcher3/util/VibratorWrapper.java
index adb8f9d..39c9c42 100644
--- a/src/com/android/launcher3/util/VibratorWrapper.java
+++ b/src/com/android/launcher3/util/VibratorWrapper.java
@@ -19,6 +19,7 @@
 import static android.os.VibrationEffect.createPredefined;
 import static android.provider.Settings.System.HAPTIC_FEEDBACK_ENABLED;
 
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 
 import android.annotation.SuppressLint;
@@ -31,13 +32,20 @@
 
 import androidx.annotation.VisibleForTesting;
 
+import com.android.launcher3.dagger.ApplicationContext;
+import com.android.launcher3.dagger.LauncherAppSingleton;
+import com.android.launcher3.dagger.LauncherBaseAppComponent;
+
+import javax.inject.Inject;
+
 /**
  * Wrapper around {@link Vibrator} to easily perform haptic feedback where necessary.
  */
-public class VibratorWrapper implements SafeCloseable {
+@LauncherAppSingleton
+public class VibratorWrapper {
 
-    public static final MainThreadInitializedObject<VibratorWrapper> INSTANCE =
-            new MainThreadInitializedObject<>(VibratorWrapper::new);
+    public static final DaggerSingletonObject<VibratorWrapper> INSTANCE =
+            new DaggerSingletonObject<>(LauncherBaseAppComponent::getVibratorWrapper);
 
     public static final AudioAttributes VIBRATION_ATTRS = new AudioAttributes.Builder()
             .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
@@ -59,38 +67,29 @@
     private final Vibrator mVibrator;
     private final boolean mHasVibrator;
 
-    private final SettingsCache mSettingsCache;
-
     @VisibleForTesting
     final SettingsCache.OnChangeListener mHapticChangeListener =
             isEnabled -> mIsHapticFeedbackEnabled = isEnabled;
 
     private boolean mIsHapticFeedbackEnabled;
 
-    private VibratorWrapper(Context context) {
-        this(context.getSystemService(Vibrator.class), SettingsCache.INSTANCE.get(context));
-    }
+    @Inject
+    public VibratorWrapper(@ApplicationContext Context context, SettingsCache settingsCache,
+            DaggerSingletonTracker tracker) {
 
-    @VisibleForTesting
-    VibratorWrapper(Vibrator vibrator, SettingsCache settingsCache) {
-        mVibrator = vibrator;
+        mVibrator = context.getSystemService(Vibrator.class);
         mHasVibrator = mVibrator.hasVibrator();
-        mSettingsCache = settingsCache;
         if (mHasVibrator) {
-            mSettingsCache.register(HAPTIC_FEEDBACK_URI, mHapticChangeListener);
-            mIsHapticFeedbackEnabled = mSettingsCache.getValue(HAPTIC_FEEDBACK_URI, 0);
+            MAIN_EXECUTOR.execute(
+                    () -> settingsCache.register(HAPTIC_FEEDBACK_URI, mHapticChangeListener));
+            mIsHapticFeedbackEnabled = settingsCache.getValue(HAPTIC_FEEDBACK_URI, 0);
+            tracker.addCloseable(
+                    () -> settingsCache.unregister(HAPTIC_FEEDBACK_URI, mHapticChangeListener));
         } else {
             mIsHapticFeedbackEnabled = false;
         }
     }
 
-    @Override
-    public void close() {
-        if (mHasVibrator) {
-            mSettingsCache.unregister(HAPTIC_FEEDBACK_URI, mHapticChangeListener);
-        }
-    }
-
     /**
      * This should be used to cancel a haptic in case where the haptic shouldn't be vibrating. For
      * example, when no animation is happening but a vibrator happens to be vibrating still.
diff --git a/src/com/android/launcher3/util/window/RefreshRateTracker.java b/src/com/android/launcher3/util/window/RefreshRateTracker.java
index 7814617..e3397d4 100644
--- a/src/com/android/launcher3/util/window/RefreshRateTracker.java
+++ b/src/com/android/launcher3/util/window/RefreshRateTracker.java
@@ -26,25 +26,34 @@
 
 import androidx.annotation.WorkerThread;
 
-import com.android.launcher3.util.MainThreadInitializedObject;
+import com.android.launcher3.dagger.ApplicationContext;
+import com.android.launcher3.dagger.LauncherAppComponent;
+import com.android.launcher3.dagger.LauncherAppSingleton;
+import com.android.launcher3.util.DaggerSingletonObject;
+import com.android.launcher3.util.DaggerSingletonTracker;
 import com.android.launcher3.util.SafeCloseable;
 
+import javax.inject.Inject;
+
 /**
  * Utility class to track refresh rate of the current device
  */
+@LauncherAppSingleton
 public class RefreshRateTracker implements DisplayListener, SafeCloseable {
 
-    private static final MainThreadInitializedObject<RefreshRateTracker> INSTANCE =
-            new MainThreadInitializedObject<>(RefreshRateTracker::new);
+    private static final DaggerSingletonObject<RefreshRateTracker> INSTANCE =
+            new DaggerSingletonObject<>(LauncherAppComponent::getRefreshRateTracker);
 
     private int mSingleFrameMs = 1;
 
     private final DisplayManager mDM;
 
-    private RefreshRateTracker(Context context) {
+    @Inject
+    RefreshRateTracker(@ApplicationContext Context context, DaggerSingletonTracker tracker) {
         mDM = context.getSystemService(DisplayManager.class);
         updateSingleFrameMs();
         mDM.registerDisplayListener(this, UI_HELPER_EXECUTOR.getHandler());
+        tracker.addCloseable(this);
     }
 
     /**
diff --git a/src/com/android/launcher3/views/AbstractSlideInView.java b/src/com/android/launcher3/views/AbstractSlideInView.java
index 85aad89..65d02d0 100644
--- a/src/com/android/launcher3/views/AbstractSlideInView.java
+++ b/src/com/android/launcher3/views/AbstractSlideInView.java
@@ -298,8 +298,7 @@
     @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     public void onBackProgressed(BackEvent backEvent) {
         final float progress = backEvent.getProgress();
-        float deceleratedProgress = Interpolators.BACK_GESTURE.getInterpolation(progress);
-        mSwipeToDismissProgress.updateValue(deceleratedProgress);
+        mSwipeToDismissProgress.updateValue(progress);
     }
 
     /**
diff --git a/src/com/android/launcher3/views/ActivityContext.java b/src/com/android/launcher3/views/ActivityContext.java
index d3160e0..b8481c5 100644
--- a/src/com/android/launcher3/views/ActivityContext.java
+++ b/src/com/android/launcher3/views/ActivityContext.java
@@ -26,9 +26,11 @@
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 
+import android.app.Activity;
 import android.app.ActivityOptions;
 import android.app.PendingIntent;
 import android.content.ActivityNotFoundException;
+import android.content.ComponentName;
 import android.content.Context;
 import android.content.ContextWrapper;
 import android.content.Intent;
@@ -46,6 +48,7 @@
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.View.AccessibilityDelegate;
+import android.view.Window;
 import android.view.WindowInsets;
 import android.view.WindowInsetsController;
 import android.view.inputmethod.InputMethodManager;
@@ -76,10 +79,11 @@
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.popup.PopupDataProvider;
 import com.android.launcher3.util.ActivityOptionsWrapper;
-import com.android.launcher3.util.PackageManagerHelper;
+import com.android.launcher3.util.ApplicationInfoWrapper;
 import com.android.launcher3.util.Preconditions;
 import com.android.launcher3.util.RunnableList;
 import com.android.launcher3.util.SplitConfigurationOptions;
+import com.android.launcher3.util.SystemUiController;
 import com.android.launcher3.util.ViewCache;
 import com.android.launcher3.widget.picker.model.WidgetPickerDataProvider;
 
@@ -175,6 +179,23 @@
     BaseDragLayer getDragLayer();
 
     /**
+     * @see Activity#getWindow()
+     * @return Window
+     */
+    @Nullable
+    default Window getWindow() {
+        return null;
+    }
+
+    /**
+     * @see Activity#getComponentName()
+     * @return ComponentName
+     */
+    default ComponentName getComponentName() {
+        return null;
+    }
+
+    /**
      * The all apps container, if it exists in this context.
      */
     default ActivityAllAppsContainerView<?> getAppsView() {
@@ -216,6 +237,11 @@
         return null;
     }
 
+    @Nullable
+    default SystemUiController getSystemUiController() {
+        return null;
+    }
+
     /**
      * Handler for actions taken on drop targets that require launcher
      */
@@ -391,7 +417,7 @@
             View v, Intent intent, @Nullable ItemInfo item) {
         Preconditions.assertUIThread();
         Context context = (Context) this;
-        if (isAppBlockedForSafeMode() && !PackageManagerHelper.isSystemApp(context, intent)) {
+        if (isAppBlockedForSafeMode() && !new ApplicationInfoWrapper(context, intent).isSystem()) {
             Toast.makeText(context, R.string.safemode_shortcut_error, Toast.LENGTH_SHORT).show();
             return null;
         }
diff --git a/src/com/android/launcher3/views/BaseDragLayer.java b/src/com/android/launcher3/views/BaseDragLayer.java
index 5d2d3f4..ea3fb3f 100644
--- a/src/com/android/launcher3/views/BaseDragLayer.java
+++ b/src/com/android/launcher3/views/BaseDragLayer.java
@@ -107,7 +107,7 @@
     protected final RectF mSystemGestureRegion = new RectF();
     private int mTouchDispatchState = 0;
 
-    protected final T mActivity;
+    protected final T mContainer;
     private final MultiValueAlpha mMultiValueAlpha;
 
     // All the touch controllers for the view
@@ -121,7 +121,7 @@
 
     public BaseDragLayer(Context context, AttributeSet attrs, int alphaChannelCount) {
         super(context, attrs);
-        mActivity = ActivityContext.lookupContext(context);
+        mContainer = ActivityContext.lookupContext(context);
         mMultiValueAlpha = new MultiValueAlpha(this, alphaChannelCount);
     }
 
@@ -159,7 +159,7 @@
             }
             mTouchCompleteListener = null;
         } else if (action == MotionEvent.ACTION_DOWN) {
-            mActivity.finishAutoCancelActionMode();
+            mContainer.finishAutoCancelActionMode();
         }
         return findActiveController(ev);
     }
@@ -173,7 +173,7 @@
     }
 
     private TouchController findControllerToHandleTouch(MotionEvent ev) {
-        AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(mActivity);
+        AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(mContainer);
         if (topView != null
                 && (isEventWithinSystemGestureRegion(ev)
                 || topView.canInterceptEventsInSystemGestureRegion())
@@ -207,7 +207,7 @@
     @Override
     public boolean onRequestSendAccessibilityEvent(View child, AccessibilityEvent event) {
         // Shortcuts can appear above folder
-        View topView = AbstractFloatingView.getTopOpenViewWithType(mActivity,
+        View topView = AbstractFloatingView.getTopOpenViewWithType(mContainer,
                 AbstractFloatingView.TYPE_ACCESSIBLE);
         if (topView != null) {
             if (child == topView) {
@@ -222,7 +222,7 @@
 
     @Override
     public void addChildrenForAccessibility(ArrayList<View> childrenForAccessibility) {
-        View topView = AbstractFloatingView.getTopOpenViewWithType(mActivity,
+        View topView = AbstractFloatingView.getTopOpenViewWithType(mContainer,
                 AbstractFloatingView.TYPE_ACCESSIBLE);
         if (topView != null) {
             // Only add the top view as a child for accessibility when it is open
@@ -458,7 +458,7 @@
 
     @Override
     protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
-        View topView = AbstractFloatingView.getTopOpenView(mActivity);
+        View topView = AbstractFloatingView.getTopOpenView(mContainer);
         if (topView != null) {
             return topView.requestFocus(direction, previouslyFocusedRect);
         } else {
@@ -468,7 +468,7 @@
 
     @Override
     public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
-        View topView = AbstractFloatingView.getTopOpenView(mActivity);
+        View topView = AbstractFloatingView.getTopOpenView(mContainer);
         if (topView != null) {
             topView.addFocusables(views, direction);
         } else {
@@ -555,7 +555,7 @@
         Insets gestureInsets = insets.getMandatorySystemGestureInsets();
         int gestureInsetBottom = gestureInsets.bottom;
         Insets imeInset = insets.getInsets(WindowInsets.Type.ime());
-        DeviceProfile dp = mActivity.getDeviceProfile();
+        DeviceProfile dp = mContainer.getDeviceProfile();
         if (dp.isTaskbarPresent) {
             // Ignore taskbar gesture insets to avoid interfering with TouchControllers.
             gestureInsetBottom = ResourceUtils.getNavbarSize(
diff --git a/src/com/android/launcher3/views/DoubleShadowBubbleTextView.java b/src/com/android/launcher3/views/DoubleShadowBubbleTextView.java
index ef66ffe..392d9a7 100644
--- a/src/com/android/launcher3/views/DoubleShadowBubbleTextView.java
+++ b/src/com/android/launcher3/views/DoubleShadowBubbleTextView.java
@@ -102,6 +102,9 @@
 
     @Override
     public void onDraw(Canvas canvas) {
+        if (shouldDrawAppContrastTile()) {
+            drawAppContrastTile(canvas);
+        }
         // If text is transparent or shadow alpha is 0, don't draw any shadow
         if (skipDoubleShadow()) {
             super.onDraw(canvas);
diff --git a/src/com/android/launcher3/views/ScrimView.java b/src/com/android/launcher3/views/ScrimView.java
index f6c4984..ce58de1 100644
--- a/src/com/android/launcher3/views/ScrimView.java
+++ b/src/com/android/launcher3/views/ScrimView.java
@@ -29,7 +29,6 @@
 import androidx.annotation.Px;
 import androidx.core.graphics.ColorUtils;
 
-import com.android.launcher3.BaseActivity;
 import com.android.launcher3.Insettable;
 import com.android.launcher3.util.SystemUiController;
 
@@ -143,7 +142,8 @@
 
     private SystemUiController getSystemUiController() {
         if (mSystemUiController == null) {
-            mSystemUiController = BaseActivity.fromContext(getContext()).getSystemUiController();
+            mSystemUiController =
+                    ActivityContext.lookupContext(getContext()).getSystemUiController();
         }
         return mSystemUiController;
     }
diff --git a/src/com/android/launcher3/views/StickyHeaderLayout.java b/src/com/android/launcher3/views/StickyHeaderLayout.java
index 090251f..4142e1f 100644
--- a/src/com/android/launcher3/views/StickyHeaderLayout.java
+++ b/src/com/android/launcher3/views/StickyHeaderLayout.java
@@ -120,7 +120,19 @@
     }
 
     private float getCurrentScroll() {
-        return mScrollOffset + (mCurrentEmptySpaceView == null ? 0 : mCurrentEmptySpaceView.getY());
+        float scroll;
+        if (mCurrentRecyclerView.getVisibility() != VISIBLE) {
+            // When no list is displayed, assume no scroll.
+            scroll = 0f;
+        } else if (mCurrentEmptySpaceView != null) {
+            // Otherwise use empty space view as reference to position.
+            scroll = mCurrentEmptySpaceView.getY();
+        } else {
+            // If there is no empty space view, but the list is visible, we are scrolled away
+            // completely, so assume all non-sticky children should also be scrolled away.
+            scroll = -mHeaderHeight;
+        }
+        return mScrollOffset + scroll;
     }
 
     @Override
diff --git a/src/com/android/launcher3/widget/DatabaseWidgetPreviewLoader.java b/src/com/android/launcher3/widget/DatabaseWidgetPreviewLoader.java
index ab42839..e100157 100644
--- a/src/com/android/launcher3/widget/DatabaseWidgetPreviewLoader.java
+++ b/src/com/android/launcher3/widget/DatabaseWidgetPreviewLoader.java
@@ -213,8 +213,8 @@
 
                 // Draw icon in the center.
                 try {
-                    Drawable icon = LauncherAppState.getInstance(mContext).getIconCache()
-                            .getFullResIcon(info.provider.getPackageName(), info.icon);
+                    Drawable icon = info.getFullResIcon(
+                            LauncherAppState.getInstance(mContext).getIconCache());
                     if (icon != null) {
                         int appIconSize = dp.iconSizePx;
                         int iconSize = (int) Math.min(appIconSize * scale,
diff --git a/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java b/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java
index e77ba24..b877d7a 100644
--- a/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java
+++ b/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java
@@ -3,6 +3,7 @@
 import android.appwidget.AppWidgetProviderInfo;
 import android.content.ComponentName;
 import android.content.Context;
+import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.graphics.Point;
 import android.graphics.Rect;
@@ -10,11 +11,13 @@
 import android.os.Parcel;
 import android.os.UserHandle;
 
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.icons.ComponentWithLabelAndIcon;
-import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.icons.cache.BaseIconCache;
+import com.android.launcher3.icons.cache.CachedObject;
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
 
 /**
@@ -23,8 +26,7 @@
  * (who's implementation is owned by the launcher). This object represents a widget type / class,
  * as opposed to a widget instance, and so should not be confused with {@link LauncherAppWidgetInfo}
  */
-public class LauncherAppWidgetProviderInfo extends AppWidgetProviderInfo
-        implements ComponentWithLabelAndIcon {
+public class LauncherAppWidgetProviderInfo extends AppWidgetProviderInfo implements CachedObject {
 
     public static final String CLS_CUSTOM_WIDGET_PREFIX = "#custom-widget-";
 
@@ -66,6 +68,8 @@
 
     protected boolean mIsMinSizeFulfilled;
 
+    private PackageManager mPM;
+
     public static LauncherAppWidgetProviderInfo fromProviderInfo(Context context,
             AppWidgetProviderInfo info) {
         final LauncherAppWidgetProviderInfo launcherInfo;
@@ -94,6 +98,7 @@
     }
 
     public void initSpans(Context context, InvariantDeviceProfile idp) {
+        mPM = context.getApplicationContext().getPackageManager();
         int minSpanX = 0;
         int minSpanY = 0;
         int maxSpanX = idp.numColumns;
@@ -101,7 +106,6 @@
         int spanX = 0;
         int spanY = 0;
 
-
         Point cellSize = new Point();
         for (DeviceProfile dp : idp.supportedProfiles) {
             dp.getCellSize(cellSize);
@@ -185,8 +189,9 @@
                 (widgetSize + widgetPadding + cellSpacing) / (cellSize + cellSpacing)));
     }
 
-    public String getLabel(PackageManager packageManager) {
-        return super.loadLabel(packageManager);
+    @Override
+    public CharSequence getLabel() {
+        return super.loadLabel(mPM);
     }
 
     public Point getMinSpans() {
@@ -222,7 +227,13 @@
     }
 
     @Override
-    public Drawable getFullResIcon(IconCache cache) {
-        return cache.getFullResIcon(provider.getPackageName(), icon);
+    public Drawable getFullResIcon(BaseIconCache cache) {
+        return cache.getFullResIcon(getActivityInfo());
+    }
+
+    @Nullable
+    @Override
+    public ApplicationInfo getApplicationInfo() {
+        return getActivityInfo().applicationInfo;
     }
 }
\ No newline at end of file
diff --git a/src/com/android/launcher3/widget/LocalColorExtractor.java b/src/com/android/launcher3/widget/LocalColorExtractor.java
index 7b500c7..d26eb38 100644
--- a/src/com/android/launcher3/widget/LocalColorExtractor.java
+++ b/src/com/android/launcher3/widget/LocalColorExtractor.java
@@ -48,4 +48,9 @@
     public SparseIntArray generateColorsOverride(WallpaperColors colors) {
         return null;
     }
+
+    /**
+     * Updates the base context to contain the colors override
+     */
+    public void applyColorsOverride(Context base, SparseIntArray override) { }
 }
diff --git a/src/com/android/launcher3/widget/PendingAddWidgetInfo.java b/src/com/android/launcher3/widget/PendingAddWidgetInfo.java
index a916252..23ab0fb 100644
--- a/src/com/android/launcher3/widget/PendingAddWidgetInfo.java
+++ b/src/com/android/launcher3/widget/PendingAddWidgetInfo.java
@@ -82,8 +82,9 @@
 
     @NonNull
     @Override
-    public LauncherAtom.ItemInfo buildProto(@Nullable CollectionInfo collectionInfo) {
-        LauncherAtom.ItemInfo info = super.buildProto(collectionInfo);
+    public LauncherAtom.ItemInfo buildProto(
+            @Nullable CollectionInfo collectionInfo, Context context) {
+        LauncherAtom.ItemInfo info = super.buildProto(collectionInfo, context);
         return info.toBuilder()
                 .addItemAttributes(LauncherAppWidgetInfo.getAttribute(sourceContainer))
                 .build();
diff --git a/src/com/android/launcher3/widget/RoundedCornerEnforcement.java b/src/com/android/launcher3/widget/RoundedCornerEnforcement.java
index cadaf89..e190dc3 100644
--- a/src/com/android/launcher3/widget/RoundedCornerEnforcement.java
+++ b/src/com/android/launcher3/widget/RoundedCornerEnforcement.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3.widget;
 
+import static com.android.launcher3.Flags.enforceSystemRadiusForAppWidgets;
+
 import android.appwidget.AppWidgetHostView;
 import android.content.Context;
 import android.content.res.Resources;
@@ -97,6 +99,10 @@
     public static float computeEnforcedRadius(@NonNull Context context) {
         Resources res = context.getResources();
         float systemRadius = res.getDimension(android.R.dimen.system_app_widget_background_radius);
+        if (enforceSystemRadiusForAppWidgets()) {
+            return systemRadius;
+        }
+
         float defaultRadius = res.getDimension(R.dimen.enforced_rounded_corner_max_radius);
         return Math.min(defaultRadius, systemRadius);
     }
diff --git a/src/com/android/launcher3/widget/custom/CustomAppWidgetProviderInfo.java b/src/com/android/launcher3/widget/custom/CustomAppWidgetProviderInfo.java
index 5ad9222..82a6883 100644
--- a/src/com/android/launcher3/widget/custom/CustomAppWidgetProviderInfo.java
+++ b/src/com/android/launcher3/widget/custom/CustomAppWidgetProviderInfo.java
@@ -18,7 +18,6 @@
 
 import android.content.ComponentName;
 import android.content.Context;
-import android.content.pm.PackageManager;
 import android.os.Parcel;
 import android.os.Parcelable;
 
@@ -64,7 +63,7 @@
     }
 
     @Override
-    public String getLabel(PackageManager packageManager) {
+    public CharSequence getLabel() {
         return Utilities.trim(label);
     }
 
diff --git a/src/com/android/launcher3/widget/custom/CustomWidgetManager.java b/src/com/android/launcher3/widget/custom/CustomWidgetManager.java
index faa5d12..20cce8f 100644
--- a/src/com/android/launcher3/widget/custom/CustomWidgetManager.java
+++ b/src/com/android/launcher3/widget/custom/CustomWidgetManager.java
@@ -18,6 +18,7 @@
 
 import static com.android.launcher3.Flags.enableSmartspaceAsAWidget;
 import static com.android.launcher3.model.data.LauncherAppWidgetInfo.CUSTOM_WIDGET_ID;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.widget.LauncherAppWidgetProviderInfo.CLS_CUSTOM_WIDGET_PREFIX;
 
 import android.appwidget.AppWidgetManager;
@@ -33,8 +34,11 @@
 import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.R;
-import com.android.launcher3.util.MainThreadInitializedObject;
-import com.android.launcher3.util.PackageUserKey;
+import com.android.launcher3.dagger.ApplicationContext;
+import com.android.launcher3.dagger.LauncherAppSingleton;
+import com.android.launcher3.dagger.LauncherBaseAppComponent;
+import com.android.launcher3.util.DaggerSingletonObject;
+import com.android.launcher3.util.DaggerSingletonTracker;
 import com.android.launcher3.util.PluginManagerWrapper;
 import com.android.launcher3.util.SafeCloseable;
 import com.android.launcher3.widget.LauncherAppWidgetHostView;
@@ -47,38 +51,45 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.function.Consumer;
+import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.stream.Stream;
 
+import javax.inject.Inject;
+
 /**
  * CustomWidgetManager handles custom widgets implemented as a plugin.
  */
-public class CustomWidgetManager implements PluginListener<CustomWidgetPlugin>, SafeCloseable {
+@LauncherAppSingleton
+public class CustomWidgetManager implements PluginListener<CustomWidgetPlugin> {
 
-    public static final MainThreadInitializedObject<CustomWidgetManager> INSTANCE =
-            new MainThreadInitializedObject<>(CustomWidgetManager::new);
+    public static final DaggerSingletonObject<CustomWidgetManager> INSTANCE =
+            new DaggerSingletonObject<>(LauncherBaseAppComponent::getCustomWidgetManager);
 
     private static final String TAG = "CustomWidgetManager";
     private static final String PLUGIN_PKG = "android";
     private final Context mContext;
     private final HashMap<ComponentName, CustomWidgetPlugin> mPlugins;
     private final List<CustomAppWidgetProviderInfo> mCustomWidgets;
-    private Consumer<PackageUserKey> mWidgetRefreshCallback;
+    private final List<Runnable> mWidgetRefreshCallbacks = new CopyOnWriteArrayList<>();
     private final @NonNull AppWidgetManager mAppWidgetManager;
 
-    private CustomWidgetManager(Context context) {
-        this(context, AppWidgetManager.getInstance(context));
+    @Inject
+    CustomWidgetManager(@ApplicationContext Context context, PluginManagerWrapper pluginManager,
+            DaggerSingletonTracker tracker) {
+        this(context, pluginManager, AppWidgetManager.getInstance(context), tracker);
     }
 
     @VisibleForTesting
-    CustomWidgetManager(Context context, @NonNull AppWidgetManager widgetManager) {
+    CustomWidgetManager(@ApplicationContext Context context,
+            PluginManagerWrapper pluginManager,
+            @NonNull AppWidgetManager widgetManager,
+            DaggerSingletonTracker tracker) {
         mContext = context;
         mAppWidgetManager = widgetManager;
         mPlugins = new HashMap<>();
         mCustomWidgets = new ArrayList<>();
-        PluginManagerWrapper.INSTANCE.get(context)
-                .addPluginListener(this, CustomWidgetPlugin.class, true);
 
+        pluginManager.addPluginListener(this, CustomWidgetPlugin.class, true);
         if (enableSmartspaceAsAWidget()) {
             for (String s: context.getResources()
                     .getStringArray(R.array.custom_widget_providers)) {
@@ -86,40 +97,34 @@
                     Class<?> cls = Class.forName(s);
                     CustomWidgetPlugin plugin = (CustomWidgetPlugin)
                             cls.getDeclaredConstructor(Context.class).newInstance(context);
-                    onPluginConnected(plugin, context);
-                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException
+                    MAIN_EXECUTOR.execute(() -> onPluginConnected(plugin, context));
+                } catch (ClassNotFoundException | InstantiationException
+                         | IllegalAccessException
                          | ClassCastException | NoSuchMethodException
                          | InvocationTargetException e) {
                     Log.e(TAG, "Exception found when trying to add custom widgets: " + e);
                 }
             }
         }
-    }
-
-    @Override
-    public void close() {
-        PluginManagerWrapper.INSTANCE.get(mContext).removePluginListener(this);
+        tracker.addCloseable(() -> pluginManager.removePluginListener(this));
     }
 
     @Override
     public void onPluginConnected(CustomWidgetPlugin plugin, Context context) {
-        List<AppWidgetProviderInfo> providers = mAppWidgetManager
-                .getInstalledProvidersForProfile(Process.myUserHandle());
-        if (providers.isEmpty()) return;
-        Parcel parcel = Parcel.obtain();
-        providers.get(0).writeToParcel(parcel, 0);
-        parcel.setDataPosition(0);
-        CustomAppWidgetProviderInfo info = newInfo(plugin, parcel);
-        parcel.recycle();
-        mPlugins.put(info.provider, plugin);
-        mCustomWidgets.add(info);
+        CustomAppWidgetProviderInfo info = getAndAddInfo(new ComponentName(
+                PLUGIN_PKG, CLS_CUSTOM_WIDGET_PREFIX + plugin.getClass().getName()));
+        if (info != null) {
+            plugin.updateWidgetInfo(info, mContext);
+            mPlugins.put(info.provider, plugin);
+            mWidgetRefreshCallbacks.forEach(MAIN_EXECUTOR::execute);
+        }
     }
 
     @Override
     public void onPluginDisconnected(CustomWidgetPlugin plugin) {
-        ComponentName cn = getWidgetProviderComponent(plugin);
-        mPlugins.remove(cn);
-        mCustomWidgets.removeIf(w -> w.getComponent().equals(cn));
+        // Leave the providerInfo as plugins can get disconnected/reconnected multiple times
+        mPlugins.values().remove(plugin);
+        mWidgetRefreshCallbacks.forEach(MAIN_EXECUTOR::execute);
     }
 
     @VisibleForTesting
@@ -130,9 +135,11 @@
 
     /**
      * Inject a callback function to refresh the widgets.
+     * @return a closeable to remove this callback
      */
-    public void setWidgetRefreshCallback(Consumer<PackageUserKey> cb) {
-        mWidgetRefreshCallback = cb;
+    public SafeCloseable addWidgetRefreshCallback(Runnable callback) {
+        mWidgetRefreshCallbacks.add(callback);
+        return () -> mWidgetRefreshCallbacks.remove(callback);
     }
 
     /**
@@ -141,8 +148,9 @@
     public void onViewCreated(LauncherAppWidgetHostView view) {
         CustomAppWidgetProviderInfo info = (CustomAppWidgetProviderInfo) view.getAppWidgetInfo();
         CustomWidgetPlugin plugin = mPlugins.get(info.provider);
-        if (plugin == null) return;
-        plugin.onViewCreated(view);
+        if (plugin != null) {
+            plugin.onViewCreated(view);
+        }
     }
 
     /**
@@ -158,14 +166,13 @@
      */
     @Nullable
     public LauncherAppWidgetProviderInfo getWidgetProvider(ComponentName cn) {
-        return mCustomWidgets.stream()
+        LauncherAppWidgetProviderInfo info = mCustomWidgets.stream()
                 .filter(w -> w.getComponent().equals(cn)).findAny().orElse(null);
-    }
-
-    private CustomAppWidgetProviderInfo newInfo(CustomWidgetPlugin plugin, Parcel parcel) {
-        CustomAppWidgetProviderInfo info = new CustomAppWidgetProviderInfo(parcel, false);
-        info.provider = getWidgetProviderComponent(plugin);
-        plugin.updateWidgetInfo(info, mContext);
+        if (info == null) {
+            // If the info is not present, add a placeholder info since the
+            // plugin might get loaded later
+            info = getAndAddInfo(cn);
+        }
         return info;
     }
 
@@ -176,8 +183,24 @@
         return CUSTOM_WIDGET_ID - mCustomWidgets.indexOf(getWidgetProvider(componentName));
     }
 
-    private ComponentName getWidgetProviderComponent(CustomWidgetPlugin plugin) {
-        return new ComponentName(
-                PLUGIN_PKG, CLS_CUSTOM_WIDGET_PREFIX + plugin.getClass().getName());
+    @Nullable
+    private CustomAppWidgetProviderInfo getAndAddInfo(ComponentName cn) {
+        for (CustomAppWidgetProviderInfo info : mCustomWidgets) {
+            if (info.provider.equals(cn)) return info;
+        }
+
+        List<AppWidgetProviderInfo> providers = mAppWidgetManager
+                .getInstalledProvidersForProfile(Process.myUserHandle());
+        if (providers.isEmpty()) return null;
+        Parcel parcel = Parcel.obtain();
+        providers.get(0).writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+        CustomAppWidgetProviderInfo info = new CustomAppWidgetProviderInfo(parcel, false);
+        parcel.recycle();
+
+        info.provider = cn;
+        info.initialLayout = 0;
+        mCustomWidgets.add(info);
+        return info;
     }
 }
diff --git a/src/com/android/launcher3/widget/model/WidgetsListExpandActionEntry.java b/src/com/android/launcher3/widget/model/WidgetsListExpandActionEntry.java
new file mode 100644
index 0000000..8c84030
--- /dev/null
+++ b/src/com/android/launcher3/widget/model/WidgetsListExpandActionEntry.java
@@ -0,0 +1,38 @@
+/*
+ * 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.model;
+
+import android.os.Process;
+
+import com.android.launcher3.model.data.PackageItemInfo;
+
+import java.util.Collections;
+
+/**
+ * Binds the section to be displayed at the bottom of the widgets list that enables user to expand
+ * and view all the widget apps including non-default. Bound when
+ * {@link WidgetsListExpandActionEntry} exists in the list on adapter.
+ */
+public class WidgetsListExpandActionEntry extends WidgetsListBaseEntry {
+
+    public WidgetsListExpandActionEntry() {
+        super(/*pkgItem=*/ new PackageItemInfo(/* packageName= */ "", Process.myUserHandle()),
+                /*titleSectionName=*/ "",
+                /*items=*/ Collections.EMPTY_LIST);
+        mPkgItem.title = "";
+    }
+}
diff --git a/src/com/android/launcher3/widget/picker/WidgetRecommendationCategoryProvider.java b/src/com/android/launcher3/widget/picker/WidgetRecommendationCategoryProvider.java
index 9253b37..f8dc6b0 100644
--- a/src/com/android/launcher3/widget/picker/WidgetRecommendationCategoryProvider.java
+++ b/src/com/android/launcher3/widget/picker/WidgetRecommendationCategoryProvider.java
@@ -24,7 +24,7 @@
 
 import com.android.launcher3.R;
 import com.android.launcher3.model.WidgetItem;
-import com.android.launcher3.util.PackageManagerHelper;
+import com.android.launcher3.util.ApplicationInfoWrapper;
 import com.android.launcher3.util.Preconditions;
 import com.android.launcher3.util.ResourceBasedOverride;
 
@@ -62,14 +62,14 @@
         // via the overridden WidgetRecommendationCategoryProvider resource.
 
         Preconditions.assertWorkerThread();
-        try (PackageManagerHelper pmHelper = new PackageManagerHelper(context)) {
-            if (item.widgetInfo != null && item.widgetInfo.getComponent() != null) {
-                ApplicationInfo applicationInfo = pmHelper.getApplicationInfo(
-                        item.widgetInfo.getComponent().getPackageName(), item.widgetInfo.getUser(),
-                        0 /* flags */);
-                if (applicationInfo != null) {
-                    return getCategoryFromApplicationCategory(applicationInfo.category);
-                }
+        if (item.widgetInfo != null && item.widgetInfo.getComponent() != null) {
+            ApplicationInfo applicationInfo = new ApplicationInfoWrapper(
+                    context,
+                    item.widgetInfo.getComponent().getPackageName(),
+                    item.widgetInfo.getUser())
+                    .getInfo();
+            if (applicationInfo != null) {
+                return getCategoryFromApplicationCategory(applicationInfo.category);
             }
         }
         return null;
diff --git a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
index c8ad564..150806a 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
@@ -16,12 +16,16 @@
 package com.android.launcher3.widget.picker;
 
 import static com.android.launcher3.Flags.enableCategorizedWidgetSuggestions;
+import static com.android.launcher3.Flags.enableTieredWidgetsByDefaultInPicker;
 import static com.android.launcher3.Flags.enableUnfoldedTwoPanePicker;
 import static com.android.launcher3.allapps.ActivityAllAppsContainerView.AdapterHolder.SEARCH;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WIDGETSTRAY_EXPAND_PRESS;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WIDGETSTRAY_SEARCHED;
 import static com.android.launcher3.testing.shared.TestProtocol.NORMAL_STATE_ORDINAL;
 import static com.android.launcher3.views.RecyclerViewFastScroller.FastScrollerLocation.WIDGET_SCROLLER;
 
+import static java.util.Collections.emptyList;
+
 import android.animation.Animator;
 import android.content.Context;
 import android.content.res.Resources;
@@ -68,6 +72,7 @@
 import com.android.launcher3.widget.BaseWidgetSheet;
 import com.android.launcher3.widget.WidgetCell;
 import com.android.launcher3.widget.model.WidgetsListBaseEntry;
+import com.android.launcher3.widget.picker.model.data.WidgetPickerData;
 import com.android.launcher3.widget.picker.search.SearchModeListener;
 import com.android.launcher3.widget.picker.search.WidgetsSearchBar;
 import com.android.launcher3.widget.picker.search.WidgetsSearchBar.WidgetsSearchDataProvider;
@@ -87,7 +92,8 @@
  */
 public class WidgetsFullSheet extends BaseWidgetSheet
         implements OnActivePageChangedListener,
-        WidgetsRecyclerView.HeaderViewDimensionsProvider, SearchModeListener {
+        WidgetsRecyclerView.HeaderViewDimensionsProvider, SearchModeListener,
+        WidgetsListAdapter.ExpandButtonClickListener {
 
     private static final long FADE_IN_DURATION = 150;
 
@@ -145,7 +151,9 @@
     protected DeviceProfile mDeviceProfile;
 
     protected TextView mNoWidgetsView;
-    protected StickyHeaderLayout mSearchScrollView;
+    protected LinearLayout mSearchScrollView;
+    // Reference to the mSearchScrollView when it is is a sticky header.
+    private @Nullable StickyHeaderLayout mStickyHeaderLayout;
     protected WidgetRecommendationsView mWidgetRecommendationsView;
     protected LinearLayout mWidgetRecommendationsContainer;
     protected View mTabBar;
@@ -220,7 +228,11 @@
 
     protected void setupViews() {
         mSearchScrollView = findViewById(R.id.search_and_recommendations_container);
-        mSearchScrollView.setCurrentRecyclerView(findViewById(R.id.primary_widgets_list_view));
+        if (mSearchScrollView instanceof StickyHeaderLayout) {
+            mStickyHeaderLayout = (StickyHeaderLayout) mSearchScrollView;
+            mStickyHeaderLayout.setCurrentRecyclerView(
+                    findViewById(R.id.primary_widgets_list_view));
+        }
         mNoWidgetsView = findViewById(R.id.no_widgets_text);
         mFastScroller = findViewById(R.id.fast_scroller);
         mFastScroller.setPopupView(findViewById(R.id.fast_scroller_popup));
@@ -251,7 +263,13 @@
         mSearchBar.initialize(new WidgetsSearchDataProvider() {
             @Override
             public List<WidgetsListBaseEntry> getWidgets() {
-                return getWidgetsToDisplay();
+                if (enableTieredWidgetsByDefaultInPicker()) {
+                    // search all
+                    return mActivityContext.getWidgetPickerDataProvider().get().getAllWidgets();
+                } else {
+                    // Can be removed when inlining enableTieredWidgetsByDefaultInPicker flag
+                    return getWidgetsToDisplay();
+                }
             }
         }, /* searchModeListener= */ this);
     }
@@ -277,14 +295,19 @@
     }
 
     private void attachScrollbarToRecyclerView(WidgetsRecyclerView recyclerView) {
-        recyclerView.bindFastScrollbar(mFastScroller, WIDGET_SCROLLER);
         if (mCurrentWidgetsRecyclerView != recyclerView) {
+            // Bind scrollbar if changing the recycler view. If widgets list updates, since
+            // scrollbar is already attached to the recycler view, it will automatically adjust as
+            // needed with recycler view's onScrollListener.
+            recyclerView.bindFastScrollbar(mFastScroller, WIDGET_SCROLLER);
             // Only reset the scroll position & expanded apps if the currently shown recycler view
             // has been updated.
             reset();
             resetExpandedHeaders();
             mCurrentWidgetsRecyclerView = recyclerView;
-            mSearchScrollView.setCurrentRecyclerView(recyclerView);
+            if (mStickyHeaderLayout != null) {
+                mStickyHeaderLayout.setCurrentRecyclerView(recyclerView);
+            }
         }
     }
 
@@ -313,7 +336,9 @@
             mAdapters.get(AdapterHolder.WORK).mWidgetsRecyclerView.scrollToTop();
         }
         mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView.scrollToTop();
-        mSearchScrollView.reset(/* animate= */ true);
+        if (mStickyHeaderLayout != null) {
+            mStickyHeaderLayout.reset(/* animate= */ true);
+        }
     }
 
     @VisibleForTesting
@@ -472,6 +497,9 @@
     /**
      * Returns all displayable widgets.
      */
+    // Used by the two pane sheet to show 3-dot menu to toggle between default lists and all lists
+    // when enableTieredWidgetsByDefaultInPicker is OFF. This code path and the 3-dot menu can be
+    // safely deleted when it's alternative "enableTieredWidgetsByDefaultInPicker" flag is inlined.
     protected List<WidgetsListBaseEntry> getWidgetsToDisplay() {
         return mActivityContext.getWidgetPickerDataProvider().get().getAllWidgets();
     }
@@ -481,16 +509,27 @@
         if (mIsInSearchMode) {
             return;
         }
-        List<WidgetsListBaseEntry> widgets = getWidgetsToDisplay();
+        List<WidgetsListBaseEntry> widgets;
+        List<WidgetsListBaseEntry> defaultWidgets = emptyList();
+
+        if (enableTieredWidgetsByDefaultInPicker()) {
+            WidgetPickerData dataProvider =
+                    mActivityContext.getWidgetPickerDataProvider().get();
+            widgets = dataProvider.getAllWidgets();
+            defaultWidgets = dataProvider.getDefaultWidgets();
+        } else {
+            // This code path can be deleted once enableTieredWidgetsByDefaultInPicker is inlined.
+            widgets = getWidgetsToDisplay();
+        }
 
         AdapterHolder primaryUserAdapterHolder = mAdapters.get(AdapterHolder.PRIMARY);
-        primaryUserAdapterHolder.mWidgetsListAdapter.setWidgets(widgets);
+        primaryUserAdapterHolder.mWidgetsListAdapter.setWidgets(widgets, defaultWidgets);
 
         if (mHasWorkProfile) {
             mViewPager.setVisibility(VISIBLE);
             mTabBar.setVisibility(VISIBLE);
             AdapterHolder workUserAdapterHolder = mAdapters.get(AdapterHolder.WORK);
-            workUserAdapterHolder.mWidgetsListAdapter.setWidgets(widgets);
+            workUserAdapterHolder.mWidgetsListAdapter.setWidgets(widgets, defaultWidgets);
             onActivePageChanged(mViewPager.getCurrentPage());
         } else {
             onActivePageChanged(0);
@@ -508,6 +547,16 @@
     }
 
     @Override
+    public void onWidgetsListExpandButtonClick(View v) {
+        AdapterHolder currentAdapterHolder = mAdapters.get(getCurrentAdapterHolderType());
+        currentAdapterHolder.mWidgetsListAdapter.useExpandedList();
+        onWidgetsBound();
+        currentAdapterHolder.mWidgetsRecyclerView.announceForAccessibility(
+                mActivityContext.getString(R.string.widgets_list_expanded));
+        mActivityContext.getStatsLogManager().logger().log(LAUNCHER_WIDGETSTRAY_EXPAND_PRESS);
+    }
+
+    @Override
     public void enterSearchMode(boolean shouldLog) {
         if (mIsInSearchMode) return;
         setViewVisibilityBasedOnSearch(/*isInSearchMode= */ true);
@@ -559,9 +608,12 @@
             mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView.setVisibility(GONE);
             mAdapters.get(getCurrentAdapterHolderType()).mWidgetsRecyclerView.setVisibility(
                     VISIBLE);
-            // Visibility of recommended widgets, recycler views and headers are handled in methods
-            // below.
-            post(this::onRecommendedWidgetsBound);
+            if (mRecommendedWidgetsCount > 0) {
+                // Display recommendations immediately, if present, so that other parts of sticky
+                // header (e.g. personal / work tabs) don't flash in interim.
+                mWidgetRecommendationsContainer.setVisibility(VISIBLE);
+            }
+            // Visibility of recycler views and headers are handled in methods below.
             onWidgetsBound();
         }
     }
@@ -822,7 +874,7 @@
                 + marginLayoutParams.topMargin;
     }
 
-    private int getCurrentAdapterHolderType() {
+    protected int getCurrentAdapterHolderType() {
         if (mIsInSearchMode) {
             return SEARCH;
         } else if (mViewPager != null) {
@@ -851,6 +903,7 @@
             WidgetsFullSheet sheet = show(BaseActivity.fromContext(getContext()), false);
             sheet.restoreRecommendations(mRecommendedWidgets, mRecommendedWidgetsMap);
             sheet.restoreHierarchyState(widgetsState);
+            sheet.restoreAdapterStates(mAdapters);
             sheet.restorePreviousAdapterHolderType(getCurrentAdapterHolderType());
         } else if (!isTwoPane()) {
             reset();
@@ -866,6 +919,17 @@
         mRecommendedWidgetsMap = recommendedWidgetsMap;
     }
 
+    private void restoreAdapterStates(SparseArray<AdapterHolder> adapters) {
+        if (adapters.contains(AdapterHolder.WORK)) {
+            mAdapters.get(AdapterHolder.WORK).mWidgetsListAdapter.restoreState(
+                    adapters.get(AdapterHolder.WORK).mWidgetsListAdapter);
+        }
+        mAdapters.get(AdapterHolder.PRIMARY).mWidgetsListAdapter.restoreState(
+                adapters.get(AdapterHolder.PRIMARY).mWidgetsListAdapter);
+        mAdapters.get(AdapterHolder.SEARCH).mWidgetsListAdapter.restoreState(
+                adapters.get(AdapterHolder.SEARCH).mWidgetsListAdapter);
+    }
+
     /**
      * Indicates if layout should be re-created on device profile change - so that a different
      * layout can be displayed.
@@ -1035,6 +1099,7 @@
                     this::getEmptySpaceHeight,
                     /* iconClickListener= */ WidgetsFullSheet.this,
                     /* iconLongClickListener= */ WidgetsFullSheet.this,
+                    /* expandButtonClickListener= */ WidgetsFullSheet.this,
                     isTwoPane());
             mWidgetsListAdapter.setHasStableIds(true);
             switch (mAdapterType) {
@@ -1051,7 +1116,7 @@
         }
 
         private int getEmptySpaceHeight() {
-            return mSearchScrollView.getHeaderHeight();
+            return mStickyHeaderLayout != null ? mStickyHeaderLayout.getHeaderHeight() : 0;
         }
 
         void setup(WidgetsRecyclerView recyclerView) {
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java b/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java
index 8dd1de4..74a9a5c 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java
@@ -49,6 +49,7 @@
 import com.android.launcher3.widget.model.WidgetListSpaceEntry;
 import com.android.launcher3.widget.model.WidgetsListBaseEntry;
 import com.android.launcher3.widget.model.WidgetsListContentEntry;
+import com.android.launcher3.widget.model.WidgetsListExpandActionEntry;
 import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
 import com.android.launcher3.widget.util.WidgetSizes;
 
@@ -82,6 +83,7 @@
     public static final int VIEW_TYPE_WIDGETS_SPACE = R.id.view_type_widgets_space;
     public static final int VIEW_TYPE_WIDGETS_LIST = R.id.view_type_widgets_list;
     public static final int VIEW_TYPE_WIDGETS_HEADER = R.id.view_type_widgets_header;
+    public static final int VIEW_TYPE_WIDGETS_EXPAND = R.id.view_type_widgets_list_expand;
 
     private final Context mContext;
     private final SparseArray<ViewHolderBinder> mViewHolderBinders = new SparseArray<>();
@@ -90,7 +92,9 @@
     @Nullable private WidgetsTwoPaneSheet.HeaderChangeListener mHeaderChangeListener;
 
     private final List<WidgetsListBaseEntry> mAllEntries = new ArrayList<>();
+    private final List<WidgetsListBaseEntry> mAllDefaultEntries = new ArrayList<>();
     private ArrayList<WidgetsListBaseEntry> mVisibleEntries = new ArrayList<>();
+
     @Nullable private PackageUserKey mWidgetsContentVisiblePackageUserKey = null;
 
     private Predicate<WidgetsListBaseEntry> mHeaderAndSelectedContentFilter = entry ->
@@ -99,12 +103,15 @@
                             .equals(mWidgetsContentVisiblePackageUserKey);
     @Nullable private Predicate<WidgetsListBaseEntry> mFilter = null;
     @Nullable private RecyclerView mRecyclerView;
-    @Nullable private PackageUserKey mPendingClickHeader;
+    @Nullable private PackageUserKey mHeaderPositionToMaintain;
     @Px private int mMaxHorizontalSpan;
 
+    private boolean mShowOnlyDefaultList = true;
+
     public WidgetsListAdapter(Context context, LayoutInflater layoutInflater,
             IntSupplier emptySpaceHeightProvider, OnClickListener iconClickListener,
             OnLongClickListener iconLongClickListener,
+            ExpandButtonClickListener expandButtonClickListener,
             boolean isTwoPane) {
         mContext = context;
         mMaxHorizontalSpan = WidgetSizes.getWidgetSizePx(
@@ -123,6 +130,16 @@
         mViewHolderBinders.put(
                 VIEW_TYPE_WIDGETS_SPACE,
                 new WidgetsSpaceViewHolderBinder(emptySpaceHeightProvider));
+        mViewHolderBinders.put(VIEW_TYPE_WIDGETS_EXPAND,
+                new WidgetsListExpandActionViewHolderBinder(layoutInflater,
+                        expandButtonClickListener::onWidgetsListExpandButtonClick));
+    }
+
+    /**
+     * Copies state info from another adapter.
+     */
+    public void restoreState(WidgetsListAdapter adapter) {
+        mShowOnlyDefaultList = adapter.mShowOnlyDefaultList;
     }
 
     public void setHeaderChangeListener(WidgetsTwoPaneSheet.HeaderChangeListener
@@ -168,10 +185,21 @@
     }
 
     /** Updates the widget list based on {@code tempEntries}. */
-    public void setWidgets(List<WidgetsListBaseEntry> tempEntries) {
+    public void setWidgets(List<WidgetsListBaseEntry> tempEntries,
+            List<WidgetsListBaseEntry> tempDefaultEntries) {
         mAllEntries.clear();
         mAllEntries.add(new WidgetListSpaceEntry());
         tempEntries.stream().sorted(mRowComparator).forEach(mAllEntries::add);
+
+        mAllDefaultEntries.clear();
+
+        if (mShowOnlyDefaultList && !tempDefaultEntries.isEmpty()) {
+            mAllDefaultEntries.add(new WidgetListSpaceEntry());
+            tempDefaultEntries.stream().sorted(mRowComparator).forEach(mAllDefaultEntries::add);
+            // Include view all action when default entries exist.
+            mAllDefaultEntries.add(new WidgetsListExpandActionEntry());
+        }
+
         updateVisibleEntries();
     }
 
@@ -179,21 +207,23 @@
     public void setWidgetsOnSearch(List<WidgetsListBaseEntry> searchResults) {
         // Forget the expanded package every time widget list is refreshed in search mode.
         mWidgetsContentVisiblePackageUserKey = null;
-        setWidgets(searchResults);
+        mShowOnlyDefaultList = false;
+        setWidgets(searchResults, /*tempDefaultEntries=*/ List.of());
     }
 
     private void updateVisibleEntries() {
         // Get the current top of the header with the matching key before adjusting the visible
         // entries.
         OptionalInt previousPositionForPackageUserKey =
-                getPositionForPackageUserKey(mPendingClickHeader);
+                getPositionForPackageUserKey(mHeaderPositionToMaintain);
         OptionalInt topForPackageUserKey =
                 getOffsetForPosition(previousPositionForPackageUserKey);
 
-        List<WidgetsListBaseEntry> newVisibleEntries = mAllEntries.stream()
+        List<WidgetsListBaseEntry> newVisibleEntries = getAllEntries().stream()
                 .filter(entry -> (((mFilter == null || mFilter.test(entry))
                         && mHeaderAndSelectedContentFilter.test(entry))
-                        || entry instanceof WidgetListSpaceEntry)
+                        || entry instanceof WidgetListSpaceEntry
+                        || entry instanceof WidgetsListExpandActionEntry)
                         && (mHeaderChangeListener == null
                         || !(entry instanceof WidgetsListContentEntry)))
                 .map(entry -> {
@@ -217,16 +247,23 @@
         mVisibleEntries.addAll(newVisibleEntries);
         diffResult.dispatchUpdatesTo(this);
 
-        if (mPendingClickHeader != null) {
+        if (mHeaderPositionToMaintain != null && mRecyclerView != null) {
             // Get the position for the clicked header after adjusting the visible entries. The
             // position may have changed if another header had previously been expanded.
             OptionalInt positionForPackageUserKey =
-                    getPositionForPackageUserKey(mPendingClickHeader);
-            scrollToPositionAndMaintainOffset(positionForPackageUserKey, topForPackageUserKey);
-            mPendingClickHeader = null;
+                    getPositionForPackageUserKey(mHeaderPositionToMaintain);
+            // Post scroll updates to be applied after diff updates.
+            mRecyclerView.post(() -> scrollToPositionAndMaintainOffset(positionForPackageUserKey,
+                    topForPackageUserKey));
+            mHeaderPositionToMaintain = null;
         }
     }
 
+    private List<WidgetsListBaseEntry> getAllEntries() {
+        return (mShowOnlyDefaultList && !mAllDefaultEntries.isEmpty()) ? mAllDefaultEntries
+                : mAllEntries;
+    }
+
     /** Returns whether {@code entry} matches {@code key}. */
     private static boolean isHeaderForPackageUserKey(
             @NonNull WidgetsListBaseEntry entry, @Nullable PackageUserKey key) {
@@ -262,7 +299,13 @@
 
         // The first entry has an empty space, count from second entries.
         int listPos = (pos > 1) ? POSITION_DEFAULT : POSITION_FIRST;
-        if (pos == (getItemCount() - 1)) {
+        int lastIndex = getItemCount() - 1;
+        // Last index may be the view all entry
+        int actualLastItemIndex = (mVisibleEntries.get(
+                lastIndex) instanceof WidgetsListExpandActionEntry) ? getItemCount() - 2
+                : getItemCount() - 1;
+
+        if (pos == (actualLastItemIndex)) {
             listPos |= POSITION_LAST;
         }
         viewHolderBinder.bindViewHolder(holder, mVisibleEntries.get(pos), listPos, payloads);
@@ -319,6 +362,8 @@
             return VIEW_TYPE_WIDGETS_HEADER;
         } else if (entry instanceof WidgetListSpaceEntry) {
             return VIEW_TYPE_WIDGETS_SPACE;
+        } else if (entry instanceof WidgetsListExpandActionEntry) {
+            return VIEW_TYPE_WIDGETS_EXPAND;
         }
         throw new UnsupportedOperationException("ViewHolderBinder not found for " + entry);
     }
@@ -341,7 +386,7 @@
 
         // Store the header that was clicked so that its position will be maintained the next time
         // we update the entries.
-        mPendingClickHeader = packageUserKey;
+        mHeaderPositionToMaintain = packageUserKey;
 
         updateVisibleEntries();
 
@@ -396,15 +441,6 @@
         LinearLayoutManager layoutManager = (LinearLayoutManager) mRecyclerView.getLayoutManager();
         if (layoutManager == null) return;
 
-        if (position == mVisibleEntries.size() - 2
-                && mVisibleEntries.get(mVisibleEntries.size() - 1)
-                instanceof WidgetsListContentEntry) {
-            // If the selected header is in the last position and its content is showing, then
-            // scroll to the final position so the last list of widgets will show.
-            layoutManager.scrollToPosition(mVisibleEntries.size() - 1);
-            return;
-        }
-
         // Scroll to the header view's current offset, accounting for the recycler view's padding.
         // If the header view couldn't be found, then it will appear at the top of the list.
         layoutManager.scrollToPositionWithOffset(
@@ -421,6 +457,33 @@
         updateVisibleEntries();
     }
 
+    /**
+     * Returns the widget content {@link WidgetsListContentEntry} for a selected header.
+     */
+    public WidgetsListContentEntry getContentEntry(PackageUserKey selectedHeader) {
+        return getAllEntries().stream().filter(entry -> entry instanceof WidgetsListContentEntry)
+                .map(entry -> (WidgetsListContentEntry) entry)
+                .filter(entry -> PackageUserKey.fromPackageItemInfo(entry.mPkgItem).equals(
+                        selectedHeader)).findFirst().orElse(null);
+    }
+
+    /**
+     * Sets adapter to use expanded list when updating widgets.
+     */
+    public void useExpandedList() {
+        mShowOnlyDefaultList = false;
+        if (mWidgetsContentVisiblePackageUserKey != null) {
+            // Maintain selected header for the next update that expands the list.
+            mHeaderPositionToMaintain = mWidgetsContentVisiblePackageUserKey;
+        } else if (mVisibleEntries.size() > 2) {
+            // Maintain last visible header shown above expand button since there was no selected
+            // header.
+            mHeaderPositionToMaintain = PackageUserKey.fromPackageItemInfo(
+                    mVisibleEntries.get(mVisibleEntries.size() - 2).mPkgItem);
+        }
+
+    }
+
     /** Comparator for sorting WidgetListRowEntry based on package title. */
     public static class WidgetListBaseRowEntryComparator implements
             Comparator<WidgetsListBaseEntry> {
@@ -439,4 +502,10 @@
             return 1;
         }
     }
+
+    /** Callback interface for the interaction with the expand button */
+    public interface ExpandButtonClickListener {
+        /** Called when user clicks the button at end of widget apps list to expand it. */
+        void onWidgetsListExpandButtonClick(View view);
+    }
 }
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListExpandActionViewHolderBinder.java b/src/com/android/launcher3/widget/picker/WidgetsListExpandActionViewHolderBinder.java
new file mode 100644
index 0000000..288c456
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/WidgetsListExpandActionViewHolderBinder.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.picker;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.launcher3.R;
+import com.android.launcher3.recyclerview.ViewHolderBinder;
+import com.android.launcher3.widget.model.WidgetsListExpandActionEntry;
+
+import java.util.List;
+
+/**
+ * Creates and populates views for the {@link WidgetsListExpandActionEntry}.
+ */
+public class WidgetsListExpandActionViewHolderBinder implements
+        ViewHolderBinder<WidgetsListExpandActionEntry, RecyclerView.ViewHolder> {
+    @NonNull
+    View.OnClickListener mExpandListClickListener;
+    private final LayoutInflater mLayoutInflater;
+
+    public WidgetsListExpandActionViewHolderBinder(
+            @NonNull LayoutInflater layoutInflater,
+            @NonNull View.OnClickListener expandListClickListener) {
+        mLayoutInflater = layoutInflater;
+        mExpandListClickListener = expandListClickListener;
+    }
+
+    @Override
+    public RecyclerView.ViewHolder newViewHolder(ViewGroup parent) {
+        return new RecyclerView.ViewHolder(mLayoutInflater.inflate(
+                R.layout.widgets_list_expand_button, parent, false)) {
+        };
+    }
+
+    @Override
+    public void bindViewHolder(RecyclerView.ViewHolder viewHolder,
+            WidgetsListExpandActionEntry data, int position, List<Object> payloads) {
+        viewHolder.itemView.setOnClickListener(mExpandListClickListener);
+    }
+}
diff --git a/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java b/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
index f4b99a0..f9bd5f1 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
@@ -16,6 +16,7 @@
 package com.android.launcher3.widget.picker;
 
 import static com.android.launcher3.Flags.enableCategorizedWidgetSuggestions;
+import static com.android.launcher3.Flags.enableTieredWidgetsByDefaultInPicker;
 import static com.android.launcher3.Flags.enableUnfoldedTwoPanePicker;
 import static com.android.launcher3.UtilitiesKt.CLIP_CHILDREN_FALSE_MODIFIER;
 import static com.android.launcher3.UtilitiesKt.CLIP_TO_PADDING_FALSE_MODIFIER;
@@ -147,7 +148,9 @@
         mHeaderDescription = mContent.findViewById(R.id.widget_picker_description);
 
         mWidgetOptionsMenu = mContent.findViewById(R.id.widget_picker_widget_options_menu);
-        setupWidgetOptionsMenu();
+        if (!enableTieredWidgetsByDefaultInPicker()) {
+            setupWidgetOptionsMenu();
+        }
 
         mRightPane = mContent.findViewById(R.id.right_pane);
         mRightPaneScrollView = mContent.findViewById(R.id.right_pane_scroll_view);
@@ -286,6 +289,9 @@
         }
     }
 
+    // Used by the two pane sheet to show 3-dot menu to toggle between default lists and all lists
+    // when enableTieredWidgetsByDefaultInPicker is OFF. This code path and the 3-dot menu can be
+    // safely deleted when it's alternative "enableTieredWidgetsByDefaultInPicker" flag is inlined.
     @Override
     protected List<WidgetsListBaseEntry> getWidgetsToDisplay() {
         List<WidgetsListBaseEntry> allWidgets =
@@ -319,6 +325,15 @@
     }
 
     @Override
+    public void onWidgetsListExpandButtonClick(View v) {
+        super.onWidgetsListExpandButtonClick(v);
+        // Refresh right pane with updated data for the selected header.
+        if (mSelectedHeader != null && mSelectedHeader != mSuggestedWidgetsPackageUserKey) {
+            getHeaderChangeListener().onHeaderChanged(mSelectedHeader);
+        }
+    }
+
+    @Override
     public void onRecommendedWidgetsBound() {
         super.onRecommendedWidgetsBound();
 
@@ -511,11 +526,20 @@
                         && !mOpenCloseAnimation.getAnimationPlayer().isRunning()
                         && !getAccessibilityInitialFocusView().isAccessibilityFocused();
                 mSelectedHeader = selectedHeader;
-                final boolean showDefaultWidgets = mWidgetOptionsMenuState != null
-                        && !mWidgetOptionsMenuState.showAllWidgets;
-                WidgetsListContentEntry contentEntry = findContentEntryForPackageUser(
-                        mActivityContext.getWidgetPickerDataProvider().get(),
-                        selectedHeader, showDefaultWidgets);
+
+                WidgetsListContentEntry contentEntry;
+                if (enableTieredWidgetsByDefaultInPicker()) {
+                    contentEntry = mAdapters.get(
+                            getCurrentAdapterHolderType()).mWidgetsListAdapter.getContentEntry(
+                            selectedHeader);
+                } else { // Can be deleted when inlining the "enableTieredWidgetsByDefaultInPicker"
+                    // flag
+                    final boolean showDefaultWidgets = mWidgetOptionsMenuState != null
+                            && !mWidgetOptionsMenuState.showAllWidgets;
+                    contentEntry = findContentEntryForPackageUser(
+                            mActivityContext.getWidgetPickerDataProvider().get(),
+                            selectedHeader, showDefaultWidgets);
+                }
 
                 if (contentEntry == null || mRightPane == null) {
                     return;
diff --git a/src/com/android/launcher3/widget/picker/search/WidgetsSearchBarController.java b/src/com/android/launcher3/widget/picker/search/WidgetsSearchBarController.java
index 2d96cbd..3008d18 100644
--- a/src/com/android/launcher3/widget/picker/search/WidgetsSearchBarController.java
+++ b/src/com/android/launcher3/widget/picker/search/WidgetsSearchBarController.java
@@ -37,8 +37,7 @@
  * Controller for a search bar with an edit text and a cancel button.
  */
 public class WidgetsSearchBarController implements TextWatcher,
-        SearchCallback<WidgetsListBaseEntry>,  ExtendedEditText.OnBackKeyListener,
-        View.OnKeyListener {
+        SearchCallback<WidgetsListBaseEntry>, View.OnKeyListener {
     private static final String TAG = "WidgetsSearchBarController";
     private static final boolean DEBUG = false;
 
@@ -54,7 +53,6 @@
         mSearchAlgorithm = algo;
         mInput = editText;
         mInput.addTextChangedListener(this);
-        mInput.setOnBackKeyListener(this);
         mInput.setOnKeyListener(this);
         mCancelButton = cancelButton;
         mCancelButton.setOnClickListener(v -> clearSearchResult());
@@ -108,12 +106,6 @@
     }
 
     @Override
-    public boolean onBackKey() {
-        clearFocus();
-        return true;
-    }
-
-    @Override
     public boolean onKey(View view, int keyCode, KeyEvent event) {
         if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_UP) {
             clearFocus();
diff --git a/src_no_quickstep/com/android/launcher3/util/StateManagerProtoLogProxy.java b/src_no_quickstep/com/android/launcher3/util/StateManagerProtoLogProxy.java
new file mode 100644
index 0000000..34e15f7
--- /dev/null
+++ b/src_no_quickstep/com/android/launcher3/util/StateManagerProtoLogProxy.java
@@ -0,0 +1,33 @@
+/*
+ * 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;
+
+/**
+ * Proxy class used for StateManager ProtoLog support.
+ */
+public class StateManagerProtoLogProxy {
+
+    public static void logGoToState(Object fromState, Object toState, String trace) { }
+
+    public static void logCreateAtomicAnimation(Object fromState, Object toState, String trace) { }
+
+    public static void logOnStateTransitionStart(Object state) { }
+
+    public static void logOnStateTransitionEnd(Object state) { }
+
+    public static void logCancelAnimation(boolean animationOngoing, String trace) { }
+}
diff --git a/tests/Android.bp b/tests/Android.bp
index 9f62d02..35a2275 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -98,6 +98,8 @@
         "com_android_launcher3_flags_lib",
         "com_android_wm_shell_flags_lib",
         "android.appwidget.flags-aconfig-java",
+        "platform-parametric-runner-lib",
+        "kotlin-reflect",
     ],
     manifest: "AndroidManifest-common.xml",
     platform_apis: true,
@@ -111,6 +113,9 @@
     asset_dirs: ["assets"],
     // TODO(b/319712088): re-enable use_resource_processor
     use_resource_processor: false,
+    static_libs: [
+        "kotlin-reflect",
+    ],
 }
 
 android_test {
@@ -143,6 +148,7 @@
     platform_apis: true,
     test_config: "Launcher3Tests.xml",
     data: [":Launcher3"],
+    plugins: ["dagger2-compiler"],
     test_suites: ["general-tests"],
 }
 
@@ -193,10 +199,7 @@
     name: "Launcher3RoboTests",
     srcs: [
         ":launcher3-robo-src",
-
-        // Test util classes
         ":launcher-testing-helpers-robo",
-        ":launcher-testing-shared",
     ],
     exclude_srcs: [
         //"src/com/android/launcher3/util/CellContentDimensionsTest.kt", // Failing - b/316553889
@@ -235,6 +238,7 @@
         "truth",
     ],
     instrumentation_for: "Launcher3",
+    plugins: ["dagger2-compiler"],
     upstream: true,
     strict_mode: false,
 }
diff --git a/tests/AndroidManifest-common.xml b/tests/AndroidManifest-common.xml
index 4b926a8..1ddd453 100644
--- a/tests/AndroidManifest-common.xml
+++ b/tests/AndroidManifest-common.xml
@@ -407,6 +407,36 @@
             </intent-filter>
         </activity>
 
+        <activity-alias android:name="AppIconActivity"
+            android:label="Application Icon"
+            android:exported="true"
+            android:targetActivity="com.android.launcher3.testcomponent.BaseTestingActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity-alias>
+        <activity-alias android:name="DiffIconActivity"
+            android:label="Different icon"
+            android:exported="true"
+            android:icon="@drawable/test_different_activity_icon"
+            android:targetActivity="com.android.launcher3.testcomponent.BaseTestingActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity-alias>
+        <activity-alias android:name="WrongIconActivity"
+            android:label="Wrong icon"
+            android:exported="true"
+            android:icon="@drawable/test_wrong_activity_icon"
+            android:targetActivity="com.android.launcher3.testcomponent.BaseTestingActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity-alias>
+
         <!-- Disable eager initialization of Jetpack libraries. See bug 197780098. -->
         <provider
             android:name="androidx.startup.InitializationProvider"
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/phonePortrait.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/phonePortrait.txt
index 82a6310..4c366c3 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/phonePortrait.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/phonePortrait.txt
@@ -77,6 +77,8 @@
 	hotseatBarBottomSpacePx: 126.0px (48.0dp)
 	mHotseatBarEdgePaddingPx: 0.0px (0.0dp)
 	mHotseatBarWorkspaceSpacePx: 0.0px (0.0dp)
+	inlineNavButtonsEndSpacingPx: 0.0px (0.0dp)
+	navButtonsLayoutWidthPx: 0.0px (0.0dp)
 	hotseatBarEndOffset: 0.0px (0.0dp)
 	hotseatQsbSpace: 0.0px (0.0dp)
 	hotseatQsbHeight: 0.0px (0.0dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/phonePortrait3Button.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/phonePortrait3Button.txt
index 4271105..6db9534 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/phonePortrait3Button.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/phonePortrait3Button.txt
@@ -77,6 +77,8 @@
 	hotseatBarBottomSpacePx: 147.0px (56.0dp)
 	mHotseatBarEdgePaddingPx: 0.0px (0.0dp)
 	mHotseatBarWorkspaceSpacePx: 0.0px (0.0dp)
+	inlineNavButtonsEndSpacingPx: 0.0px (0.0dp)
+	navButtonsLayoutWidthPx: 0.0px (0.0dp)
 	hotseatBarEndOffset: 0.0px (0.0dp)
 	hotseatQsbSpace: 0.0px (0.0dp)
 	hotseatQsbHeight: 0.0px (0.0dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/phoneVerticalBar.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/phoneVerticalBar.txt
index 8bd6b99..6e76b13 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/phoneVerticalBar.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/phoneVerticalBar.txt
@@ -77,6 +77,8 @@
 	hotseatBarBottomSpacePx: 0.0px (0.0dp)
 	mHotseatBarEdgePaddingPx: 63.0px (24.0dp)
 	mHotseatBarWorkspaceSpacePx: 42.0px (16.0dp)
+	inlineNavButtonsEndSpacingPx: 0.0px (0.0dp)
+	navButtonsLayoutWidthPx: 0.0px (0.0dp)
 	hotseatBarEndOffset: 0.0px (0.0dp)
 	hotseatQsbSpace: 0.0px (0.0dp)
 	hotseatQsbHeight: 0.0px (0.0dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/phoneVerticalBar3Button.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/phoneVerticalBar3Button.txt
index 8dbb413..1af9215 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/phoneVerticalBar3Button.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/phoneVerticalBar3Button.txt
@@ -77,6 +77,8 @@
 	hotseatBarBottomSpacePx: 0.0px (0.0dp)
 	mHotseatBarEdgePaddingPx: 63.0px (24.0dp)
 	mHotseatBarWorkspaceSpacePx: 42.0px (16.0dp)
+	inlineNavButtonsEndSpacingPx: 0.0px (0.0dp)
+	navButtonsLayoutWidthPx: 0.0px (0.0dp)
 	hotseatBarEndOffset: 0.0px (0.0dp)
 	hotseatQsbSpace: 0.0px (0.0dp)
 	hotseatQsbHeight: 0.0px (0.0dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/tabletLandscape.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/tabletLandscape.txt
index ab4b286..958597f 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/tabletLandscape.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/tabletLandscape.txt
@@ -77,6 +77,8 @@
 	hotseatBarBottomSpacePx: 80.0px (40.0dp)
 	mHotseatBarEdgePaddingPx: 0.0px (0.0dp)
 	mHotseatBarWorkspaceSpacePx: 0.0px (0.0dp)
+	inlineNavButtonsEndSpacingPx: 0.0px (0.0dp)
+	navButtonsLayoutWidthPx: 0.0px (0.0dp)
 	hotseatBarEndOffset: 0.0px (0.0dp)
 	hotseatQsbSpace: 0.0px (0.0dp)
 	hotseatQsbHeight: 0.0px (0.0dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/tabletLandscape3Button.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/tabletLandscape3Button.txt
index 80835bc..aad67b4 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/tabletLandscape3Button.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/tabletLandscape3Button.txt
@@ -77,6 +77,8 @@
 	hotseatBarBottomSpacePx: 80.0px (40.0dp)
 	mHotseatBarEdgePaddingPx: 0.0px (0.0dp)
 	mHotseatBarWorkspaceSpacePx: 0.0px (0.0dp)
+	inlineNavButtonsEndSpacingPx: 0.0px (0.0dp)
+	navButtonsLayoutWidthPx: 0.0px (0.0dp)
 	hotseatBarEndOffset: 0.0px (0.0dp)
 	hotseatQsbSpace: 0.0px (0.0dp)
 	hotseatQsbHeight: 0.0px (0.0dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/tabletPortrait.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/tabletPortrait.txt
index fc53107..090e54b 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/tabletPortrait.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/tabletPortrait.txt
@@ -77,6 +77,8 @@
 	hotseatBarBottomSpacePx: 152.0px (76.0dp)
 	mHotseatBarEdgePaddingPx: 0.0px (0.0dp)
 	mHotseatBarWorkspaceSpacePx: 0.0px (0.0dp)
+	inlineNavButtonsEndSpacingPx: 0.0px (0.0dp)
+	navButtonsLayoutWidthPx: 0.0px (0.0dp)
 	hotseatBarEndOffset: 0.0px (0.0dp)
 	hotseatQsbSpace: 0.0px (0.0dp)
 	hotseatQsbHeight: 0.0px (0.0dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/tabletPortrait3Button.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/tabletPortrait3Button.txt
index 836819f..43b1a65 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/tabletPortrait3Button.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/tabletPortrait3Button.txt
@@ -77,6 +77,8 @@
 	hotseatBarBottomSpacePx: 152.0px (76.0dp)
 	mHotseatBarEdgePaddingPx: 0.0px (0.0dp)
 	mHotseatBarWorkspaceSpacePx: 0.0px (0.0dp)
+	inlineNavButtonsEndSpacingPx: 0.0px (0.0dp)
+	navButtonsLayoutWidthPx: 0.0px (0.0dp)
 	hotseatBarEndOffset: 0.0px (0.0dp)
 	hotseatQsbSpace: 0.0px (0.0dp)
 	hotseatQsbHeight: 0.0px (0.0dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape.txt
index 108182f..fe5737e 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape.txt
@@ -77,6 +77,8 @@
 	hotseatBarBottomSpacePx: 126.0px (48.0dp)
 	mHotseatBarEdgePaddingPx: 0.0px (0.0dp)
 	mHotseatBarWorkspaceSpacePx: 0.0px (0.0dp)
+	inlineNavButtonsEndSpacingPx: 0.0px (0.0dp)
+	navButtonsLayoutWidthPx: 0.0px (0.0dp)
 	hotseatBarEndOffset: 0.0px (0.0dp)
 	hotseatQsbSpace: 0.0px (0.0dp)
 	hotseatQsbHeight: 0.0px (0.0dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape3Button.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape3Button.txt
index 313d2a3..36e47a0 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape3Button.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape3Button.txt
@@ -77,6 +77,8 @@
 	hotseatBarBottomSpacePx: 126.0px (48.0dp)
 	mHotseatBarEdgePaddingPx: 0.0px (0.0dp)
 	mHotseatBarWorkspaceSpacePx: 0.0px (0.0dp)
+	inlineNavButtonsEndSpacingPx: 0.0px (0.0dp)
+	navButtonsLayoutWidthPx: 0.0px (0.0dp)
 	hotseatBarEndOffset: 0.0px (0.0dp)
 	hotseatQsbSpace: 0.0px (0.0dp)
 	hotseatQsbHeight: 0.0px (0.0dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape3Button_decoupleDepth.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape3Button_decoupleDepth.txt
index 46cce24..52fea05 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape3Button_decoupleDepth.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape3Button_decoupleDepth.txt
@@ -77,6 +77,8 @@
 	hotseatBarBottomSpacePx: 126.0px (48.0dp)
 	mHotseatBarEdgePaddingPx: 0.0px (0.0dp)
 	mHotseatBarWorkspaceSpacePx: 0.0px (0.0dp)
+	inlineNavButtonsEndSpacingPx: 0.0px (0.0dp)
+	navButtonsLayoutWidthPx: 0.0px (0.0dp)
 	hotseatBarEndOffset: 0.0px (0.0dp)
 	hotseatQsbSpace: 0.0px (0.0dp)
 	hotseatQsbHeight: 0.0px (0.0dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape_decoupleDepth.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape_decoupleDepth.txt
index 44b99e9..6d972a8 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape_decoupleDepth.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape_decoupleDepth.txt
@@ -77,6 +77,8 @@
 	hotseatBarBottomSpacePx: 126.0px (48.0dp)
 	mHotseatBarEdgePaddingPx: 0.0px (0.0dp)
 	mHotseatBarWorkspaceSpacePx: 0.0px (0.0dp)
+	inlineNavButtonsEndSpacingPx: 0.0px (0.0dp)
+	navButtonsLayoutWidthPx: 0.0px (0.0dp)
 	hotseatBarEndOffset: 0.0px (0.0dp)
 	hotseatQsbSpace: 0.0px (0.0dp)
 	hotseatQsbHeight: 0.0px (0.0dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait.txt
index fb392a8..417353d 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait.txt
@@ -77,6 +77,8 @@
 	hotseatBarBottomSpacePx: 126.0px (48.0dp)
 	mHotseatBarEdgePaddingPx: 0.0px (0.0dp)
 	mHotseatBarWorkspaceSpacePx: 0.0px (0.0dp)
+	inlineNavButtonsEndSpacingPx: 0.0px (0.0dp)
+	navButtonsLayoutWidthPx: 0.0px (0.0dp)
 	hotseatBarEndOffset: 0.0px (0.0dp)
 	hotseatQsbSpace: 0.0px (0.0dp)
 	hotseatQsbHeight: 0.0px (0.0dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait3Button.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait3Button.txt
index 2c4b3c3..03dc23a 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait3Button.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait3Button.txt
@@ -77,6 +77,8 @@
 	hotseatBarBottomSpacePx: 126.0px (48.0dp)
 	mHotseatBarEdgePaddingPx: 0.0px (0.0dp)
 	mHotseatBarWorkspaceSpacePx: 0.0px (0.0dp)
+	inlineNavButtonsEndSpacingPx: 0.0px (0.0dp)
+	navButtonsLayoutWidthPx: 0.0px (0.0dp)
 	hotseatBarEndOffset: 0.0px (0.0dp)
 	hotseatQsbSpace: 0.0px (0.0dp)
 	hotseatQsbHeight: 0.0px (0.0dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait3Button_decoupleDepth.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait3Button_decoupleDepth.txt
index e7b72f2..45d3171 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait3Button_decoupleDepth.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait3Button_decoupleDepth.txt
@@ -77,6 +77,8 @@
 	hotseatBarBottomSpacePx: 126.0px (48.0dp)
 	mHotseatBarEdgePaddingPx: 0.0px (0.0dp)
 	mHotseatBarWorkspaceSpacePx: 0.0px (0.0dp)
+	inlineNavButtonsEndSpacingPx: 0.0px (0.0dp)
+	navButtonsLayoutWidthPx: 0.0px (0.0dp)
 	hotseatBarEndOffset: 0.0px (0.0dp)
 	hotseatQsbSpace: 0.0px (0.0dp)
 	hotseatQsbHeight: 0.0px (0.0dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait_decoupleDepth.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait_decoupleDepth.txt
index eae50f1..55322d6 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait_decoupleDepth.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait_decoupleDepth.txt
@@ -77,6 +77,8 @@
 	hotseatBarBottomSpacePx: 126.0px (48.0dp)
 	mHotseatBarEdgePaddingPx: 0.0px (0.0dp)
 	mHotseatBarWorkspaceSpacePx: 0.0px (0.0dp)
+	inlineNavButtonsEndSpacingPx: 0.0px (0.0dp)
+	navButtonsLayoutWidthPx: 0.0px (0.0dp)
 	hotseatBarEndOffset: 0.0px (0.0dp)
 	hotseatQsbSpace: 0.0px (0.0dp)
 	hotseatQsbHeight: 0.0px (0.0dp)
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 d7dd40b..825b52b 100644
--- a/tests/multivalentTests/shared/com/android/launcher3/testing/shared/TestProtocol.java
+++ b/tests/multivalentTests/shared/com/android/launcher3/testing/shared/TestProtocol.java
@@ -167,10 +167,6 @@
 
     public static final String PERMANENT_DIAG_TAG = "TaplTarget";
     public static final String ICON_MISSING = "b/282963545";
-    public static final String UIOBJECT_STALE_ELEMENT = "b/319501259";
-    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 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/AbstractDeviceProfileTest.kt b/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
index 8770859..f8794f8 100644
--- a/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
@@ -79,7 +79,7 @@
         val statusBarNaturalPx: Int,
         val statusBarRotatedPx: Int,
         val gesturePx: Int,
-        val cutoutPx: Int
+        val cutoutPx: Int,
     )
 
     open val deviceSpecs =
@@ -91,7 +91,7 @@
                     statusBarNaturalPx = 118,
                     statusBarRotatedPx = 74,
                     gesturePx = 63,
-                    cutoutPx = 118
+                    cutoutPx = 118,
                 ),
             "tablet" to
                 DeviceSpec(
@@ -100,7 +100,7 @@
                     statusBarNaturalPx = 104,
                     statusBarRotatedPx = 104,
                     gesturePx = 0,
-                    cutoutPx = 0
+                    cutoutPx = 0,
                 ),
             "twopanel-phone" to
                 DeviceSpec(
@@ -109,7 +109,7 @@
                     statusBarNaturalPx = 133,
                     statusBarRotatedPx = 110,
                     gesturePx = 63,
-                    cutoutPx = 133
+                    cutoutPx = 133,
                 ),
             "twopanel-tablet" to
                 DeviceSpec(
@@ -118,14 +118,15 @@
                     statusBarNaturalPx = 110,
                     statusBarRotatedPx = 133,
                     gesturePx = 0,
-                    cutoutPx = 0
-                )
+                    cutoutPx = 0,
+                ),
         )
 
     protected fun initializeVarsForPhone(
         deviceSpec: DeviceSpec,
         isGestureMode: Boolean = true,
-        isVerticalBar: Boolean = false
+        isVerticalBar: Boolean = false,
+        isFixedLandscape: Boolean = false,
     ) {
         val (naturalX, naturalY) = deviceSpec.naturalSize
         val windowsBounds = phoneWindowsBounds(deviceSpec, isGestureMode, naturalX, naturalY)
@@ -137,14 +138,15 @@
             displayInfo,
             rotation = if (isVerticalBar) Surface.ROTATION_90 else Surface.ROTATION_0,
             isGestureMode,
-            densityDpi = deviceSpec.densityDpi
+            densityDpi = deviceSpec.densityDpi,
+            isFixedLandscape = isFixedLandscape,
         )
     }
 
     protected fun initializeVarsForTablet(
         deviceSpec: DeviceSpec,
         isLandscape: Boolean = false,
-        isGestureMode: Boolean = true
+        isGestureMode: Boolean = true,
     ) {
         val (naturalX, naturalY) = deviceSpec.naturalSize
         val windowsBounds = tabletWindowsBounds(deviceSpec, naturalX, naturalY)
@@ -156,7 +158,7 @@
             displayInfo,
             rotation = if (isLandscape) Surface.ROTATION_0 else Surface.ROTATION_90,
             isGestureMode,
-            densityDpi = deviceSpec.densityDpi
+            densityDpi = deviceSpec.densityDpi,
         )
     }
 
@@ -165,7 +167,8 @@
         deviceSpecFolded: DeviceSpec,
         isLandscape: Boolean = false,
         isGestureMode: Boolean = true,
-        isFolded: Boolean = false
+        isFolded: Boolean = false,
+        isFixedLandscape: Boolean = false,
     ) {
         val (unfoldedNaturalX, unfoldedNaturalY) = deviceSpecUnfolded.naturalSize
         val unfoldedWindowsBounds =
@@ -182,7 +185,7 @@
         val perDisplayBoundsCache =
             mapOf(
                 unfoldedDisplayInfo to unfoldedWindowsBounds,
-                foldedDisplayInfo to foldedWindowsBounds
+                foldedDisplayInfo to foldedWindowsBounds,
             )
 
         if (isFolded) {
@@ -191,7 +194,8 @@
                 displayInfo = foldedDisplayInfo,
                 rotation = if (isLandscape) Surface.ROTATION_90 else Surface.ROTATION_0,
                 isGestureMode = isGestureMode,
-                densityDpi = deviceSpecFolded.densityDpi
+                densityDpi = deviceSpecFolded.densityDpi,
+                isFixedLandscape = isFixedLandscape,
             )
         } else {
             initializeCommonVars(
@@ -199,7 +203,8 @@
                 displayInfo = unfoldedDisplayInfo,
                 rotation = if (isLandscape) Surface.ROTATION_0 else Surface.ROTATION_90,
                 isGestureMode = isGestureMode,
-                densityDpi = deviceSpecUnfolded.densityDpi
+                densityDpi = deviceSpecUnfolded.densityDpi,
+                isFixedLandscape = isFixedLandscape,
             )
         }
     }
@@ -208,7 +213,7 @@
         deviceSpec: DeviceSpec,
         isGestureMode: Boolean,
         naturalX: Int,
-        naturalY: Int
+        naturalY: Int,
     ): List<WindowBounds> {
         val buttonsNavHeight = Utilities.dpToPx(48f, deviceSpec.densityDpi)
 
@@ -217,14 +222,14 @@
                 0,
                 max(deviceSpec.statusBarNaturalPx, deviceSpec.cutoutPx),
                 0,
-                if (isGestureMode) deviceSpec.gesturePx else buttonsNavHeight
+                if (isGestureMode) deviceSpec.gesturePx else buttonsNavHeight,
             )
         val rotation90Insets =
             Rect(
                 deviceSpec.cutoutPx,
                 deviceSpec.statusBarRotatedPx,
                 if (isGestureMode) 0 else buttonsNavHeight,
-                if (isGestureMode) deviceSpec.gesturePx else 0
+                if (isGestureMode) deviceSpec.gesturePx else 0,
             )
         val rotation180Insets =
             Rect(
@@ -233,29 +238,29 @@
                 0,
                 max(
                     if (isGestureMode) deviceSpec.gesturePx else buttonsNavHeight,
-                    deviceSpec.cutoutPx
-                )
+                    deviceSpec.cutoutPx,
+                ),
             )
         val rotation270Insets =
             Rect(
                 if (isGestureMode) 0 else buttonsNavHeight,
                 deviceSpec.statusBarRotatedPx,
                 deviceSpec.cutoutPx,
-                if (isGestureMode) deviceSpec.gesturePx else 0
+                if (isGestureMode) deviceSpec.gesturePx else 0,
             )
 
         return listOf(
             WindowBounds(Rect(0, 0, naturalX, naturalY), rotation0Insets, Surface.ROTATION_0),
             WindowBounds(Rect(0, 0, naturalY, naturalX), rotation90Insets, Surface.ROTATION_90),
             WindowBounds(Rect(0, 0, naturalX, naturalY), rotation180Insets, Surface.ROTATION_180),
-            WindowBounds(Rect(0, 0, naturalY, naturalX), rotation270Insets, Surface.ROTATION_270)
+            WindowBounds(Rect(0, 0, naturalY, naturalX), rotation270Insets, Surface.ROTATION_270),
         )
     }
 
     private fun tabletWindowsBounds(
         deviceSpec: DeviceSpec,
         naturalX: Int,
-        naturalY: Int
+        naturalY: Int,
     ): List<WindowBounds> {
         val naturalInsets = Rect(0, deviceSpec.statusBarNaturalPx, 0, 0)
         val rotatedInsets = Rect(0, deviceSpec.statusBarRotatedPx, 0, 0)
@@ -264,7 +269,7 @@
             WindowBounds(Rect(0, 0, naturalX, naturalY), naturalInsets, Surface.ROTATION_0),
             WindowBounds(Rect(0, 0, naturalY, naturalX), rotatedInsets, Surface.ROTATION_90),
             WindowBounds(Rect(0, 0, naturalX, naturalY), naturalInsets, Surface.ROTATION_180),
-            WindowBounds(Rect(0, 0, naturalY, naturalX), rotatedInsets, Surface.ROTATION_270)
+            WindowBounds(Rect(0, 0, naturalY, naturalX), rotatedInsets, Surface.ROTATION_270),
         )
     }
 
@@ -273,7 +278,8 @@
         displayInfo: CachedDisplayInfo,
         rotation: Int,
         isGestureMode: Boolean = true,
-        densityDpi: Int
+        densityDpi: Int,
+        isFixedLandscape: Boolean = false,
     ) {
         setFlagsRule.setFlags(true, Flags.FLAG_ENABLE_TWOLINE_TOGGLE)
         LauncherPrefs.get(testContext).put(LauncherPrefs.ENABLE_TWOLINE_ALLAPPS_TOGGLE, true)
@@ -307,6 +313,11 @@
 
         whenever(launcherPrefs.get(LauncherPrefs.TASKBAR_PINNING)).thenReturn(false)
         whenever(launcherPrefs.get(LauncherPrefs.TASKBAR_PINNING_IN_DESKTOP_MODE)).thenReturn(true)
+        whenever(launcherPrefs.get(LauncherPrefs.FIXED_LANDSCAPE_MODE)).thenReturn(isFixedLandscape)
+        whenever(launcherPrefs.get(LauncherPrefs.HOTSEAT_COUNT)).thenReturn(-1)
+        whenever(launcherPrefs.get(LauncherPrefs.DEVICE_TYPE)).thenReturn(-1)
+        whenever(launcherPrefs.get(LauncherPrefs.WORKSPACE_SIZE)).thenReturn("")
+        whenever(launcherPrefs.get(LauncherPrefs.DB_FILE)).thenReturn("")
         val info = spy(DisplayController.Info(context, windowManagerProxy, perDisplayBoundsCache))
         whenever(displayController.info).thenReturn(info)
         whenever(info.isTransientTaskbar).thenReturn(isGestureMode)
diff --git a/tests/multivalentTests/src/com/android/launcher3/AppWidgetsRestoredReceiverTest.kt b/tests/multivalentTests/src/com/android/launcher3/AppWidgetsRestoredReceiverTest.kt
index 21abab4..0e06051 100644
--- a/tests/multivalentTests/src/com/android/launcher3/AppWidgetsRestoredReceiverTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/AppWidgetsRestoredReceiverTest.kt
@@ -29,7 +29,7 @@
 
     @Before
     fun setup() {
-        launcherPrefs = LauncherPrefs(DeviceHelpers.context)
+        launcherPrefs = LauncherPrefs.get(DeviceHelpers.context)
         receiverUnderTest = AppWidgetsRestoredReceiver()
     }
 
diff --git a/tests/multivalentTests/src/com/android/launcher3/AutoInstallsLayoutTest.kt b/tests/multivalentTests/src/com/android/launcher3/AutoInstallsLayoutTest.kt
index b04bcca..f73a9d3 100644
--- a/tests/multivalentTests/src/com/android/launcher3/AutoInstallsLayoutTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/AutoInstallsLayoutTest.kt
@@ -41,6 +41,8 @@
 import com.android.launcher3.LauncherSettings.Favorites.SPANX
 import com.android.launcher3.LauncherSettings.Favorites.SPANY
 import com.android.launcher3.LauncherSettings.Favorites._ID
+import com.android.launcher3.dagger.LauncherAppComponent
+import com.android.launcher3.dagger.LauncherAppSingleton
 import com.android.launcher3.model.data.AppInfo
 import com.android.launcher3.pm.UserCache
 import com.android.launcher3.util.ApiWrapper
@@ -54,6 +56,8 @@
 import com.android.launcher3.util.UserIconInfo.TYPE_WORK
 import com.android.launcher3.widget.LauncherWidgetHolder
 import com.google.common.truth.Truth.assertThat
+import dagger.BindsInstance
+import dagger.Component
 import java.io.StringReader
 import org.junit.After
 import org.junit.Before
@@ -162,7 +166,9 @@
     @Test
     fun work_item_added_to_home() {
         val apiWrapperMock = spy(ApiWrapper.INSTANCE[targetContext])
-        targetContext.putObject(ApiWrapper.INSTANCE, apiWrapperMock)
+        targetContext.initDaggerComponent(
+            DaggerAutoInstallsLayoutTestComponent.builder().bindApiWrapper(apiWrapperMock)
+        )
         doReturn(
                 mapOf(
                     myUserHandle() to UserIconInfo(myUserHandle(), TYPE_MAIN, 0),
@@ -198,7 +204,7 @@
             callback,
             SourceResources.wrap(targetContext.resources),
             { Xml.newPullParser().also { it.setInput(StringReader(build())) } },
-            TAG_WORKSPACE
+            TAG_WORKSPACE,
         )
 
     class MyCallback : LayoutParserCallback {
@@ -214,3 +220,14 @@
         }
     }
 }
+
+@LauncherAppSingleton
+@Component
+interface AutoInstallsLayoutTestComponent : LauncherAppComponent {
+    @Component.Builder
+    interface Builder : LauncherAppComponent.Builder {
+        @BindsInstance fun bindApiWrapper(wrapper: ApiWrapper): Builder
+
+        override fun build(): AutoInstallsLayoutTestComponent
+    }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/FakeLauncherPrefs.kt b/tests/multivalentTests/src/com/android/launcher3/FakeLauncherPrefs.kt
new file mode 100644
index 0000000..946bbc5
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/FakeLauncherPrefs.kt
@@ -0,0 +1,77 @@
+/*
+ * 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
+
+import android.content.Context
+import com.android.launcher3.util.Executors.MAIN_EXECUTOR
+
+/** Emulates Launcher preferences for a test environment. */
+class FakeLauncherPrefs(private val context: Context) : LauncherPrefs() {
+    private val prefsMap = mutableMapOf<String, Any>()
+    private val listeners = mutableSetOf<LauncherPrefChangeListener>()
+
+    @Suppress("UNCHECKED_CAST")
+    override fun <T> get(item: ContextualItem<T>): T {
+        return prefsMap.getOrDefault(item.sharedPrefKey, item.defaultValueFromContext(context)) as T
+    }
+
+    @Suppress("UNCHECKED_CAST")
+    override fun <T> get(item: ConstantItem<T>): T {
+        return prefsMap.getOrDefault(item.sharedPrefKey, item.defaultValue) as T
+    }
+
+    override fun put(vararg itemsToValues: Pair<Item, Any>) = putSync(*itemsToValues)
+
+    override fun <T : Any> put(item: Item, value: T) = putSync(item to value)
+
+    override fun putSync(vararg itemsToValues: Pair<Item, Any>) {
+        itemsToValues
+            .map { (i, v) -> i.sharedPrefKey to v }
+            .forEach { (k, v) ->
+                prefsMap[k] = v
+                notifyChange(k)
+            }
+    }
+
+    override fun addListener(listener: LauncherPrefChangeListener, vararg items: Item) {
+        listeners.add(listener)
+    }
+
+    override fun removeListener(listener: LauncherPrefChangeListener, vararg items: Item) {
+        listeners.remove(listener)
+    }
+
+    override fun has(vararg items: Item) = items.all { it.sharedPrefKey in prefsMap }
+
+    override fun remove(vararg items: Item) = removeSync(*items)
+
+    override fun removeSync(vararg items: Item) {
+        items
+            .filter { it.sharedPrefKey in prefsMap }
+            .forEach {
+                prefsMap.remove(it.sharedPrefKey)
+                notifyChange(it.sharedPrefKey)
+            }
+    }
+
+    override fun close() = Unit
+
+    private fun notifyChange(key: String) {
+        // Mimics SharedPreferencesImpl#notifyListeners main thread dispatching.
+        MAIN_EXECUTOR.execute { listeners.forEach { it.onPrefChanged(key) } }
+    }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/FakeLauncherPrefsTest.kt b/tests/multivalentTests/src/com/android/launcher3/FakeLauncherPrefsTest.kt
new file mode 100644
index 0000000..2463c93
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/FakeLauncherPrefsTest.kt
@@ -0,0 +1,135 @@
+/*
+ * 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
+
+import androidx.test.core.app.ApplicationProvider.getApplicationContext
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.android.launcher3.util.LauncherMultivalentJUnit
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private val TEST_CONSTANT_ITEM = LauncherPrefs.nonRestorableItem("TEST_BOOLEAN_ITEM", false)
+
+private val TEST_CONTEXTUAL_ITEM =
+    ContextualItem(
+        "TEST_CONTEXTUAL_ITEM",
+        true,
+        { false },
+        EncryptionType.ENCRYPTED,
+        Boolean::class.java,
+    )
+
+@RunWith(LauncherMultivalentJUnit::class)
+class FakeLauncherPrefsTest {
+    private val launcherPrefs = FakeLauncherPrefs(getApplicationContext())
+
+    @Test
+    fun testGet_constantItemNotInPrefs_returnsDefaultValue() {
+        assertThat(launcherPrefs.get(TEST_CONSTANT_ITEM)).isFalse()
+    }
+
+    @Test
+    fun testGet_constantItemInPrefs_returnsStoredValue() {
+        launcherPrefs.put(TEST_CONSTANT_ITEM, true)
+        assertThat(launcherPrefs.get(TEST_CONSTANT_ITEM)).isTrue()
+    }
+
+    @Test
+    fun testGet_contextualItemNotInPrefs_returnsDefaultValue() {
+        assertThat(launcherPrefs.get(TEST_CONTEXTUAL_ITEM)).isFalse()
+    }
+
+    @Test
+    fun testGet_contextualItemInPrefs_returnsStoredValue() {
+        launcherPrefs.put(TEST_CONTEXTUAL_ITEM, true)
+        assertThat(launcherPrefs.get(TEST_CONTEXTUAL_ITEM)).isTrue()
+    }
+
+    @Test
+    fun testPut_multipleItems_storesAll() {
+        launcherPrefs.put(TEST_CONSTANT_ITEM to true, TEST_CONTEXTUAL_ITEM to true)
+        assertThat(launcherPrefs.get(TEST_CONSTANT_ITEM)).isTrue()
+        assertThat(launcherPrefs.get(TEST_CONTEXTUAL_ITEM)).isTrue()
+    }
+
+    @Test
+    fun testHas_itemNotInPrefs_returnsFalse() {
+        assertThat(launcherPrefs.has(TEST_CONSTANT_ITEM)).isFalse()
+    }
+
+    @Test
+    fun testHas_itemInPrefs_returnsTrue() {
+        launcherPrefs.put(TEST_CONSTANT_ITEM, true)
+        assertThat(launcherPrefs.has(TEST_CONSTANT_ITEM)).isTrue()
+    }
+
+    @Test
+    fun testHas_twoItemsWithOneInPrefs_returnsFalse() {
+        launcherPrefs.put(TEST_CONSTANT_ITEM, true)
+        assertThat(launcherPrefs.has(TEST_CONSTANT_ITEM, TEST_CONTEXTUAL_ITEM)).isFalse()
+    }
+
+    @Test
+    fun testHas_twoItemsInPrefs_returnsTrue() {
+        launcherPrefs.put(TEST_CONSTANT_ITEM to true, TEST_CONTEXTUAL_ITEM to true)
+        assertThat(launcherPrefs.has(TEST_CONSTANT_ITEM, TEST_CONTEXTUAL_ITEM)).isTrue()
+    }
+
+    @Test
+    fun testRemove_itemInPrefs_removesItem() {
+        launcherPrefs.put(TEST_CONSTANT_ITEM, true)
+        launcherPrefs.remove(TEST_CONSTANT_ITEM)
+        assertThat(launcherPrefs.has(TEST_CONSTANT_ITEM)).isFalse()
+    }
+
+    @Test
+    fun testRemove_itemsInPrefs_removesItems() {
+        launcherPrefs.put(TEST_CONSTANT_ITEM to true, TEST_CONTEXTUAL_ITEM to true)
+        launcherPrefs.remove(TEST_CONSTANT_ITEM, TEST_CONTEXTUAL_ITEM)
+        assertThat(launcherPrefs.has(TEST_CONSTANT_ITEM, TEST_CONTEXTUAL_ITEM)).isFalse()
+    }
+
+    @Test
+    fun testAddListener_changeItemInPrefs_callsListener() {
+        var changedKey: String? = null
+        launcherPrefs.addListener({ changedKey = it }, TEST_CONSTANT_ITEM)
+        getInstrumentation().runOnMainSync { launcherPrefs.put(TEST_CONSTANT_ITEM, true) }
+        assertThat(changedKey).isEqualTo(TEST_CONSTANT_ITEM.sharedPrefKey)
+    }
+
+    @Test
+    fun testAddListener_removeItemFromPrefs_callsListener() {
+        var changedKey: String? = null
+        launcherPrefs.put(TEST_CONSTANT_ITEM, true)
+        launcherPrefs.addListener({ changedKey = it }, TEST_CONSTANT_ITEM)
+
+        getInstrumentation().runOnMainSync { launcherPrefs.remove(TEST_CONSTANT_ITEM) }
+        assertThat(changedKey).isEqualTo(TEST_CONSTANT_ITEM.sharedPrefKey)
+    }
+
+    @Test
+    fun testRemoveListener_changeItemInPrefs_doesNotCallListener() {
+        var changedKey: String? = null
+        val listener = LauncherPrefChangeListener { changedKey = it }
+        launcherPrefs.addListener(listener, TEST_CONSTANT_ITEM)
+
+        launcherPrefs.removeListener(listener)
+        getInstrumentation().runOnMainSync { launcherPrefs.put(TEST_CONSTANT_ITEM, true) }
+        assertThat(changedKey).isNull()
+    }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/LauncherPrefsTest.kt b/tests/multivalentTests/src/com/android/launcher3/LauncherPrefsTest.kt
index b813095..4aeef2e 100644
--- a/tests/multivalentTests/src/com/android/launcher3/LauncherPrefsTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/LauncherPrefsTest.kt
@@ -17,7 +17,6 @@
 
 import android.content.Context
 import android.content.SharedPreferences
-import android.content.SharedPreferences.OnSharedPreferenceChangeListener
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import androidx.test.platform.app.InstrumentationRegistry
@@ -63,7 +62,7 @@
     @Test
     fun addListener_listeningForStringItemUpdates_isCorrectlyNotifiedOfUpdates() {
         val latch = CountDownLatch(1)
-        val listener = OnSharedPreferenceChangeListener { _, _ -> latch.countDown() }
+        val listener = LauncherPrefChangeListener { latch.countDown() }
 
         with(launcherPrefs) {
             putSync(TEST_STRING_ITEM.to(TEST_STRING_ITEM.defaultValue))
@@ -78,7 +77,7 @@
     @Test
     fun removeListener_previouslyListeningForStringItemUpdates_isNoLongerNotifiedOfUpdates() {
         val latch = CountDownLatch(1)
-        val listener = OnSharedPreferenceChangeListener { _, _ -> latch.countDown() }
+        val listener = LauncherPrefChangeListener { latch.countDown() }
 
         with(launcherPrefs) {
             addListener(listener, TEST_STRING_ITEM)
@@ -94,14 +93,14 @@
     @Test
     fun addListenerAndRemoveListener_forMultipleItems_bothWorkProperly() {
         var latch = CountDownLatch(3)
-        val listener = OnSharedPreferenceChangeListener { _, _ -> latch.countDown() }
+        val listener = LauncherPrefChangeListener { latch.countDown() }
 
         with(launcherPrefs) {
             addListener(listener, TEST_INT_ITEM, TEST_STRING_ITEM, TEST_BOOLEAN_ITEM)
             putSync(
                 TEST_INT_ITEM.to(TEST_INT_ITEM.defaultValue + 123),
                 TEST_STRING_ITEM.to(TEST_STRING_ITEM.defaultValue + "abc"),
-                TEST_BOOLEAN_ITEM.to(!TEST_BOOLEAN_ITEM.defaultValue)
+                TEST_BOOLEAN_ITEM.to(!TEST_BOOLEAN_ITEM.defaultValue),
             )
             assertThat(latch.await(WAIT_TIME_IN_SECONDS, TimeUnit.SECONDS)).isTrue()
 
@@ -110,7 +109,7 @@
             putSync(
                 TEST_INT_ITEM.to(TEST_INT_ITEM.defaultValue),
                 TEST_STRING_ITEM.to(TEST_STRING_ITEM.defaultValue),
-                TEST_BOOLEAN_ITEM.to(TEST_BOOLEAN_ITEM.defaultValue)
+                TEST_BOOLEAN_ITEM.to(TEST_BOOLEAN_ITEM.defaultValue),
             )
             remove(TEST_INT_ITEM, TEST_STRING_ITEM, TEST_BOOLEAN_ITEM)
 
@@ -150,7 +149,7 @@
             putSync(
                 TEST_STRING_ITEM.to(TEST_STRING_ITEM.defaultValue),
                 TEST_INT_ITEM.to(TEST_INT_ITEM.defaultValue),
-                TEST_BOOLEAN_ITEM.to(TEST_BOOLEAN_ITEM.defaultValue)
+                TEST_BOOLEAN_ITEM.to(TEST_BOOLEAN_ITEM.defaultValue),
             )
             assertThat(has(TEST_BOOLEAN_ITEM, TEST_INT_ITEM, TEST_STRING_ITEM)).isTrue()
             remove(TEST_STRING_ITEM, TEST_INT_ITEM, TEST_BOOLEAN_ITEM)
@@ -191,7 +190,7 @@
             LauncherPrefs.backedUpItem(
                 TEST_PREF_KEY,
                 TEST_DEFAULT_VALUE,
-                EncryptionType.DEVICE_PROTECTED
+                EncryptionType.DEVICE_PROTECTED,
             )
 
         val bootAwarePrefs: SharedPreferences =
@@ -212,7 +211,7 @@
             LauncherPrefs.backedUpItem(
                 TEST_PREF_KEY,
                 TEST_DEFAULT_VALUE,
-                EncryptionType.DEVICE_PROTECTED
+                EncryptionType.DEVICE_PROTECTED,
             )
 
         val bootAwarePrefs: SharedPreferences =
diff --git a/tests/multivalentTests/src/com/android/launcher3/RoboObjectInitializer.kt b/tests/multivalentTests/src/com/android/launcher3/RoboObjectInitializer.kt
deleted file mode 100644
index c5f9f86..0000000
--- a/tests/multivalentTests/src/com/android/launcher3/RoboObjectInitializer.kt
+++ /dev/null
@@ -1,35 +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
-
-import com.android.launcher3.util.MainThreadInitializedObject
-import com.android.launcher3.util.MainThreadInitializedObject.SandboxApplication
-import com.android.launcher3.util.SafeCloseable
-
-/**
- * Initializes [MainThreadInitializedObject] instances for Robolectric tests.
- *
- * Unlike instrumentation tests, Robolectric creates a new application instance for each test, which
- * could cause the various static objects defined in [MainThreadInitializedObject] to leak. Thus, a
- * [SandboxApplication] for Robolectric tests can implement this interface to limit the lifecycle of
- * these objects to a single test.
- */
-interface RoboObjectInitializer {
-
-    /** Overrides an object with [type] to [value]. */
-    fun <T : SafeCloseable> initializeObject(type: MainThreadInitializedObject<T>, value: T)
-}
diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/FavoriteItemsTransaction.java b/tests/multivalentTests/src/com/android/launcher3/celllayout/FavoriteItemsTransaction.java
index 0c3081f..a9082e2 100644
--- a/tests/multivalentTests/src/com/android/launcher3/celllayout/FavoriteItemsTransaction.java
+++ b/tests/multivalentTests/src/com/android/launcher3/celllayout/FavoriteItemsTransaction.java
@@ -22,6 +22,7 @@
 
 import android.content.Context;
 
+import com.android.launcher3.Flags;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherModel;
 import com.android.launcher3.LauncherSettings;
@@ -59,7 +60,11 @@
         runOnExecutorSync(MODEL_EXECUTOR, () -> {
             ModelDbController controller = model.getModelDbController();
             // Migrate any previous data so that the DB state is correct
-            controller.tryMigrateDB(null /* restoreEventLogger */);
+            if (Flags.gridMigrationRefactor()) {
+                controller.attemptMigrateDb(null /* restoreEventLogger */);
+            } else {
+                controller.tryMigrateDB(null /* restoreEventLogger */);
+            }
 
             // Create DB again to load fresh data
             controller.createEmptyDB();
diff --git a/tests/multivalentTests/src/com/android/launcher3/folder/PreviewItemManagerTest.kt b/tests/multivalentTests/src/com/android/launcher3/folder/PreviewItemManagerTest.kt
index d236551..111ffaa 100644
--- a/tests/multivalentTests/src/com/android/launcher3/folder/PreviewItemManagerTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/folder/PreviewItemManagerTest.kt
@@ -29,7 +29,6 @@
 import com.android.launcher3.icons.BaseIconFactory
 import com.android.launcher3.icons.FastBitmapDrawable
 import com.android.launcher3.icons.UserBadgeDrawable
-import com.android.launcher3.model.ModelTestRule
 import com.android.launcher3.model.data.FolderInfo
 import com.android.launcher3.model.data.ItemInfo
 import com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_ARCHIVED
@@ -45,7 +44,6 @@
 import com.google.common.truth.Truth.assertThat
 import org.junit.After
 import org.junit.Before
-import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -54,8 +52,6 @@
 @RunWith(AndroidJUnit4::class)
 class PreviewItemManagerTest {
 
-    @get:Rule val modelTestRule = ModelTestRule()
-
     private lateinit var previewItemManager: PreviewItemManager
     private lateinit var context: Context
     private lateinit var folderItems: ArrayList<ItemInfo>
@@ -99,8 +95,8 @@
                 BaseIconFactory(
                     context,
                     context.resources.configuration.densityDpi,
-                    previewItemManager.mIconSize
-                )
+                    previewItemManager.mIconSize,
+                ),
             )
 
         // Set second icon to be non-themed.
@@ -111,8 +107,8 @@
                 BaseIconFactory(
                     context,
                     context.resources.configuration.densityDpi,
-                    previewItemManager.mIconSize
-                )
+                    previewItemManager.mIconSize,
+                ),
             )
 
         // Set third icon to be themed with badge.
@@ -123,8 +119,8 @@
                 BaseIconFactory(
                     context,
                     context.resources.configuration.densityDpi,
-                    previewItemManager.mIconSize
-                )
+                    previewItemManager.mIconSize,
+                ),
             )
         folderApps[2].bitmap = folderApps[2].bitmap.withFlags(profileFlagOp(UserIconInfo.TYPE_WORK))
 
@@ -137,8 +133,8 @@
                 BaseIconFactory(
                     context,
                     context.resources.configuration.densityDpi,
-                    previewItemManager.mIconSize
-                )
+                    previewItemManager.mIconSize,
+                ),
             )
 
         defaultThemedIcons = get(context).get(THEMED_ICONS)
diff --git a/tests/multivalentTests/src/com/android/launcher3/icons/IconCacheTest.java b/tests/multivalentTests/src/com/android/launcher3/icons/IconCacheTest.java
index 495d583..ce00b28 100644
--- a/tests/multivalentTests/src/com/android/launcher3/icons/IconCacheTest.java
+++ b/tests/multivalentTests/src/com/android/launcher3/icons/IconCacheTest.java
@@ -15,21 +15,40 @@
  */
 package com.android.launcher3.icons;
 
+import static android.os.Process.myUserHandle;
+
 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
 
 import static com.android.launcher3.icons.IconCache.EXTRA_SHORTCUT_BADGE_OVERRIDE_PACKAGE;
+import static com.android.launcher3.icons.IconCacheUpdateHandlerTestKt.waitForUpdateHandlerToFinish;
+import static com.android.launcher3.model.data.AppInfo.makeLaunchIntent;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+import static com.android.launcher3.util.LauncherModelHelper.TEST_ACTIVITY;
+import static com.android.launcher3.util.LauncherModelHelper.TEST_ACTIVITY2;
+import static com.android.launcher3.util.LauncherModelHelper.TEST_PACKAGE;
+import static com.android.launcher3.util.TestUtil.runOnExecutorSync;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.spy;
 
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.ContextWrapper;
 import android.content.Intent;
+import android.content.pm.LauncherActivityInfo;
+import android.content.pm.LauncherApps;
 import android.content.pm.ShortcutInfo;
 import android.content.pm.ShortcutInfo.Builder;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.drawable.Icon;
 import android.os.PersistableBundle;
+import android.os.UserHandle;
 import android.text.TextUtils;
 
 import androidx.annotation.Nullable;
@@ -37,15 +56,29 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.icons.cache.CachingLogic;
+import com.android.launcher3.icons.cache.IconCacheUpdateHandler;
+import com.android.launcher3.icons.cache.LauncherActivityCachingLogic;
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.ItemInfoWithIcon;
 import com.android.launcher3.model.data.PackageItemInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.settings.SettingsActivity;
+import com.android.launcher3.shortcuts.ShortcutKey;
+import com.android.launcher3.util.ApplicationInfoWrapper;
+import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.PackageUserKey;
+
+import com.google.common.truth.Truth;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 public class IconCacheTest {
@@ -112,6 +145,162 @@
         assertEquals(((PackageItemInfo) item).packageName, otherPackage);
     }
 
+    @Test
+    public void launcherActivityInfo_cached_in_memory() {
+        ComponentName cn = new ComponentName(TEST_PACKAGE, TEST_ACTIVITY);
+        UserHandle user = myUserHandle();
+        ComponentKey cacheKey = new ComponentKey(cn, user);
+
+        LauncherActivityInfo lai = mContext.getSystemService(LauncherApps.class)
+                .resolveActivity(makeLaunchIntent(cn), user);
+        assertNotNull(lai);
+
+        WorkspaceItemInfo info = new WorkspaceItemInfo();
+        info.intent = makeLaunchIntent(cn);
+        runOnExecutorSync(MODEL_EXECUTOR,
+                () -> mIconCache.getTitleAndIcon(info, lai, false));
+        assertNotNull(info.bitmap);
+        assertFalse(info.bitmap.isLowRes());
+
+        // Verify that icon is in memory cache
+        runOnExecutorSync(MODEL_EXECUTOR,
+                () -> assertNotNull(mIconCache.getInMemoryEntryLocked(cacheKey)));
+
+        // Schedule async update and wait for it to complete
+        Set<PackageUserKey> updates =
+                executeIconUpdate(lai, LauncherActivityCachingLogic.INSTANCE);
+
+        // Verify that the icon was not updated and is still in memory cache
+        Truth.assertThat(updates).isEmpty();
+        runOnExecutorSync(MODEL_EXECUTOR,
+                () -> assertNotNull(mIconCache.getInMemoryEntryLocked(cacheKey)));
+    }
+
+    @Test
+    public void shortcutInfo_not_cached_in_memory() {
+        CacheableShortcutInfo si = mockShortcutInfo(0);
+        ShortcutKey cacheKey = ShortcutKey.fromInfo(si.getShortcutInfo());
+
+        WorkspaceItemInfo info = new WorkspaceItemInfo();
+        runOnExecutorSync(MODEL_EXECUTOR, () -> mIconCache.getShortcutIcon(info, si));
+        assertNotNull(info.bitmap);
+        assertFalse(info.bitmap.isLowRes());
+
+        // Verify that icon is in memory cache
+        runOnExecutorSync(MODEL_EXECUTOR,
+                () -> assertNull(mIconCache.getInMemoryEntryLocked(cacheKey)));
+
+        Set<PackageUserKey> updates =
+                executeIconUpdate(si, CacheableShortcutCachingLogic.INSTANCE);
+        // Verify that the icon was not updated and is still in memory cache
+        Truth.assertThat(updates).isEmpty();
+        runOnExecutorSync(MODEL_EXECUTOR,
+                () -> assertNull(mIconCache.getInMemoryEntryLocked(cacheKey)));
+
+        // Now update the shortcut with a newer version
+        updates = executeIconUpdate(
+                mockShortcutInfo(System.currentTimeMillis() + 2000),
+                CacheableShortcutCachingLogic.INSTANCE);
+
+        // Verify that icon was updated but it is still not in mem-cache
+        Truth.assertThat(updates).containsExactly(
+                new PackageUserKey(cacheKey.getPackageName(), cacheKey.user));
+        runOnExecutorSync(MODEL_EXECUTOR,
+                () -> assertNull(mIconCache.getInMemoryEntryLocked(cacheKey)));
+    }
+
+    @Test
+    public void item_kept_in_db_if_nothing_changes() {
+        ComponentName cn = new ComponentName(TEST_PACKAGE, TEST_ACTIVITY);
+        UserHandle user = myUserHandle();
+
+        LauncherActivityInfo lai = mContext.getSystemService(LauncherApps.class)
+                .resolveActivity(makeLaunchIntent(cn), user);
+        assertNotNull(lai);
+
+        // Since this is a new update, there should not be any update
+        Truth.assertThat(executeIconUpdate(lai, LauncherActivityCachingLogic.INSTANCE)).isEmpty();
+        assertTrue(mIconCache.isItemInDb(new ComponentKey(cn, user)));
+
+        // Another update should not cause any changes
+        Truth.assertThat(executeIconUpdate(lai, LauncherActivityCachingLogic.INSTANCE)).isEmpty();
+        assertTrue(mIconCache.isItemInDb(new ComponentKey(cn, user)));
+    }
+
+    @Test
+    public void item_updated_in_db_if_appInfo_changes() {
+        ComponentName cn = new ComponentName(TEST_PACKAGE, TEST_ACTIVITY);
+        UserHandle user = myUserHandle();
+
+        LauncherActivityInfo lai = mContext.getSystemService(LauncherApps.class)
+                .resolveActivity(makeLaunchIntent(cn), user);
+        assertNotNull(lai);
+
+        // Since this is a new update, there should not be any update
+        Truth.assertThat(executeIconUpdate(lai, LauncherActivityCachingLogic.INSTANCE)).isEmpty();
+        assertTrue(mIconCache.isItemInDb(new ComponentKey(cn, user)));
+
+        // Another update should trigger an update
+        lai.getApplicationInfo().sourceDir = "some-random-source-dir";
+        Truth.assertThat(executeIconUpdate(lai, LauncherActivityCachingLogic.INSTANCE))
+                .containsExactly(new PackageUserKey(TEST_PACKAGE, user));
+        assertTrue(mIconCache.isItemInDb(new ComponentKey(cn, user)));
+    }
+
+    @Test
+    public void item_removed_in_db_if_item_removed() {
+        ComponentName cn = new ComponentName(TEST_PACKAGE, TEST_ACTIVITY);
+        UserHandle user = myUserHandle();
+
+        LauncherActivityInfo lai = mContext.getSystemService(LauncherApps.class)
+                .resolveActivity(makeLaunchIntent(cn), user);
+        assertNotNull(lai);
+
+        // Since this is a new update, there should not be any update
+        Truth.assertThat(executeIconUpdate(lai, LauncherActivityCachingLogic.INSTANCE)).isEmpty();
+        assertTrue(mIconCache.isItemInDb(new ComponentKey(cn, user)));
+
+        // Another update should trigger an update
+        ComponentName cn2 = new ComponentName(TEST_PACKAGE, TEST_ACTIVITY2);
+        LauncherActivityInfo lai2 = mContext.getSystemService(LauncherApps.class)
+                .resolveActivity(makeLaunchIntent(cn2), user);
+
+        Truth.assertThat(executeIconUpdate(lai2, LauncherActivityCachingLogic.INSTANCE)).isEmpty();
+        assertFalse(mIconCache.isItemInDb(new ComponentKey(cn, user)));
+        assertTrue(mIconCache.isItemInDb(new ComponentKey(cn2, user)));
+    }
+
+    /**
+     * Executes the icon update for the provided entry and returns the updated packages
+     */
+    private <T> Set<PackageUserKey> executeIconUpdate(T object, CachingLogic<T> cachingLogic) {
+        HashSet<PackageUserKey> updates = new HashSet<>();
+
+        runOnExecutorSync(MODEL_EXECUTOR, () -> {
+            IconCacheUpdateHandler updateHandler = mIconCache.getUpdateHandler();
+            updateHandler.updateIcons(
+                    Collections.singletonList(object),
+                    cachingLogic,
+                    (a, b) -> a.forEach(p -> updates.add(new PackageUserKey(p, b))));
+            updateHandler.finish();
+        });
+        waitForUpdateHandlerToFinish(mIconCache);
+        return updates;
+    }
+
+    private CacheableShortcutInfo mockShortcutInfo(long updateTime) {
+        ShortcutInfo info = new ShortcutInfo.Builder(
+                        getInstrumentation().getContext(), "test-shortcut")
+                .setIntent(new Intent(Intent.ACTION_VIEW))
+                .setShortLabel("Test")
+                .setIcon(Icon.createWithBitmap(Bitmap.createBitmap(200, 200, Config.ARGB_8888)))
+                .build();
+        ShortcutInfo spied = spy(info);
+        doReturn(updateTime).when(spied).getLastChangedTimestamp();
+        return new CacheableShortcutInfo(spied,
+                new ApplicationInfoWrapper(getInstrumentation().getContext().getApplicationInfo()));
+    }
+
     private ItemInfoWithIcon getBadgingInfo(Context context,
             @Nullable ComponentName cn, @Nullable String badgeOverride) throws Exception {
         Builder builder = new Builder(context, "test-shortcut")
diff --git a/tests/multivalentTests/src/com/android/launcher3/icons/IconCacheUpdateHandlerTest.kt b/tests/multivalentTests/src/com/android/launcher3/icons/IconCacheUpdateHandlerTest.kt
index e27926f..bae74c8 100644
--- a/tests/multivalentTests/src/com/android/launcher3/icons/IconCacheUpdateHandlerTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/icons/IconCacheUpdateHandlerTest.kt
@@ -17,81 +17,239 @@
 package com.android.launcher3.icons
 
 import android.content.ComponentName
-import android.content.pm.PackageInfo
-import android.database.Cursor
-import android.os.UserHandle
+import android.content.pm.ApplicationInfo
+import android.database.MatrixCursor
+import android.os.Handler
+import android.os.Process.myUserHandle
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.launcher3.icons.cache.BaseIconCache
-import com.android.launcher3.icons.cache.CachingLogic
+import com.android.launcher3.icons.cache.BaseIconCache.IconDB
+import com.android.launcher3.icons.cache.CachedObject
+import com.android.launcher3.icons.cache.CachedObjectCachingLogic
 import com.android.launcher3.icons.cache.IconCacheUpdateHandler
+import com.android.launcher3.util.RoboApiWrapper
+import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.FutureTask
+import org.junit.After
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
 import org.mockito.Mock
 import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.any
+import org.mockito.kotlin.anyOrNull
+import org.mockito.kotlin.doAnswer
 import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.never
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class IconCacheUpdateHandlerTest {
 
-    @Mock private lateinit var cursor: Cursor
-    @Mock private lateinit var user: UserHandle
-    @Mock private lateinit var cachingLogic: CachingLogic<String>
+    @Mock private lateinit var iconProvider: IconProvider
     @Mock private lateinit var baseIconCache: BaseIconCache
+    @Mock private lateinit var cacheDb: IconDB
+    @Mock private lateinit var workerHandler: Handler
 
-    private var componentMap: HashMap<ComponentName, String> = hashMapOf()
-    private var ignorePackages: Set<String> = setOf()
-    private var packageInfoMap: HashMap<String, PackageInfo> = hashMapOf()
+    @Captor private lateinit var deleteCaptor: ArgumentCaptor<String>
 
-    private val dummyRowData =
-        IconCacheRowData(
-            "com.android.fake/.FakeActivity",
-            System.currentTimeMillis(),
-            1,
-            1.0.toLong(),
-            "stateOfConfusion"
+    private var cursor =
+        MatrixCursor(
+            arrayOf(IconDB.COLUMN_ROWID, IconDB.COLUMN_COMPONENT, IconDB.COLUMN_FRESHNESS_ID)
         )
 
+    private lateinit var updateHandlerUnderTest: IconCacheUpdateHandler
+
     @Before
     fun setup() {
-
         MockitoAnnotations.initMocks(this)
-        // Load in a specific row to the database
-        doReturn(0).`when`(cursor).getColumnIndex(BaseIconCache.IconDB.COLUMN_COMPONENT)
-        doReturn(1).`when`(cursor).getColumnIndex(BaseIconCache.IconDB.COLUMN_LAST_UPDATED)
-        doReturn(2).`when`(cursor).getColumnIndex(BaseIconCache.IconDB.COLUMN_VERSION)
-        doReturn(3).`when`(cursor).getColumnIndex(BaseIconCache.IconDB.COLUMN_ROWID)
-        doReturn(4).`when`(cursor).getColumnIndex(BaseIconCache.IconDB.COLUMN_SYSTEM_STATE)
-        doReturn(dummyRowData.component).`when`(cursor).getString(0)
-        doReturn(dummyRowData.lastUpdated).`when`(cursor).getLong(1)
-        doReturn(dummyRowData.version).`when`(cursor).getInt(2)
-        doReturn(dummyRowData.row).`when`(cursor).getLong(3)
-        doReturn(dummyRowData.systemState).`when`(cursor).getString(4)
+        doReturn(iconProvider).whenever(baseIconCache).iconProvider
+        doReturn(cursor).whenever(cacheDb).query(any(), any(), any())
+
+        updateHandlerUnderTest = IconCacheUpdateHandler(baseIconCache, cacheDb, workerHandler)
+    }
+
+    @After
+    fun tearDown() {
+        cursor?.close()
     }
 
     @Test
-    fun `IconCacheUpdateHandler returns null if the component name is malformed`() {
-        val updateHandlerUnderTest = IconCacheUpdateHandler(packageInfoMap, baseIconCache)
+    fun `keeps correct icons irrespective of call order`() {
+        val obj1 = TestCachedObject(1).apply { addToCursor(cursor) }
+        val obj2 = TestCachedObject(2).apply { addToCursor(cursor) }
 
-        val result =
-            updateHandlerUnderTest.updateOrDeleteIcon(
-                cursor,
-                componentMap,
-                ignorePackages,
-                user,
-                cachingLogic
+        updateHandlerUnderTest.updateIcons(obj1)
+        updateHandlerUnderTest.updateIcons(obj2)
+        updateHandlerUnderTest.finish()
+
+        verify(cacheDb, never()).delete(any(), anyOrNull())
+    }
+
+    @Test
+    fun `removes missing entries in single call`() {
+        TestCachedObject(1).addToCursor(cursor)
+        TestCachedObject(2).addToCursor(cursor)
+        TestCachedObject(3).addToCursor(cursor)
+        TestCachedObject(4).addToCursor(cursor)
+        TestCachedObject(5).addToCursor(cursor)
+
+        updateHandlerUnderTest.updateIcons(TestCachedObject(1), TestCachedObject(4))
+        updateHandlerUnderTest.finish()
+
+        verifyItemsDeleted(2, 3, 5)
+    }
+
+    @Test
+    fun `removes missing entries in multiple calls`() {
+        TestCachedObject(1).addToCursor(cursor)
+        TestCachedObject(2).addToCursor(cursor)
+        TestCachedObject(3).addToCursor(cursor)
+        TestCachedObject(4).addToCursor(cursor)
+        TestCachedObject(5).addToCursor(cursor)
+        TestCachedObject(6).addToCursor(cursor)
+
+        updateHandlerUnderTest.updateIcons(TestCachedObject(1), TestCachedObject(2))
+        updateHandlerUnderTest.updateIcons(TestCachedObject(4), TestCachedObject(5))
+        updateHandlerUnderTest.finish()
+
+        verifyItemsDeleted(3, 6)
+    }
+
+    @Test
+    fun `keeps valid app infos`() {
+        val appInfo = ApplicationInfo()
+        doReturn("app-fresh").whenever(iconProvider).getStateForApp(eq(appInfo))
+
+        TestCachedObject(1).addToCursor(cursor)
+        TestCachedObject(2).addToCursor(cursor)
+        cursor.addRow(arrayOf(33, TestCachedObject(1).getPackageKey(), "app-fresh"))
+
+        updateHandlerUnderTest.updateIcons(
+            TestCachedObject(1, appInfo = appInfo),
+            TestCachedObject(2),
+        )
+        updateHandlerUnderTest.finish()
+
+        verify(cacheDb, never()).delete(any(), anyOrNull())
+    }
+
+    @Test
+    fun `deletes stale app infos`() {
+        val appInfo1 = ApplicationInfo()
+        doReturn("app1-fresh").whenever(iconProvider).getStateForApp(eq(appInfo1))
+
+        val appInfo2 = ApplicationInfo()
+        doReturn("app2-fresh").whenever(iconProvider).getStateForApp(eq(appInfo2))
+
+        TestCachedObject(1).addToCursor(cursor)
+        TestCachedObject(2).addToCursor(cursor)
+        cursor.addRow(arrayOf(33, TestCachedObject(1).getPackageKey(), "app1-not-fresh"))
+        cursor.addRow(arrayOf(34, TestCachedObject(2).getPackageKey(), "app2-fresh"))
+
+        updateHandlerUnderTest.updateIcons(
+            TestCachedObject(1, appInfo = appInfo1),
+            TestCachedObject(2, appInfo = appInfo2),
+        )
+        updateHandlerUnderTest.finish()
+
+        verifyItemsDeleted(33)
+    }
+
+    @Test
+    fun `updates stale entries`() {
+        doAnswer { i ->
+                (i.arguments[0] as Runnable).run()
+                true
+            }
+            .whenever(workerHandler)
+            .postAtTime(any(), anyOrNull(), any())
+
+        TestCachedObject(1).addToCursor(cursor)
+        TestCachedObject(2).addToCursor(cursor)
+        TestCachedObject(3).addToCursor(cursor)
+
+        var updatedPackages = mutableSetOf<String>()
+        updateHandlerUnderTest.updateIcons(
+            listOf(
+                TestCachedObject(1, freshnessId = "not-fresh"),
+                TestCachedObject(2, freshnessId = "not-fresh"),
+                TestCachedObject(3),
+            ),
+            CachedObjectCachingLogic,
+        ) { apps, _ ->
+            updatedPackages.addAll(apps)
+        }
+        updateHandlerUnderTest.finish()
+
+        assertThat(updatedPackages)
+            .isEqualTo(
+                mutableSetOf(TestCachedObject(1).cn.packageName, TestCachedObject(2).cn.packageName)
             )
+    }
 
-        assert(result == null)
+    private fun IconCacheUpdateHandler.updateIcons(vararg items: TestCachedObject) {
+        updateIcons(items.toList(), CachedObjectCachingLogic) { _, _ -> }
+    }
+
+    private fun verifyItemsDeleted(vararg rowIds: Long) {
+        verify(cacheDb, times(1)).delete(deleteCaptor.capture(), anyOrNull())
+        val actual =
+            deleteCaptor.value
+                .split('(')
+                ?.get(1)
+                ?.split(')')
+                ?.get(0)
+                ?.split(",")
+                ?.map { it.trim().toLong() }!!
+                .sorted()
+        assertThat(actual).isEqualTo(rowIds.toList().sorted())
     }
 }
 
-data class IconCacheRowData(
-    val component: String,
-    val lastUpdated: Long,
-    val version: Int,
-    val row: Long,
-    val systemState: String
-)
+/** Utility method to wait for the icon update handler to finish */
+fun IconCache.waitForUpdateHandlerToFinish() {
+    var cacheUpdateInProgress = true
+    while (cacheUpdateInProgress) {
+        val cacheCheck = FutureTask {
+            // Check for pending message on the worker thread itself as some task may be
+            // running currently
+            workerHandler.hasMessages(0, iconUpdateToken)
+        }
+        workerHandler.postDelayed(cacheCheck, 10)
+        RoboApiWrapper.waitForLooperSync(workerHandler.looper)
+        cacheUpdateInProgress = cacheCheck.get()
+    }
+}
+
+class TestCachedObject(
+    val rowId: Long,
+    val cn: ComponentName =
+        ComponentName.unflattenFromString("com.android.fake$rowId/.FakeActivity")!!,
+    val freshnessId: String = "fresh-$rowId",
+    val appInfo: ApplicationInfo? = null,
+) : CachedObject {
+
+    override fun getComponent() = cn
+
+    override fun getUser() = myUserHandle()
+
+    override fun getLabel(): CharSequence? = null
+
+    override fun getApplicationInfo(): ApplicationInfo? = appInfo
+
+    override fun getFreshnessIdentifier(iconProvider: IconProvider): String? = freshnessId
+
+    fun addToCursor(cursor: MatrixCursor) =
+        cursor.addRow(arrayOf(rowId, cn.flattenToString(), freshnessId))
+
+    fun getPackageKey() =
+        BaseIconCache.getPackageKey(cn.packageName, user).componentName.flattenToString()
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.kt b/tests/multivalentTests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.kt
index 43dc36b..ce04682 100644
--- a/tests/multivalentTests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.kt
@@ -27,7 +27,6 @@
 import com.google.common.truth.Truth.assertThat
 import org.junit.After
 import org.junit.Before
-import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mockito.times
@@ -44,8 +43,6 @@
 @RunWith(AndroidJUnit4::class)
 class AddWorkspaceItemsTaskTest : AbstractWorkspaceModelTest() {
 
-    @get:Rule val modelTestRule = ModelTestRule()
-
     private lateinit var mDataModelCallbacks: MyCallbacks
 
     private val mWorkspaceItemSpaceFinder: WorkspaceItemSpaceFinder = mock()
@@ -121,18 +118,8 @@
     @Test
     fun givenMultipleItems_whenExecuteTask_thenAddThem() {
         val itemsToAdd =
-            arrayOf(
-                getNewItem(),
-                getExistingItem(),
-                getNewItem(),
-                getNewItem(),
-                getExistingItem(),
-            )
-        givenNewItemSpaces(
-            NewItemSpace(1, 3, 3),
-            NewItemSpace(2, 0, 0),
-            NewItemSpace(2, 0, 1),
-        )
+            arrayOf(getNewItem(), getExistingItem(), getNewItem(), getNewItem(), getExistingItem())
+        givenNewItemSpaces(NewItemSpace(1, 3, 3), NewItemSpace(2, 0, 0), NewItemSpace(2, 0, 1))
         val nonEmptyScreenIds = listOf(0, 1)
 
         val addedItems = testAddItems(nonEmptyScreenIds, *itemsToAdd)
@@ -173,7 +160,7 @@
                 eq(IntArray.wrap(*nonEmptyScreenIds.toIntArray())),
                 eq(IntArray()),
                 eq(1),
-                eq(1)
+                eq(1),
             )
     }
 
@@ -183,7 +170,7 @@
      */
     private fun testAddItems(
         nonEmptyScreenIds: List<Int>,
-        vararg itemsToAdd: WorkspaceItemInfo
+        vararg itemsToAdd: WorkspaceItemInfo,
     ): List<AddedItem> {
         setupWorkspaces(nonEmptyScreenIds)
         val task = newTask(*itemsToAdd)
@@ -220,7 +207,7 @@
     override fun bindAppsAdded(
         newScreens: IntArray?,
         addNotAnimated: ArrayList<ItemInfo>,
-        addAnimated: ArrayList<ItemInfo>
+        addAnimated: ArrayList<ItemInfo>,
     ) {
         addedItems.addAll(addAnimated.map { AddedItem(it, true) })
         addedItems.addAll(addNotAnimated.map { AddedItem(it, false) })
diff --git a/tests/multivalentTests/src/com/android/launcher3/model/AsyncBindingTest.kt b/tests/multivalentTests/src/com/android/launcher3/model/AsyncBindingTest.kt
index dce75b9..ba59253 100644
--- a/tests/multivalentTests/src/com/android/launcher3/model/AsyncBindingTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/model/AsyncBindingTest.kt
@@ -64,8 +64,6 @@
 
     @get:Rule val setFlagsRule = SetFlagsRule()
 
-    @get:Rule val modelTestRule = ModelTestRule()
-
     @Spy private var callbacks = MyCallbacks()
     @Mock private lateinit var itemInflater: ItemInflater<*>
 
@@ -138,7 +136,7 @@
     @Test
     fun test_bind_sync_partially_inflates_on_background() {
         modelHelper.loadModelSync()
-        assertTrue(modelHelper.model.isModelLoaded)
+        assertTrue(modelHelper.model.isModelLoaded())
         callbacks.inflater = itemInflater
 
         val firstPageBindIds = IntSet()
@@ -203,7 +201,7 @@
             pendingTasks: RunnableList,
             onCompleteSignal: RunnableList,
             workspaceItemCount: Int,
-            isBindSync: Boolean
+            isBindSync: Boolean,
         ) {
             this.pendingTasks = pendingTasks
             this.onCompleteSignal = onCompleteSignal
diff --git a/tests/multivalentTests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java b/tests/multivalentTests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java
index 535080a..600af42 100644
--- a/tests/multivalentTests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java
+++ b/tests/multivalentTests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java
@@ -64,11 +64,7 @@
 @RunWith(AndroidJUnit4.class)
 public class CacheDataUpdatedTaskTest {
 
-    @Rule(order = 0)
-    public TestRule testStabilityRule = new TestStabilityRule();
-
-    @Rule(order = 1)
-    public ModelTestRule mModelTestRule = new ModelTestRule();
+    @Rule public TestRule testStabilityRule = new TestStabilityRule();
 
     private static final String PENDING_APP_1 = TEST_PACKAGE + ".pending1";
     private static final String PENDING_APP_2 = TEST_PACKAGE + ".pending2";
diff --git a/tests/multivalentTests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java b/tests/multivalentTests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java
index e14e145..1e2431f 100644
--- a/tests/multivalentTests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java
+++ b/tests/multivalentTests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java
@@ -39,7 +39,6 @@
 
 import org.junit.After;
 import org.junit.Before;
-import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -50,8 +49,6 @@
 @RunWith(AndroidJUnit4.class)
 public class DefaultLayoutProviderTest {
 
-    @Rule public ModelTestRule rule = new ModelTestRule();
-
     private LauncherModelHelper mModelHelper;
     private LauncherModelHelper.SandboxModelContext mTargetContext;
 
diff --git a/tests/multivalentTests/src/com/android/launcher3/model/FirstScreenBroadcastHelperTest.kt b/tests/multivalentTests/src/com/android/launcher3/model/FirstScreenBroadcastHelperTest.kt
index d2d9512..9cc380e 100644
--- a/tests/multivalentTests/src/com/android/launcher3/model/FirstScreenBroadcastHelperTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/model/FirstScreenBroadcastHelperTest.kt
@@ -34,7 +34,6 @@
 import com.android.launcher3.util.PackageManagerHelper
 import com.android.launcher3.util.PackageUserKey
 import junit.framework.Assert.assertEquals
-import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentCaptor
@@ -46,8 +45,6 @@
 @RunWith(AndroidJUnit4::class)
 class FirstScreenBroadcastHelperTest {
 
-    @get:Rule val modelTestRule = ModelTestRule()
-
     private val context = spy(InstrumentationRegistry.getInstrumentation().targetContext)
     private val mockPmHelper = mock<PackageManagerHelper>()
     private val expectedAppPackage = "appPackageExpected"
@@ -70,7 +67,7 @@
                 container = CONTAINER_HOTSEAT
                 intent = expectedIntent
             },
-            LauncherAppWidgetInfo().apply { providerName = expectedComponentName }
+            LauncherAppWidgetInfo().apply { providerName = expectedComponentName },
         )
 
     @Test
@@ -89,7 +86,7 @@
         val sessionInfoMap: HashMap<PackageUserKey, SessionInfo> =
             hashMapOf(
                 PackageUserKey(unexpectedAppPackage, UserHandle(0)) to sessionInfoExpected,
-                PackageUserKey(expectedAppPackage, UserHandle(0)) to sessionInfoUnexpected
+                PackageUserKey(expectedAppPackage, UserHandle(0)) to sessionInfoUnexpected,
             )
 
         // When
@@ -98,7 +95,7 @@
                 packageManagerHelper = mockPmHelper,
                 firstScreenItems = firstScreenItems,
                 userKeyToSessionMap = sessionInfoMap,
-                allWidgets = listOf()
+                allWidgets = listOf(),
             )
 
         // Then
@@ -108,7 +105,7 @@
                     installerPackage = expectedInstallerPackage,
                     pendingWorkspaceItems = mutableSetOf(expectedAppPackage),
                     pendingHotseatItems = mutableSetOf(expectedAppPackage),
-                    pendingWidgetItems = mutableSetOf(expectedAppPackage)
+                    pendingWidgetItems = mutableSetOf(expectedAppPackage),
                 )
             )
 
@@ -133,7 +130,7 @@
                             providerName = expectedComponentName
                             screenId = 0
                         }
-                    )
+                    ),
             )
 
         // Then
@@ -143,7 +140,7 @@
                     installerPackage = expectedInstallerPackage,
                     installedHotseatItems = mutableSetOf(expectedAppPackage),
                     installedWorkspaceItems = mutableSetOf(expectedAppPackage),
-                    firstScreenInstalledWidgets = mutableSetOf(expectedAppPackage)
+                    firstScreenInstalledWidgets = mutableSetOf(expectedAppPackage),
                 )
             )
         assertEquals(expectedResult, actualResult)
@@ -178,8 +175,8 @@
                         LauncherAppWidgetInfo().apply {
                             providerName = unexpectedComponentName
                             screenId = 0
-                        }
-                    )
+                        },
+                    ),
             )
 
         // Then
@@ -190,7 +187,7 @@
                     installedHotseatItems = mutableSetOf(),
                     installedWorkspaceItems = mutableSetOf(),
                     firstScreenInstalledWidgets = mutableSetOf(expectedAppPackage),
-                    secondaryScreenInstalledWidgets = mutableSetOf(expectedAppPackage2)
+                    secondaryScreenInstalledWidgets = mutableSetOf(expectedAppPackage2),
                 )
             )
         assertEquals(expectedResult, actualResult)
@@ -224,7 +221,7 @@
                 packageManagerHelper = mockPmHelper,
                 firstScreenItems = firstScreenItems,
                 userKeyToSessionMap = sessionInfoMap,
-                allWidgets = listOf()
+                allWidgets = listOf(),
             )
 
         // Then
@@ -232,7 +229,7 @@
             listOf(
                 FirstScreenBroadcastModel(
                     installerPackage = expectedInstallerPackage,
-                    pendingCollectionItems = mutableSetOf(expectedAppPackage)
+                    pendingCollectionItems = mutableSetOf(expectedAppPackage),
                 )
             )
         assertEquals(expectedResult, actualResult)
@@ -259,7 +256,7 @@
                 firstScreenInstalledWidgets =
                     mutableSetOf<String>().apply { repeat(20) { add(it.toString()) } },
                 secondaryScreenInstalledWidgets =
-                    mutableSetOf<String>().apply { repeat(20) { add(it.toString()) } }
+                    mutableSetOf<String>().apply { repeat(20) { add(it.toString()) } },
             )
 
         // When
@@ -334,7 +331,7 @@
                     installedWorkspaceItems = mutableSetOf("installedWorkspaceItems"),
                     installedHotseatItems = mutableSetOf("installedHotseatItems"),
                     firstScreenInstalledWidgets = mutableSetOf("firstScreenInstalledWidgetItems"),
-                    secondaryScreenInstalledWidgets = mutableSetOf("secondaryInstalledWidgetItems")
+                    secondaryScreenInstalledWidgets = mutableSetOf("secondaryInstalledWidgetItems"),
                 )
             )
         val expectedPendingIntent =
@@ -342,7 +339,7 @@
                 context,
                 0 /* requestCode */,
                 Intent(),
-                PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
+                PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE,
             )
 
         // When
@@ -354,40 +351,40 @@
 
         assertEquals(
             "com.android.launcher3.action.FIRST_SCREEN_ACTIVE_INSTALLS",
-            argumentCaptor.value.action
+            argumentCaptor.value.action,
         )
         assertEquals(expectedInstallerPackage, argumentCaptor.value.`package`)
         assertEquals(
             expectedPendingIntent,
-            argumentCaptor.value.getParcelableExtra("verificationToken")
+            argumentCaptor.value.getParcelableExtra("verificationToken"),
         )
         assertEquals(
             arrayListOf("pendingCollectionItem"),
-            argumentCaptor.value.getStringArrayListExtra("folderItem")
+            argumentCaptor.value.getStringArrayListExtra("folderItem"),
         )
         assertEquals(
             arrayListOf("pendingWorkspaceItem"),
-            argumentCaptor.value.getStringArrayListExtra("workspaceItem")
+            argumentCaptor.value.getStringArrayListExtra("workspaceItem"),
         )
         assertEquals(
             arrayListOf("pendingHotseatItems"),
-            argumentCaptor.value.getStringArrayListExtra("hotseatItem")
+            argumentCaptor.value.getStringArrayListExtra("hotseatItem"),
         )
         assertEquals(
             arrayListOf("pendingWidgetItems"),
-            argumentCaptor.value.getStringArrayListExtra("widgetItem")
+            argumentCaptor.value.getStringArrayListExtra("widgetItem"),
         )
         assertEquals(
             arrayListOf("installedWorkspaceItems"),
-            argumentCaptor.value.getStringArrayListExtra("workspaceInstalledItems")
+            argumentCaptor.value.getStringArrayListExtra("workspaceInstalledItems"),
         )
         assertEquals(
             arrayListOf("installedHotseatItems"),
-            argumentCaptor.value.getStringArrayListExtra("hotseatInstalledItems")
+            argumentCaptor.value.getStringArrayListExtra("hotseatInstalledItems"),
         )
         assertEquals(
             arrayListOf("firstScreenInstalledWidgetItems", "secondaryInstalledWidgetItems"),
-            argumentCaptor.value.getStringArrayListExtra("widgetInstalledItems")
+            argumentCaptor.value.getStringArrayListExtra("widgetInstalledItems"),
         )
     }
 }
diff --git a/tests/multivalentTests/src/com/android/launcher3/model/FolderIconLoadTest.kt b/tests/multivalentTests/src/com/android/launcher3/model/FolderIconLoadTest.kt
index d002493..7cd5da4 100644
--- a/tests/multivalentTests/src/com/android/launcher3/model/FolderIconLoadTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/model/FolderIconLoadTest.kt
@@ -18,19 +18,17 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.launcher3.LauncherAppState
+import com.android.launcher3.icons.waitForUpdateHandlerToFinish
 import com.android.launcher3.model.data.WorkspaceItemInfo
 import com.android.launcher3.util.Executors
 import com.android.launcher3.util.LauncherLayoutBuilder
 import com.android.launcher3.util.LauncherModelHelper
 import com.android.launcher3.util.LauncherModelHelper.*
-import com.android.launcher3.util.RoboApiWrapper
 import com.android.launcher3.util.TestUtil
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
-import java.util.concurrent.CountDownLatch
 import org.junit.After
 import org.junit.Before
-import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -39,8 +37,6 @@
 @RunWith(AndroidJUnit4::class)
 class FolderIconLoadTest {
 
-    @get:Rule(order = 0) val modelTestRule = ModelTestRule()
-
     private lateinit var modelHelper: LauncherModelHelper
 
     private val uniqueActivities =
@@ -58,7 +54,7 @@
             TEST_ACTIVITY11,
             TEST_ACTIVITY12,
             TEST_ACTIVITY13,
-            TEST_ACTIVITY14
+            TEST_ACTIVITY14,
         )
 
     @Before
@@ -146,14 +142,9 @@
         // The first load initializes the DB, load again so that icons are now used from the DB
         // Wait for the icon cache to be updated and then reload
         val app = LauncherAppState.getInstance(modelHelper.sandboxContext)
-        val cache = app.iconCache
-        while (cache.isIconUpdateInProgress) {
-            val wait = CountDownLatch(1)
-            Executors.MODEL_EXECUTOR.handler.postDelayed({ wait.countDown() }, 10)
-            RoboApiWrapper.waitForLooperSync(Executors.MODEL_EXECUTOR.handler.looper)
-            wait.await()
-        }
-        TestUtil.runOnExecutorSync(Executors.MODEL_EXECUTOR) { cache.clearMemoryCache() }
+        app.iconCache.waitForUpdateHandlerToFinish()
+
+        TestUtil.runOnExecutorSync(Executors.MODEL_EXECUTOR) { app.iconCache.clearMemoryCache() }
         // Reload again with correct icon state
         app.model.forceReload()
         modelHelper.loadModelSync()
diff --git a/tests/multivalentTests/src/com/android/launcher3/model/GridSizeMigrationUtilTest.kt b/tests/multivalentTests/src/com/android/launcher3/model/GridSizeMigrationTest.kt
similarity index 80%
rename from tests/multivalentTests/src/com/android/launcher3/model/GridSizeMigrationUtilTest.kt
rename to tests/multivalentTests/src/com/android/launcher3/model/GridSizeMigrationTest.kt
index f57e8a1..7933331 100644
--- a/tests/multivalentTests/src/com/android/launcher3/model/GridSizeMigrationUtilTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/model/GridSizeMigrationTest.kt
@@ -22,13 +22,16 @@
 import android.database.sqlite.SQLiteDatabase
 import android.graphics.Point
 import android.os.Process
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.launcher3.Flags
 import com.android.launcher3.InvariantDeviceProfile
 import com.android.launcher3.LauncherPrefs
 import com.android.launcher3.LauncherPrefs.Companion.WORKSPACE_SIZE
 import com.android.launcher3.LauncherSettings.Favorites.*
-import com.android.launcher3.model.GridSizeMigrationUtil.DbReader
+import com.android.launcher3.model.GridSizeMigrationDBController.DbReader
 import com.android.launcher3.pm.UserCache
 import com.android.launcher3.provider.LauncherDbUtils
 import com.android.launcher3.util.LauncherModelHelper
@@ -38,10 +41,10 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 
-/** Unit tests for [GridSizeMigrationUtil] */
+/** Unit tests for [GridSizeMigrationDBController, GridSizeMigrationLogic] */
 @SmallTest
 @RunWith(AndroidJUnit4::class)
-class GridSizeMigrationUtilTest {
+class GridSizeMigrationTest {
 
     private lateinit var modelHelper: LauncherModelHelper
     private lateinit var context: Context
@@ -82,9 +85,22 @@
         modelHelper.destroy()
     }
 
-    /** Old migration logic, should be modified once is not needed anymore */
     @Test
     @Throws(Exception::class)
+    @EnableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR)
+    fun testMigrationRefactorFlagOn() {
+        testMigration()
+    }
+
+    @Test
+    @Throws(Exception::class)
+    @DisableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR)
+    fun testMigrationRefactorFlagOff() {
+        testMigration()
+    }
+
+    /** Old migration logic, should be modified once is not needed anymore */
+    @Throws(Exception::class)
     fun testMigration() {
         // Src Hotseat icons
         addItem(ITEM_TYPE_APPLICATION, 0, CONTAINER_HOTSEAT, 0, 0, testPackage1, 1, TMP_TABLE)
@@ -113,15 +129,34 @@
         idp.numRows = 4
         val srcReader = DbReader(db, TMP_TABLE, context)
         val destReader = DbReader(db, TABLE_NAME, context)
-        GridSizeMigrationUtil.migrate(
-            dbHelper,
-            srcReader,
-            destReader,
-            idp.numDatabaseHotseatIcons,
-            Point(idp.numColumns, idp.numRows),
-            DeviceGridState(context),
-            DeviceGridState(idp),
-        )
+        if (Flags.gridMigrationRefactor()) {
+            var gridSizeMigrationLogic = GridSizeMigrationLogic()
+            val idsInUse = mutableListOf<Int>()
+            gridSizeMigrationLogic.migrateHotseat(
+                idp.numDatabaseHotseatIcons,
+                srcReader,
+                destReader,
+                dbHelper,
+                idsInUse,
+            )
+            gridSizeMigrationLogic.migrateWorkspace(
+                srcReader,
+                destReader,
+                dbHelper,
+                Point(idp.numColumns, idp.numRows),
+                idsInUse,
+            )
+        } else {
+            GridSizeMigrationDBController.migrate(
+                dbHelper,
+                srcReader,
+                destReader,
+                idp.numDatabaseHotseatIcons,
+                Point(idp.numColumns, idp.numRows),
+                DeviceGridState(context),
+                DeviceGridState(idp),
+            )
+        }
 
         // Check hotseat items
         var c =
@@ -187,9 +222,22 @@
         assertThat(locMap[testPackage9]).isEqualTo(Point(0, 2))
     }
 
-    /** Old migration logic, should be modified once is not needed anymore */
     @Test
     @Throws(Exception::class)
+    @EnableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR)
+    fun testMigrationBackAndForthRefactorFlagOn() {
+        testMigrationBackAndForth()
+    }
+
+    @Test
+    @Throws(Exception::class)
+    @DisableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR)
+    fun testMigrationBackAndForthRefactorFlagOff() {
+        testMigrationBackAndForth()
+    }
+
+    /** Old migration logic, should be modified once is not needed anymore */
+    @Throws(Exception::class)
     fun testMigrationBackAndForth() {
         // Hotseat items in grid A
         // 1 2 _ 3 4
@@ -224,15 +272,34 @@
         val readerGridA = DbReader(db, TMP_TABLE, context)
         val readerGridB = DbReader(db, TABLE_NAME, context)
         // migrate from A -> B
-        GridSizeMigrationUtil.migrate(
-            dbHelper,
-            readerGridA,
-            readerGridB,
-            idp.numDatabaseHotseatIcons,
-            Point(idp.numColumns, idp.numRows),
-            DeviceGridState(context),
-            DeviceGridState(idp),
-        )
+        if (Flags.gridMigrationRefactor()) {
+            var gridSizeMigrationLogic = GridSizeMigrationLogic()
+            val idsInUse = mutableListOf<Int>()
+            gridSizeMigrationLogic.migrateHotseat(
+                idp.numDatabaseHotseatIcons,
+                readerGridA,
+                readerGridB,
+                dbHelper,
+                idsInUse,
+            )
+            gridSizeMigrationLogic.migrateWorkspace(
+                readerGridA,
+                readerGridB,
+                dbHelper,
+                Point(idp.numColumns, idp.numRows),
+                idsInUse,
+            )
+        } else {
+            GridSizeMigrationDBController.migrate(
+                dbHelper,
+                readerGridA,
+                readerGridB,
+                idp.numDatabaseHotseatIcons,
+                Point(idp.numColumns, idp.numRows),
+                DeviceGridState(context),
+                DeviceGridState(idp),
+            )
+        }
 
         // Check hotseat items in grid B
         var c =
@@ -280,15 +347,8 @@
         addItem(ITEM_TYPE_APPLICATION, 0, CONTAINER_DESKTOP, 0, 2, testPackage9)
 
         // migrate from B -> A
-        GridSizeMigrationUtil.migrate(
-            dbHelper,
-            readerGridB,
-            readerGridA,
-            5,
-            Point(5, 5),
-            DeviceGridState(idp),
-            DeviceGridState(context),
-        )
+        migrateGrid(dbHelper, readerGridB, readerGridA, 5, 5, 5)
+
         // Check hotseat items in grid A
         c =
             db.query(
@@ -339,14 +399,13 @@
         db.delete(TMP_TABLE, "$_ID=7", null)
 
         // migrate from A -> B
-        GridSizeMigrationUtil.migrate(
+        migrateGrid(
             dbHelper,
             readerGridA,
             readerGridB,
             idp.numDatabaseHotseatIcons,
-            Point(idp.numColumns, idp.numRows),
-            DeviceGridState(context),
-            DeviceGridState(idp),
+            idp.numColumns,
+            idp.numRows,
         )
 
         // Check hotseat items in grid B
@@ -392,6 +451,44 @@
         assertThat(locMap[testPackage9]).isEqualTo(Triple(0, 0, 2))
     }
 
+    private fun migrateGrid(
+        dbHelper: DatabaseHelper,
+        srcReader: DbReader,
+        destReader: DbReader,
+        destHotseatSize: Int,
+        pointX: Int,
+        pointY: Int,
+    ) {
+        if (Flags.gridMigrationRefactor()) {
+            var gridSizeMigrationLogic = GridSizeMigrationLogic()
+            val idsInUse = mutableListOf<Int>()
+            gridSizeMigrationLogic.migrateHotseat(
+                idp.numDatabaseHotseatIcons,
+                srcReader,
+                destReader,
+                dbHelper,
+                idsInUse,
+            )
+            gridSizeMigrationLogic.migrateWorkspace(
+                srcReader,
+                destReader,
+                dbHelper,
+                Point(idp.numColumns, idp.numRows),
+                idsInUse,
+            )
+        } else {
+            GridSizeMigrationDBController.migrate(
+                dbHelper,
+                srcReader,
+                destReader,
+                destHotseatSize,
+                Point(pointX, pointY),
+                DeviceGridState(idp),
+                DeviceGridState(context),
+            )
+        }
+    }
+
     private fun verifyHotseat(c: Cursor, idp: InvariantDeviceProfile, expected: List<String?>) {
         assertThat(c.count).isEqualTo(idp.numDatabaseHotseatIcons)
         val screenIndex = c.getColumnIndex(SCREEN)
@@ -421,6 +518,17 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR)
+    fun migrateToLargerHotseatRefactorFlagOn() {
+        migrateToLargerHotseat()
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR)
+    fun migrateToLargerHotseatRefactorFlagOff() {
+        migrateToLargerHotseat()
+    }
+
     fun migrateToLargerHotseat() {
         val srcHotseatItems =
             intArrayOf(
@@ -471,14 +579,13 @@
         idp.numRows = 4
         val srcReader = DbReader(db, TMP_TABLE, context)
         val destReader = DbReader(db, TABLE_NAME, context)
-        GridSizeMigrationUtil.migrate(
+        migrateGrid(
             dbHelper,
             srcReader,
             destReader,
             idp.numDatabaseHotseatIcons,
-            Point(idp.numColumns, idp.numRows),
-            DeviceGridState(context),
-            DeviceGridState(idp),
+            idp.numColumns,
+            idp.numRows,
         )
 
         // Check hotseat items
@@ -516,6 +623,17 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR)
+    fun migrateFromLargerHotseatRefactorFlagOn() {
+        migrateFromLargerHotseat()
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR)
+    fun migrateFromLargerHotseatRefactorFlagOff() {
+        migrateFromLargerHotseat()
+    }
+
     fun migrateFromLargerHotseat() {
         addItem(ITEM_TYPE_APPLICATION, 0, CONTAINER_HOTSEAT, 0, 0, testPackage1, 1, TMP_TABLE)
         addItem(ITEM_TYPE_DEEP_SHORTCUT, 2, CONTAINER_HOTSEAT, 0, 0, testPackage2, 2, TMP_TABLE)
@@ -528,14 +646,13 @@
         idp.numRows = 4
         val srcReader = DbReader(db, TMP_TABLE, context)
         val destReader = DbReader(db, TABLE_NAME, context)
-        GridSizeMigrationUtil.migrate(
+        migrateGrid(
             dbHelper,
             srcReader,
             destReader,
             idp.numDatabaseHotseatIcons,
-            Point(idp.numColumns, idp.numRows),
-            DeviceGridState(context),
-            DeviceGridState(idp),
+            idp.numColumns,
+            idp.numRows,
         )
 
         // Check hotseat items
@@ -573,11 +690,24 @@
         c.close()
     }
 
+    @Test
+    @Throws(Exception::class)
+    @EnableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR)
+    fun migrateFromSmallerGridBigDifferenceRefactorFlagOn() {
+        migrateFromSmallerGridBigDifference()
+    }
+
+    @Test
+    @Throws(Exception::class)
+    @DisableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR)
+    fun migrateFromSmallerGridBigDifferenceRefactorFlagOff() {
+        migrateFromSmallerGridBigDifference()
+    }
+
     /**
      * Migrating from a smaller grid to a large one should reflow the pages if the column difference
      * is more than 2
      */
-    @Test
     @Throws(Exception::class)
     fun migrateFromSmallerGridBigDifference() {
         enableNewMigrationLogic("2,2")
@@ -594,14 +724,13 @@
         idp.numRows = 5
         val srcReader = DbReader(db, TMP_TABLE, context)
         val destReader = DbReader(db, TABLE_NAME, context)
-        GridSizeMigrationUtil.migrate(
+        migrateGrid(
             dbHelper,
             srcReader,
             destReader,
             idp.numDatabaseHotseatIcons,
-            Point(idp.numColumns, idp.numRows),
-            DeviceGridState(context),
-            DeviceGridState(idp),
+            idp.numColumns,
+            idp.numRows,
         )
 
         // Get workspace items
@@ -636,9 +765,22 @@
         assertThat(locMap[testPackage5]).isEqualTo(0)
     }
 
-    /** Migrating from a larger grid to a smaller, we reflow from page 0 */
     @Test
     @Throws(Exception::class)
+    @EnableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR)
+    fun migrateFromLargerGridRefactorFlagOn() {
+        migrateFromLargerGrid()
+    }
+
+    @Test
+    @Throws(Exception::class)
+    @DisableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR)
+    fun migrateFromLargerGridRefactorFlagOff() {
+        migrateFromLargerGrid()
+    }
+
+    /** Migrating from a larger grid to a smaller, we reflow from page 0 */
+    @Throws(Exception::class)
     fun migrateFromLargerGrid() {
         enableNewMigrationLogic("5,5")
 
@@ -654,14 +796,13 @@
         idp.numRows = 4
         val srcReader = DbReader(db, TMP_TABLE, context)
         val destReader = DbReader(db, TABLE_NAME, context)
-        GridSizeMigrationUtil.migrate(
+        migrateGrid(
             dbHelper,
             srcReader,
             destReader,
             idp.numDatabaseHotseatIcons,
-            Point(idp.numColumns, idp.numRows),
-            DeviceGridState(context),
-            DeviceGridState(idp),
+            idp.numColumns,
+            idp.numRows,
         )
 
         // Get workspace items
diff --git a/tests/multivalentTests/src/com/android/launcher3/model/LoaderCursorTest.java b/tests/multivalentTests/src/com/android/launcher3/model/LoaderCursorTest.java
index ac911b3..b4945d7 100644
--- a/tests/multivalentTests/src/com/android/launcher3/model/LoaderCursorTest.java
+++ b/tests/multivalentTests/src/com/android/launcher3/model/LoaderCursorTest.java
@@ -67,7 +67,6 @@
 
 import org.junit.After;
 import org.junit.Before;
-import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -78,8 +77,6 @@
 @RunWith(AndroidJUnit4.class)
 public class LoaderCursorTest {
 
-    @Rule public ModelTestRule rule = new ModelTestRule();
-
     private LauncherModelHelper mModelHelper;
     private LauncherAppState mApp;
     private PackageManagerHelper mPmHelper;
diff --git a/tests/multivalentTests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java b/tests/multivalentTests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java
index a0d9da9..0f1fc00 100644
--- a/tests/multivalentTests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java
+++ b/tests/multivalentTests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java
@@ -37,7 +37,6 @@
 
 import org.junit.After;
 import org.junit.Before;
-import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -48,8 +47,6 @@
 @RunWith(AndroidJUnit4.class)
 public class PackageInstallStateChangedTaskTest {
 
-    @Rule public ModelTestRule mModelTestRule = new ModelTestRule();
-
     private static final String PENDING_APP_1 = TEST_PACKAGE + ".pending1";
     private static final String PENDING_APP_2 = TEST_PACKAGE + ".pending2";
 
diff --git a/tests/multivalentTests/src/com/android/launcher3/model/WidgetsModelTest.kt b/tests/multivalentTests/src/com/android/launcher3/model/WidgetsModelTest.kt
index ff545fe..ae4ff04 100644
--- a/tests/multivalentTests/src/com/android/launcher3/model/WidgetsModelTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/model/WidgetsModelTest.kt
@@ -43,6 +43,7 @@
 import com.google.common.truth.Truth.assertThat
 import java.util.concurrent.CountDownLatch
 import java.util.concurrent.TimeUnit
+import java.util.function.Predicate
 import org.junit.Assert.fail
 import org.junit.Before
 import org.junit.Rule
@@ -64,6 +65,7 @@
     @Mock private lateinit var appWidgetManager: AppWidgetManager
     @Mock private lateinit var app: LauncherAppState
     @Mock private lateinit var iconCacheMock: IconCache
+    @Mock private lateinit var widgetsFilterDataProvider: WidgetsFilterDataProvider
 
     private lateinit var context: Context
     private lateinit var idp: InvariantDeviceProfile
@@ -215,6 +217,27 @@
         // No exception
     }
 
+    @Test
+    fun updateWidgetFilters_setsFiltersCorrectly() {
+        val testDefaultWidgetFilter = Predicate<WidgetItem> { w -> w.widgetInfo != null }
+        whenever(widgetsFilterDataProvider.getDefaultWidgetsFilter())
+            .thenReturn(testDefaultWidgetFilter)
+        val testPredicatedWidgetFilter = Predicate<WidgetItem> { w -> w.widgetInfo != null }
+        whenever(widgetsFilterDataProvider.getPredictedWidgetsFilter())
+            .thenReturn(testPredicatedWidgetFilter)
+
+        underTest.updateWidgetFilters(widgetsFilterDataProvider)
+
+        assertThat(underTest.defaultWidgetsFilter).isEqualTo(testDefaultWidgetFilter)
+        assertThat(underTest.predictedWidgetsFilter).isEqualTo(testPredicatedWidgetFilter)
+    }
+
+    @Test
+    fun widgetFilters_nullInitially() {
+        assertThat(underTest.defaultWidgetsFilter).isNull()
+        assertThat(underTest.predictedWidgetsFilter).isNull()
+    }
+
     private fun loadWidgets() {
         val latch = CountDownLatch(1)
         Executors.MODEL_EXECUTOR.execute {
diff --git a/tests/multivalentTests/src/com/android/launcher3/model/WorkspaceItemProcessorTest.kt b/tests/multivalentTests/src/com/android/launcher3/model/WorkspaceItemProcessorTest.kt
index 1d9c161..ed8b397 100644
--- a/tests/multivalentTests/src/com/android/launcher3/model/WorkspaceItemProcessorTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/model/WorkspaceItemProcessorTest.kt
@@ -40,6 +40,7 @@
 import com.android.launcher3.backuprestore.LauncherRestoreEventLogger.RestoreError.Companion.MISSING_INFO
 import com.android.launcher3.backuprestore.LauncherRestoreEventLogger.RestoreError.Companion.MISSING_WIDGET_PROVIDER
 import com.android.launcher3.backuprestore.LauncherRestoreEventLogger.RestoreError.Companion.PROFILE_DELETED
+import com.android.launcher3.icons.CacheableShortcutInfo
 import com.android.launcher3.model.data.FolderInfo
 import com.android.launcher3.model.data.IconRequestInfo
 import com.android.launcher3.model.data.ItemInfo
@@ -57,7 +58,6 @@
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
 import org.junit.Before
-import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentCaptor
@@ -76,8 +76,6 @@
 @RunWith(AndroidJUnit4::class)
 class WorkspaceItemProcessorTest {
 
-    @get:Rule val modelTestRule = ModelTestRule()
-
     @Mock private lateinit var mockIconRequestInfo: IconRequestInfo<WorkspaceItemInfo>
     @Mock private lateinit var mockWorkspaceInfo: WorkspaceItemInfo
     @Mock private lateinit var mockBgDataModel: BgDataModel
@@ -97,7 +95,7 @@
     private var mUnlockedUsersArray: LongSparseArray<Boolean> = LongSparseArray()
     private var mKeyToPinnedShortcutsMap: MutableMap<ShortcutKey, ShortcutInfo> = mutableMapOf()
     private var mInstallingPkgs: HashMap<PackageUserKey, PackageInstaller.SessionInfo> = hashMapOf()
-    private var mAllDeepShortcuts: MutableList<ShortcutInfo> = mutableListOf()
+    private var mAllDeepShortcuts: MutableList<CacheableShortcutInfo> = mutableListOf()
     private var mWidgetProvidersMap: MutableMap<ComponentKey, AppWidgetProviderInfo?> =
         mutableMapOf()
     private var mPendingPackages: MutableSet<PackageUserKey> = mutableSetOf()
@@ -118,11 +116,17 @@
                 `package` = "pkg"
                 putExtra(ShortcutKey.EXTRA_SHORTCUT_ID, "")
             }
+        mockLauncherApps =
+            mock<LauncherApps>().apply {
+                whenever(isPackageEnabled("package", mUserHandle)).thenReturn(true)
+                whenever(isActivityEnabled(mComponentName, mUserHandle)).thenReturn(true)
+            }
         mockContext =
             mock<Context>().apply {
                 whenever(packageManager).thenReturn(mock())
                 whenever(packageManager.getUserBadgedLabel(any(), any())).thenReturn("")
                 whenever(applicationContext).thenReturn(ApplicationProvider.getApplicationContext())
+                whenever(getSystemService(LauncherApps::class.java)).thenReturn(mockLauncherApps)
             }
         mockAppState =
             mock<LauncherAppState>().apply {
@@ -135,11 +139,6 @@
                 whenever(getAppLaunchIntent(mComponentName.packageName, mUserHandle))
                     .thenReturn(intent)
             }
-        mockLauncherApps =
-            mock<LauncherApps>().apply {
-                whenever(isPackageEnabled("package", mUserHandle)).thenReturn(true)
-                whenever(isActivityEnabled(mComponentName, mUserHandle)).thenReturn(true)
-            }
         mockCursor =
             mock(LoaderCursor::class.java, RETURNS_DEEP_STUBS).apply {
                 user = mUserHandle
@@ -193,7 +192,7 @@
         pendingPackages: MutableSet<PackageUserKey> = mPendingPackages,
         unlockedUsers: LongSparseArray<Boolean> = mUnlockedUsersArray,
         installingPkgs: HashMap<PackageUserKey, PackageInstaller.SessionInfo> = mInstallingPkgs,
-        allDeepShortcuts: MutableList<ShortcutInfo> = mAllDeepShortcuts
+        allDeepShortcuts: MutableList<CacheableShortcutInfo> = mAllDeepShortcuts,
     ) =
         WorkspaceItemProcessor(
             c = cursor,
@@ -212,7 +211,7 @@
             isSdCardReady = isSdCardReady,
             shortcutKeyToPinnedShortcuts = shortcutKeyToPinnedShortcuts,
             installingPkgs = installingPkgs,
-            allDeepShortcuts = allDeepShortcuts
+            allDeepShortcuts = allDeepShortcuts,
         )
 
     @Test
@@ -351,7 +350,7 @@
                     " targetPkg=package," +
                     " component=ComponentInfo{package/class}." +
                     " Unable to create launch Intent.",
-                MISSING_INFO
+                MISSING_INFO,
             )
         verify(mockCursor, times(0)).checkAndAddItem(any(), any(), anyOrNull())
     }
@@ -386,7 +385,8 @@
             .that(mockCursor.restoreFlag)
             .isEqualTo(0)
         assertThat(mIconRequestInfos).isEmpty()
-        assertThat(mAllDeepShortcuts).containsExactly(expectedShortcutInfo)
+        assertThat(mAllDeepShortcuts.size).isEqualTo(1)
+        assertThat(mAllDeepShortcuts[0].shortcutInfo).isEqualTo(expectedShortcutInfo)
         verify(mockCursor).markRestored()
         verify(mockCursor).checkAndAddItem(any(), any(), anyOrNull())
     }
@@ -412,7 +412,7 @@
         verify(mockCursor)
             .markDeleted(
                 "Pinned shortcut not found from request. package=pkg, user=UserHandle{0}",
-                "shortcut_not_found"
+                "shortcut_not_found",
             )
     }
 
@@ -451,7 +451,8 @@
             .that(mockCursor.restoreFlag)
             .isEqualTo(0)
         assertThat(mIconRequestInfos).isEmpty()
-        assertThat(mAllDeepShortcuts).containsExactly(expectedShortcutInfo)
+        assertThat(mAllDeepShortcuts.size).isEqualTo(1)
+        assertThat(mAllDeepShortcuts[0].shortcutInfo).isEqualTo(expectedShortcutInfo)
         verify(mockCursor).markRestored()
         verify(mockCursor).checkAndAddItem(any(), any(), anyOrNull())
     }
@@ -549,7 +550,7 @@
         val inflationResult =
             WidgetInflater.InflationResult(
                 type = WidgetInflater.TYPE_REAL,
-                widgetInfo = expectedWidgetProviderInfo
+                widgetInfo = expectedWidgetProviderInfo,
             )
         mockWidgetInflater =
             mock<WidgetInflater>().apply {
@@ -607,7 +608,7 @@
         val inflationResult =
             WidgetInflater.InflationResult(
                 type = WidgetInflater.TYPE_PENDING,
-                widgetInfo = mockProviderInfo
+                widgetInfo = mockProviderInfo,
             )
         mockWidgetInflater =
             mock<WidgetInflater>().apply {
@@ -662,7 +663,7 @@
         verify(mockCursor)
             .markDeleted(
                 "processWidget: Unrestored Pending widget removed: id=1, appWidgetId=0, component=$expectedComponentName, restoreFlag:=4",
-                LauncherRestoreEventLogger.RestoreError.APP_NOT_INSTALLED
+                LauncherRestoreEventLogger.RestoreError.APP_NOT_INSTALLED,
             )
     }
 
@@ -670,12 +671,6 @@
     fun `When widget inflation result is TYPE_DELETE then mark deleted`() {
         // Given
         val expectedProvider = "com.google.android.testApp/com.android.testApp.testAppProvider"
-        val expectedComponentName = ComponentName.unflattenFromString(expectedProvider)
-        val expectedPackage = expectedComponentName!!.packageName
-        mockPmHelper =
-            mock<PackageManagerHelper>().apply {
-                whenever(isAppArchived(expectedPackage)).thenReturn(true)
-            }
         mockCursor =
             mock<LoaderCursor>().apply {
                 itemType = ITEM_TYPE_APPWIDGET
@@ -694,7 +689,7 @@
                 type = WidgetInflater.TYPE_DELETE,
                 widgetInfo = null,
                 reason = "test_delete_reason",
-                restoreErrorType = MISSING_WIDGET_PROVIDER
+                restoreErrorType = MISSING_WIDGET_PROVIDER,
             )
         mockWidgetInflater =
             mock<WidgetInflater>().apply {
diff --git a/tests/multivalentTests/src/com/android/launcher3/model/WorkspaceItemSpaceFinderTest.kt b/tests/multivalentTests/src/com/android/launcher3/model/WorkspaceItemSpaceFinderTest.kt
index ae8e966..dd03eee 100644
--- a/tests/multivalentTests/src/com/android/launcher3/model/WorkspaceItemSpaceFinderTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/model/WorkspaceItemSpaceFinderTest.kt
@@ -21,7 +21,6 @@
 import com.google.common.truth.Truth.assertThat
 import org.junit.After
 import org.junit.Before
-import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -30,8 +29,6 @@
 @RunWith(AndroidJUnit4::class)
 class WorkspaceItemSpaceFinderTest : AbstractWorkspaceModelTest() {
 
-    @get:Rule val modelTestRule = ModelTestRule()
-
     private val mItemSpaceFinder = WorkspaceItemSpaceFinder()
 
     @Before
@@ -52,7 +49,7 @@
                 mExistingScreens,
                 mNewScreens,
                 spanX,
-                spanY
+                spanY,
             )
             .let { NewItemSpace.fromIntArray(it) }
 
@@ -62,7 +59,7 @@
                     newItemSpace.cellX,
                     newItemSpace.cellY,
                     spanX,
-                    spanY
+                    spanY,
                 )
             )
             .isTrue()
@@ -171,7 +168,7 @@
             screen0 = listOf(Rect(2, 0, 5, 2)),
             screen1 = fullScreenSpaces, // full screens are skipped
             screen2 = fullScreenSpaces, // full screens are skipped
-            screen3 = emptyScreenSpaces
+            screen3 = emptyScreenSpaces,
         )
 
         val spaceFound = findSpace(3, 1)
diff --git a/tests/multivalentTests/src/com/android/launcher3/pm/InstallSessionTrackerTest.kt b/tests/multivalentTests/src/com/android/launcher3/pm/InstallSessionTrackerTest.kt
index d860710..15a9964 100644
--- a/tests/multivalentTests/src/com/android/launcher3/pm/InstallSessionTrackerTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/pm/InstallSessionTrackerTest.kt
@@ -26,7 +26,6 @@
 import androidx.test.filters.SdkSuppress
 import androidx.test.filters.SmallTest
 import com.android.launcher3.Flags.FLAG_ENABLE_SUPPORT_FOR_ARCHIVING
-import com.android.launcher3.model.ModelTestRule
 import com.android.launcher3.util.Executors.MODEL_EXECUTOR
 import com.android.launcher3.util.LauncherModelHelper
 import com.android.launcher3.util.PackageUserKey
@@ -45,9 +44,7 @@
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class InstallSessionTrackerTest {
-    @get:Rule(order = 0) val setFlagsRule = SetFlagsRule()
-
-    @get:Rule(order = 1) val modelTestRule = ModelTestRule()
+    @get:Rule val setFlagsRule = SetFlagsRule()
 
     private val mockInstallSessionHelper: InstallSessionHelper = mock()
     private val mockCallback: InstallSessionTracker.Callback = mock()
@@ -67,7 +64,7 @@
                 mockInstallSessionHelper,
                 mockCallback,
                 mockPackageInstaller,
-                launcherApps
+                launcherApps,
             )
     }
 
diff --git a/tests/multivalentTests/src/com/android/launcher3/pm/UserCacheTest.kt b/tests/multivalentTests/src/com/android/launcher3/pm/UserCacheTest.kt
index 482dced..5f08c31 100644
--- a/tests/multivalentTests/src/com/android/launcher3/pm/UserCacheTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/pm/UserCacheTest.kt
@@ -20,7 +20,6 @@
 import android.os.UserHandle
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.platform.app.InstrumentationRegistry
-import com.android.launcher3.model.ModelTestRule
 import com.android.launcher3.util.Executors.MODEL_EXECUTOR
 import com.android.launcher3.util.LauncherModelHelper
 import com.android.launcher3.util.TestUtil
@@ -28,15 +27,12 @@
 import com.google.common.truth.Truth.assertThat
 import org.junit.After
 import org.junit.Before
-import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
 @RunWith(AndroidJUnit4::class)
 class UserCacheTest {
 
-    @get:Rule val modelTestRule = ModelTestRule()
-
     private val launcherModelHelper = LauncherModelHelper()
     private val sandboxContext = launcherModelHelper.sandboxContext
     private lateinit var userCache: UserCache
diff --git a/tests/multivalentTests/src/com/android/launcher3/provider/RestoreDbTaskTest.java b/tests/multivalentTests/src/com/android/launcher3/provider/RestoreDbTaskTest.java
index b3675a6..d0c168a 100644
--- a/tests/multivalentTests/src/com/android/launcher3/provider/RestoreDbTaskTest.java
+++ b/tests/multivalentTests/src/com/android/launcher3/provider/RestoreDbTaskTest.java
@@ -101,7 +101,7 @@
         mMockController = Mockito.mock(ModelDbController.class);
         mMockDb = mock(SQLiteDatabase.class);
         mMockCursor = mock(Cursor.class);
-        mPrefs = new LauncherPrefs(mContext);
+        mPrefs = LauncherPrefs.get(mContext);
         mMockRestoreEventLogger = mock(LauncherRestoreEventLogger.class);
     }
 
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/ApplicationInfoWrapperTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/ApplicationInfoWrapperTest.kt
new file mode 100644
index 0000000..86c3fd8
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/util/ApplicationInfoWrapperTest.kt
@@ -0,0 +1,131 @@
+/*
+ * 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.pm.ApplicationInfo
+import android.content.pm.ApplicationInfo.FLAG_EXTERNAL_STORAGE
+import android.content.pm.ApplicationInfo.FLAG_INSTALLED
+import android.content.pm.ApplicationInfo.FLAG_SUSPENDED
+import android.content.pm.ApplicationInfo.FLAG_SYSTEM
+import android.content.pm.LauncherApps
+import android.os.UserHandle
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.Flags.FLAG_ENABLE_SUPPORT_FOR_ARCHIVING
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito
+import org.mockito.kotlin.any
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.whenever
+
+/** Unit tests for {@link ApplicationInfoWrapper}. */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ApplicationInfoWrapperTest {
+
+    @get:Rule val setFlagsRule = SetFlagsRule(SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT)
+
+    private lateinit var context: Context
+    private lateinit var launcherApps: LauncherApps
+
+    @Before
+    fun setup() {
+        context = Mockito.mock(Context::class.java)
+        launcherApps = Mockito.mock(LauncherApps::class.java)
+        whenever(context.getSystemService(eq(LauncherApps::class.java))).thenReturn(launcherApps)
+    }
+
+    @Test
+    @EnableFlags(FLAG_ENABLE_SUPPORT_FOR_ARCHIVING)
+    fun archivedApp_appInfoIsNotNull() {
+        val applicationInfo = ApplicationInfo()
+        applicationInfo.isArchived = true
+        whenever(launcherApps.getApplicationInfo(eq(TEST_PACKAGE), any(), eq(TEST_USER)))
+            .thenReturn(applicationInfo)
+
+        val wrapper = ApplicationInfoWrapper(context, TEST_PACKAGE, TEST_USER)
+        assertNotNull(wrapper.getInfo())
+        assertTrue(wrapper.isArchived())
+        assertFalse(wrapper.isInstalled())
+    }
+
+    @Test
+    fun notInstalledApp_nullAppInfo() {
+        val applicationInfo = ApplicationInfo()
+        whenever(launcherApps.getApplicationInfo(eq(TEST_PACKAGE), any(), eq(TEST_USER)))
+            .thenReturn(applicationInfo)
+
+        val wrapper = ApplicationInfoWrapper(context, TEST_PACKAGE, TEST_USER)
+        assertNull(wrapper.getInfo())
+        assertFalse(wrapper.isInstalled())
+    }
+
+    @Test
+    fun appInfo_suspended() {
+        val wrapper =
+            ApplicationInfoWrapper(
+                ApplicationInfo().apply { flags = FLAG_INSTALLED.or(FLAG_SUSPENDED) }
+            )
+        assertTrue(wrapper.isSuspended())
+    }
+
+    @Test
+    fun appInfo_notSuspended() {
+        val wrapper = ApplicationInfoWrapper(ApplicationInfo())
+        assertFalse(wrapper.isSuspended())
+    }
+
+    @Test
+    fun appInfo_system() {
+        val wrapper = ApplicationInfoWrapper(ApplicationInfo().apply { flags = FLAG_SYSTEM })
+        assertTrue(wrapper.isSystem())
+    }
+
+    @Test
+    fun appInfo_notSystem() {
+        val wrapper = ApplicationInfoWrapper(ApplicationInfo())
+        assertFalse(wrapper.isSystem())
+    }
+
+    @Test
+    fun appInfo_onSDCard() {
+        val wrapper =
+            ApplicationInfoWrapper(ApplicationInfo().apply { flags = FLAG_EXTERNAL_STORAGE })
+        assertTrue(wrapper.isOnSdCard())
+    }
+
+    @Test
+    fun appInfo_notOnSDCard() {
+        val wrapper = ApplicationInfoWrapper(ApplicationInfo())
+        assertFalse(wrapper.isOnSdCard())
+    }
+
+    companion object {
+        const val TEST_PACKAGE = "com.android.test.package"
+        private val TEST_USER = UserHandle.of(3)
+    }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/DaggerSingletonDeadlockTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/DaggerSingletonDeadlockTest.kt
new file mode 100644
index 0000000..642c628
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/util/DaggerSingletonDeadlockTest.kt
@@ -0,0 +1,77 @@
+/*
+ * 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 androidx.test.filters.SmallTest
+import com.android.launcher3.dagger.LauncherAppComponent
+import com.android.launcher3.util.LauncherModelHelper.SandboxModelContext
+import java.util.concurrent.TimeUnit.SECONDS
+import kotlin.reflect.KFunction
+import kotlin.reflect.full.memberFunctions
+import org.junit.After
+import org.junit.Assert
+import org.junit.Test
+import org.junit.runner.RunWith
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
+
+@SmallTest
+@RunWith(ParameterizedAndroidJunit4::class)
+class DaggerSingletonDeadlockTest(val method: KFunction<*>, val methodName: String) {
+
+    private val context = SandboxModelContext()
+
+    @After
+    fun tearDown() {
+        context.onDestroy()
+    }
+
+    /** Test to verify that the object can be created successfully on the main thread. */
+    @Test
+    fun objectCreationOnMainThread() {
+        Executors.MAIN_EXECUTOR.submit {
+                method.call(context.appComponent).also(Assert::assertNotNull)
+            }
+            .get(10, SECONDS)
+    }
+
+    /**
+     * Test to verify that the object can be created successfully on the background thread, when the
+     * main thread is blocked.
+     */
+    @Test
+    fun objectCreationOnBackgroundThread() {
+        TestUtil.runOnExecutorSync(Executors.MAIN_EXECUTOR) {
+            Executors.THREAD_POOL_EXECUTOR.submit {
+                    method.call(context.appComponent).also(Assert::assertNotNull)
+                }
+                .get(10, SECONDS)
+        }
+    }
+
+    companion object {
+        @Parameters(name = "{1}")
+        @JvmStatic
+        fun getTestMethods() =
+            LauncherAppComponent::class
+                .memberFunctions
+                .filter { it.parameters.size == 1 }
+                .map {
+                    arrayOf(it, if (it.name.startsWith("get")) it.name.substring(3) else it.name)
+                }
+    }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/DisplayControllerTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/DisplayControllerTest.kt
index 308f200..a3a680e 100644
--- a/tests/multivalentTests/src/com/android/launcher3/util/DisplayControllerTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/util/DisplayControllerTest.kt
@@ -207,4 +207,21 @@
             .onDisplayInfoChanged(any(), any(), eq(CHANGE_TASKBAR_PINNING))
         assertFalse(displayController.getInfo().isTransientTaskbar())
     }
+
+    @Test
+    @UiThreadTest
+    fun testLockedTaskbarChangeOnConfigurationChanged() {
+        whenever(windowManagerProxy.showLockedTaskbarOnHome(any())).thenReturn(true)
+        whenever(windowManagerProxy.isHomeVisible(any())).thenReturn(true)
+        whenever(windowManagerProxy.isInDesktopMode()).thenReturn(false)
+        whenever(launcherPrefs.get(TASKBAR_PINNING)).thenReturn(false)
+        DisplayController.enableTaskbarModePreferenceForTests(true)
+        assertTrue(displayController.getInfo().isTransientTaskbar())
+
+        displayController.onConfigurationChanged(configuration)
+
+        verify(displayInfoChangeListener)
+            .onDisplayInfoChanged(any(), any(), eq(CHANGE_TASKBAR_PINNING))
+        assertFalse(displayController.getInfo().isTransientTaskbar())
+    }
 }
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/LauncherModelHelper.java b/tests/multivalentTests/src/com/android/launcher3/util/LauncherModelHelper.java
index 2d53e29..09b9a3b 100644
--- a/tests/multivalentTests/src/com/android/launcher3/util/LauncherModelHelper.java
+++ b/tests/multivalentTests/src/com/android/launcher3/util/LauncherModelHelper.java
@@ -22,6 +22,7 @@
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 import static com.android.launcher3.util.TestUtil.runOnExecutorSync;
+import static com.android.launcher3.util.TestUtil.grantWriteSecurePermission;
 
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
@@ -30,6 +31,7 @@
 
 import android.content.ContentProvider;
 import android.content.ContentResolver;
+import android.content.Context;
 import android.content.pm.PackageInstaller;
 import android.content.pm.PackageInstaller.SessionParams;
 import android.content.pm.PackageManager;
@@ -185,6 +187,8 @@
      */
     public LauncherModelHelper setupDefaultLayoutProvider(LauncherLayoutBuilder builder)
             throws Exception {
+        grantWriteSecurePermission();
+
         InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(sandboxContext);
         if (idp.numRows == 0 && idp.numColumns == 0) {
             idp.numRows = idp.numColumns = idp.numDatabaseHotseatIcons = DEFAULT_GRID_SIZE;
@@ -247,15 +251,16 @@
         private final File mDbDir;
 
         public SandboxModelContext() {
-            super(ApplicationProvider.getApplicationContext());
+            this(ApplicationProvider.getApplicationContext());
+        }
+
+        public SandboxModelContext(Context context) {
+            super(context);
 
             // System settings cache content provider. Ensure that they are statically initialized
-            Settings.Secure.getString(
-                    ApplicationProvider.getApplicationContext().getContentResolver(), "test");
-            Settings.System.getString(
-                    ApplicationProvider.getApplicationContext().getContentResolver(), "test");
-            Settings.Global.getString(
-                    ApplicationProvider.getApplicationContext().getContentResolver(), "test");
+            Settings.Secure.getString(context.getContentResolver(), "test");
+            Settings.System.getString(context.getContentResolver(), "test");
+            Settings.Global.getString(context.getContentResolver(), "test");
 
             mPm = spy(getBaseContext().getPackageManager());
             mDbDir = new File(getCacheDir(), UUID.randomUUID().toString());
@@ -283,11 +288,11 @@
         }
 
         @Override
-        public void onDestroy() {
+        protected void cleanUpObjects() {
             if (deleteContents(mDbDir)) {
                 mDbDir.delete();
             }
-            super.onDestroy();
+            super.cleanUpObjects();
         }
 
         @Override
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/ModelTestExtensions.kt b/tests/multivalentTests/src/com/android/launcher3/util/ModelTestExtensions.kt
index 6bd182b..8d072d8 100644
--- a/tests/multivalentTests/src/com/android/launcher3/util/ModelTestExtensions.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/util/ModelTestExtensions.kt
@@ -1,6 +1,7 @@
 package com.android.launcher3.util
 
 import android.content.ContentValues
+import com.android.launcher3.Flags
 import com.android.launcher3.LauncherModel
 import com.android.launcher3.LauncherSettings.Favorites
 import com.android.launcher3.LauncherSettings.Favorites.APPWIDGET_ID
@@ -30,7 +31,8 @@
         loadModelSync()
         TestUtil.runOnExecutorSync(Executors.MODEL_EXECUTOR) {
             modelDbController.run {
-                tryMigrateDB(null /* restoreEventLogger */)
+                if (Flags.gridMigrationRefactor()) attemptMigrateDb(null /* restoreEventLogger */)
+                else tryMigrateDB(null /* restoreEventLogger */)
                 createEmptyDB()
                 clearEmptyDbFlag()
             }
@@ -67,12 +69,12 @@
         tableName: String = Favorites.TABLE_NAME,
         appWidgetId: Int = -1,
         appWidgetSource: Int = -1,
-        appWidgetProvider: String? = null
+        appWidgetProvider: String? = null,
     ) {
         loadModelSync()
         TestUtil.runOnExecutorSync(Executors.MODEL_EXECUTOR) {
             val controller: ModelDbController = modelDbController
-            controller.tryMigrateDB(null /* restoreEventLogger */)
+            controller.attemptMigrateDb(null /* restoreEventLogger */)
             modelDbController.newTransaction().use { transaction ->
                 val values =
                     ContentValues().apply {
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/PackageManagerHelperTest.java b/tests/multivalentTests/src/com/android/launcher3/util/PackageManagerHelperTest.java
deleted file mode 100644
index b5e797e..0000000
--- a/tests/multivalentTests/src/com/android/launcher3/util/PackageManagerHelperTest.java
+++ /dev/null
@@ -1,86 +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.util;
-
-import static com.android.launcher3.Flags.FLAG_ENABLE_SUPPORT_FOR_ARCHIVING;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-import android.content.Context;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.LauncherApps;
-import android.content.pm.PackageManager;
-import android.os.UserHandle;
-import android.platform.test.annotations.RequiresFlagsEnabled;
-import android.platform.test.flag.junit.CheckFlagsRule;
-import android.platform.test.flag.junit.DeviceFlagsValueProvider;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-import org.junit.runner.RunWith;
-
-/** Unit tests for {@link PackageManagerHelper}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public final class PackageManagerHelperTest {
-    @Rule
-    public ExpectedException exception = ExpectedException.none();
-
-    @Rule
-    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
-
-    private static final String TEST_PACKAGE = "com.android.test.package";
-    private static final int TEST_USER = 2;
-
-    private Context mContext;
-    private LauncherApps mLauncherApps;
-    private PackageManagerHelper mPackageManagerHelper;
-
-    @Before
-    public void setup() {
-        mContext = mock(Context.class);
-        mLauncherApps = mock(LauncherApps.class);
-        when(mContext.getSystemService(eq(LauncherApps.class))).thenReturn(mLauncherApps);
-        when(mContext.getResources()).thenReturn(
-                InstrumentationRegistry.getInstrumentation().getTargetContext().getResources());
-        mPackageManagerHelper = new PackageManagerHelper(mContext);
-    }
-
-    @Test
-    @RequiresFlagsEnabled(FLAG_ENABLE_SUPPORT_FOR_ARCHIVING)
-    public void getApplicationInfo_archivedApp_appInfoIsNotNull()
-            throws PackageManager.NameNotFoundException {
-        ApplicationInfo applicationInfo = new ApplicationInfo();
-        applicationInfo.isArchived = true;
-        when(mLauncherApps.getApplicationInfo(TEST_PACKAGE, 0 /* flags */,
-                UserHandle.of(TEST_USER)))
-                .thenReturn(applicationInfo);
-
-        assertThat(mPackageManagerHelper.getApplicationInfo(TEST_PACKAGE, UserHandle.of(TEST_USER),
-                0 /* flags */))
-                .isNotNull();
-    }
-}
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/SandboxApplication.kt b/tests/multivalentTests/src/com/android/launcher3/util/SandboxApplication.kt
new file mode 100644
index 0000000..efe7637
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/util/SandboxApplication.kt
@@ -0,0 +1,164 @@
+/*
+ * 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.ContextParams
+import android.content.ContextWrapper
+import android.content.pm.ApplicationInfo
+import android.content.res.Configuration
+import android.os.Bundle
+import android.os.IBinder
+import android.os.UserHandle
+import android.view.Display
+import androidx.test.core.app.ApplicationProvider
+import com.android.launcher3.util.LauncherModelHelper.SandboxModelContext
+import com.android.launcher3.util.MainThreadInitializedObject.ObjectSandbox
+import org.junit.Rule
+import org.junit.rules.ExternalResource
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+/**
+ * Sandbox application where created [Context] instances are still sandboxed within it.
+ *
+ * Tests can declare this application as a [Rule], so that it is set up and destroyed automatically.
+ * Alternatively, they can call [init] and [onDestroy] directly. Either way, these need to be called
+ * for it to work and avoid leaks from created singletons.
+ *
+ * The create [Context] APIs construct a `ContextImpl`, which resets the application to the true
+ * application, thus leaving the sandbox. This implementation wraps the created contexts to
+ * propagate this application (see [SandboxApplicationWrapper]).
+ */
+class SandboxApplication private constructor(private val base: SandboxApplicationWrapper) :
+    SandboxModelContext(base), TestRule {
+
+    constructor(
+        base: Context = ApplicationProvider.getApplicationContext()
+    ) : this(SandboxApplicationWrapper(base))
+
+    /**
+     * Initializes the sandbox application propagation logic.
+     *
+     * This function either needs to be called manually or automatically through using [Rule].
+     */
+    fun init() {
+        base.app = this@SandboxApplication
+    }
+
+    /** Returns `this` if [init] was called, otherwise crashes the test. */
+    override fun getApplicationContext(): Context = base.applicationContext
+
+    override fun shouldCleanUpOnDestroy(): Boolean {
+        // Defer to the true application to decide whether to clean up. For instance, we do not want
+        // to cleanup under Robolectric.
+        val app = ApplicationProvider.getApplicationContext<Context>()
+        return if (app is ObjectSandbox) app.shouldCleanUpOnDestroy() else true
+    }
+
+    override fun apply(statement: Statement, description: Description): Statement {
+        return object : ExternalResource() {
+                override fun before() = init()
+
+                override fun after() = onDestroy()
+            }
+            .apply(statement, description)
+    }
+}
+
+private class SandboxApplicationWrapper(base: Context, var app: Context? = null) :
+    ContextWrapper(base) {
+
+    override fun getApplicationContext(): Context {
+        return checkNotNull(app) { "SandboxApplication accessed before #init() was called." }
+    }
+
+    override fun createPackageContext(packageName: String?, flags: Int): Context {
+        return SandboxApplicationWrapper(super.createPackageContext(packageName, flags), app)
+    }
+
+    override fun createPackageContextAsUser(
+        packageName: String,
+        flags: Int,
+        user: UserHandle,
+    ): Context {
+        return SandboxApplicationWrapper(
+            super.createPackageContextAsUser(packageName, flags, user),
+            app,
+        )
+    }
+
+    override fun createContextAsUser(user: UserHandle, flags: Int): Context {
+        return SandboxApplicationWrapper(super.createContextAsUser(user, flags), app)
+    }
+
+    override fun createApplicationContext(application: ApplicationInfo?, flags: Int): Context {
+        return SandboxApplicationWrapper(super.createApplicationContext(application, flags), app)
+    }
+
+    override fun createContextForSdkInSandbox(sdkInfo: ApplicationInfo, flags: Int): Context {
+        return SandboxApplicationWrapper(super.createContextForSdkInSandbox(sdkInfo, flags), app)
+    }
+
+    override fun createContextForSplit(splitName: String?): Context {
+        return SandboxApplicationWrapper(super.createContextForSplit(splitName), app)
+    }
+
+    override fun createConfigurationContext(overrideConfiguration: Configuration): Context {
+        return SandboxApplicationWrapper(
+            super.createConfigurationContext(overrideConfiguration),
+            app,
+        )
+    }
+
+    override fun createDisplayContext(display: Display): Context {
+        return SandboxApplicationWrapper(super.createDisplayContext(display), app)
+    }
+
+    override fun createDeviceContext(deviceId: Int): Context {
+        return SandboxApplicationWrapper(super.createDeviceContext(deviceId), app)
+    }
+
+    override fun createWindowContext(type: Int, options: Bundle?): Context {
+        return SandboxApplicationWrapper(super.createWindowContext(type, options), app)
+    }
+
+    override fun createWindowContext(display: Display, type: Int, options: Bundle?): Context {
+        return SandboxApplicationWrapper(super.createWindowContext(display, type, options), app)
+    }
+
+    override fun createContext(contextParams: ContextParams): Context {
+        return SandboxApplicationWrapper(super.createContext(contextParams), app)
+    }
+
+    override fun createAttributionContext(attributionTag: String?): Context {
+        return SandboxApplicationWrapper(super.createAttributionContext(attributionTag), app)
+    }
+
+    override fun createCredentialProtectedStorageContext(): Context {
+        return SandboxApplicationWrapper(super.createCredentialProtectedStorageContext(), app)
+    }
+
+    override fun createDeviceProtectedStorageContext(): Context {
+        return SandboxApplicationWrapper(super.createDeviceProtectedStorageContext(), app)
+    }
+
+    override fun createTokenContext(token: IBinder, display: Display): Context {
+        return SandboxApplicationWrapper(super.createTokenContext(token, display), app)
+    }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/SandboxApplicationTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/SandboxApplicationTest.kt
new file mode 100644
index 0000000..d87a406
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/util/SandboxApplicationTest.kt
@@ -0,0 +1,78 @@
+/*
+ * 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.hardware.display.DisplayManager
+import android.view.Display
+import android.view.Display.DEFAULT_DISPLAY
+import android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(LauncherMultivalentJUnit::class)
+class SandboxApplicationTest {
+    @get:Rule val app = SandboxApplication()
+
+    private val display: Display
+        get() {
+            return checkNotNull(app.getSystemService(DisplayManager::class.java))
+                .getDisplay(DEFAULT_DISPLAY)
+        }
+
+    @Test
+    fun testCreateDisplayContext_isSandboxed() {
+        val displayContext = app.createDisplayContext(display)
+        assertThat(displayContext.applicationContext).isEqualTo(app)
+    }
+
+    @Test
+    fun testCreateWindowContext_fromSandboxedDisplayContext_isSandboxed() {
+        val displayContext = app.createDisplayContext(display)
+        val nestedContext = displayContext.createWindowContext(TYPE_APPLICATION_OVERLAY, null)
+        assertThat(nestedContext.applicationContext).isEqualTo(app)
+    }
+
+    @Test(expected = IllegalStateException::class)
+    fun testGetApplicationContext_beforeManualInit_throwsException() {
+        val manualApp = SandboxApplication()
+        assertThat(manualApp.applicationContext).isEqualTo(manualApp)
+    }
+
+    @Test
+    fun testGetApplicationContext_afterManualInit_isApplication() {
+        SandboxApplication().run {
+            init()
+            assertThat(applicationContext).isEqualTo(this)
+            onDestroy()
+        }
+    }
+
+    @Test
+    fun testGetObject_objectCreatesDisplayContext_isSandboxed() {
+        class TestSingleton(context: Context) : SafeCloseable {
+            override fun close() = Unit
+
+            val displayContext = context.createDisplayContext(display)
+        }
+
+        val displayContext = MainThreadInitializedObject { TestSingleton(it) }[app].displayContext
+        assertThat(displayContext.applicationContext).isEqualTo(app)
+    }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/ScreenOnTrackerTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/ScreenOnTrackerTest.kt
index 430aad2..45cc19c 100644
--- a/tests/multivalentTests/src/com/android/launcher3/util/ScreenOnTrackerTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/util/ScreenOnTrackerTest.kt
@@ -39,13 +39,14 @@
     @Mock private lateinit var receiver: SimpleBroadcastReceiver
     @Mock private lateinit var context: Context
     @Mock private lateinit var listener: ScreenOnTracker.ScreenOnListener
+    @Mock private lateinit var tracker: DaggerSingletonTracker
 
     private lateinit var underTest: ScreenOnTracker
 
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
-        underTest = ScreenOnTracker(context, receiver)
+        underTest = ScreenOnTracker(context, receiver, tracker)
     }
 
     @Test
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/SystemUiControllerTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/SystemUiControllerTest.kt
index 612fcd4..043bdac 100644
--- a/tests/multivalentTests/src/com/android/launcher3/util/SystemUiControllerTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/util/SystemUiControllerTest.kt
@@ -52,7 +52,7 @@
     fun setup() {
         MockitoAnnotations.initMocks(this)
         `when`(window.decorView).thenReturn(decorView)
-        underTest = SystemUiController(window)
+        underTest = SystemUiController(window.decorView)
     }
 
     @Test
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/TestUtil.java b/tests/multivalentTests/src/com/android/launcher3/util/TestUtil.java
index 3646f0c..eb25acf 100644
--- a/tests/multivalentTests/src/com/android/launcher3/util/TestUtil.java
+++ b/tests/multivalentTests/src/com/android/launcher3/util/TestUtil.java
@@ -15,22 +15,24 @@
  */
 package com.android.launcher3.util;
 
-import static android.util.Base64.NO_PADDING;
-import static android.util.Base64.NO_WRAP;
-
 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
 
-import static com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_KEY;
 import static com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_LABEL;
 import static com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_TAG;
+import static com.android.launcher3.LauncherSettings.Settings.LAYOUT_PROVIDER_KEY;
+import static com.android.launcher3.LauncherSettings.Settings.createBlobProviderKey;
 
 import static org.junit.Assert.assertTrue;
 
+import android.Manifest;
 import android.app.Instrumentation;
 import android.app.blob.BlobHandle;
 import android.app.blob.BlobStoreManager;
 import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
 import android.content.pm.LauncherApps;
+import android.content.pm.PackageManager;
 import android.content.res.Resources;
 import android.graphics.Point;
 import android.os.AsyncTask;
@@ -41,7 +43,6 @@
 import android.os.UserHandle;
 import android.provider.Settings;
 import android.system.OsConstants;
-import android.util.Base64;
 import android.util.Log;
 
 import androidx.test.uiautomator.UiDevice;
@@ -168,11 +169,12 @@
             session.commit(AsyncTask.THREAD_POOL_EXECUTOR, i -> wait.countDown());
         }
 
-        String key = Base64.encodeToString(digest, NO_WRAP | NO_PADDING);
-        Settings.Secure.putString(context.getContentResolver(), LAYOUT_DIGEST_KEY, key);
+        grantWriteSecurePermission();
+        Settings.Secure.putString(
+                context.getContentResolver(), LAYOUT_PROVIDER_KEY, createBlobProviderKey(digest));
         wait.await();
         return () ->
-            Settings.Secure.putString(context.getContentResolver(), LAYOUT_DIGEST_KEY, null);
+            Settings.Secure.putString(context.getContentResolver(), LAYOUT_PROVIDER_KEY, null);
     }
 
     /**
@@ -224,6 +226,23 @@
         assertTrue(message, failed);
     }
 
+    /**
+     * Grants [WRITE_SECURE_SETTINGS] permission in runtime.
+     */
+    public static void grantWriteSecurePermission() {
+        getInstrumentation().getUiAutomation()
+                .adoptShellPermissionIdentity(Manifest.permission.WRITE_SECURE_SETTINGS);
+    }
+
+    /**
+     * Returns the activity info corresponding to the system app for the provided category
+     */
+    public static ActivityInfo resolveSystemAppInfo(String category) {
+        return getInstrumentation().getTargetContext().getPackageManager().resolveActivity(
+                new Intent(Intent.ACTION_MAIN).addCategory(category),
+                PackageManager.MATCH_SYSTEM_ONLY).activityInfo;
+    }
+
     /** Interface to indicate a runnable which can throw any exception. */
     public interface UncheckedRunnable {
         /** Method to run the task */
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/VibratorWrapperTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/VibratorWrapperTest.kt
index d321e41..0f212eb 100644
--- a/tests/multivalentTests/src/com/android/launcher3/util/VibratorWrapperTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/util/VibratorWrapperTest.kt
@@ -21,8 +21,8 @@
 import android.os.VibrationEffect.Composition.PRIMITIVE_LOW_TICK
 import android.os.VibrationEffect.Composition.PRIMITIVE_TICK
 import android.os.Vibrator
-import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.launcher3.util.LauncherModelHelper.SandboxModelContext
 import com.android.launcher3.util.VibratorWrapper.HAPTIC_FEEDBACK_URI
 import com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC
 import com.android.launcher3.util.VibratorWrapper.VIBRATION_ATTRS
@@ -41,40 +41,36 @@
 import org.mockito.kotlin.same
 
 @SmallTest
-@RunWith(AndroidJUnit4::class)
+@RunWith(LauncherMultivalentJUnit::class)
 class VibratorWrapperTest {
 
     @Mock private lateinit var settingsCache: SettingsCache
-    @Mock private lateinit var vibrator: Vibrator
+    private lateinit var vibrator: Vibrator
+    private val context: SandboxModelContext = SandboxModelContext()
     @Captor private lateinit var vibrationEffectCaptor: ArgumentCaptor<VibrationEffect>
-
+    @Mock private lateinit var tracker: DaggerSingletonTracker
     private lateinit var underTest: VibratorWrapper
 
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
+        vibrator = context.spyService(Vibrator::class.java)
         `when`(settingsCache.getValue(HAPTIC_FEEDBACK_URI, 0)).thenReturn(true)
         `when`(vibrator.hasVibrator()).thenReturn(true)
         `when`(vibrator.areAllPrimitivesSupported(PRIMITIVE_TICK)).thenReturn(true)
         `when`(vibrator.areAllPrimitivesSupported(PRIMITIVE_LOW_TICK)).thenReturn(true)
         `when`(vibrator.getPrimitiveDurations(PRIMITIVE_LOW_TICK)).thenReturn(intArrayOf(10))
 
-        underTest = VibratorWrapper(vibrator, settingsCache)
+        underTest = VibratorWrapper(context, settingsCache, tracker)
     }
 
     @Test
     fun init_register_onChangeListener() {
+        TestUtil.runOnExecutorSync(Executors.MAIN_EXECUTOR) {}
         verify(settingsCache).register(HAPTIC_FEEDBACK_URI, underTest.mHapticChangeListener)
     }
 
     @Test
-    fun close_unregister_onChangeListener() {
-        underTest.close()
-
-        verify(settingsCache).unregister(HAPTIC_FEEDBACK_URI, underTest.mHapticChangeListener)
-    }
-
-    @Test
     fun vibrate() {
         underTest.vibrate(OVERVIEW_HAPTIC)
 
@@ -117,7 +113,7 @@
     @Test
     fun haptic_feedback_disabled_no_vibrate() {
         `when`(vibrator.hasVibrator()).thenReturn(false)
-        underTest = VibratorWrapper(vibrator, settingsCache)
+        underTest = VibratorWrapper(context, settingsCache, tracker)
 
         underTest.vibrate(OVERVIEW_HAPTIC)
 
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/GeneratedPreviewTest.kt b/tests/multivalentTests/src/com/android/launcher3/widget/GeneratedPreviewTest.kt
index ec83b8b..7484bce 100644
--- a/tests/multivalentTests/src/com/android/launcher3/widget/GeneratedPreviewTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/GeneratedPreviewTest.kt
@@ -38,8 +38,8 @@
     @get:Rule val checkFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()
     private val providerName =
         ComponentName(
-            "com.android.launcher3.tests",
-            "com.android.launcher3.testcomponent.AppWidgetNoConfig"
+            getInstrumentation().getContext().getPackageName(),
+            "com.android.launcher3.testcomponent.AppWidgetNoConfig",
         )
     private val generatedPreviewLayout =
         getInstrumentation().context.run {
@@ -61,7 +61,7 @@
                     ActivityContextWrapper(
                         ContextThemeWrapper(
                             context,
-                            com.android.launcher3.R.style.WidgetContainerTheme
+                            com.android.launcher3.R.style.WidgetContainerTheme,
                         )
                     )
                 )
@@ -78,7 +78,7 @@
             object : WidgetManagerHelper(context) {
                 override fun loadGeneratedPreview(
                     info: AppWidgetProviderInfo,
-                    widgetCategory: Int
+                    widgetCategory: Int,
                 ) =
                     generatedPreview.takeIf {
                         info === appWidgetProviderInfo &&
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/RoundedCornerEnforcementTest.kt b/tests/multivalentTests/src/com/android/launcher3/widget/RoundedCornerEnforcementTest.kt
index db77702..c82e84c 100644
--- a/tests/multivalentTests/src/com/android/launcher3/widget/RoundedCornerEnforcementTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/RoundedCornerEnforcementTest.kt
@@ -19,14 +19,19 @@
 import android.content.Context
 import android.content.res.Resources
 import android.graphics.Rect
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
 import android.view.View
 import android.view.ViewGroup
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.launcher3.Flags
 import com.android.launcher3.R
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertSame
 import org.junit.Assert.assertTrue
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mockito.mock
@@ -38,6 +43,8 @@
 @RunWith(AndroidJUnit4::class)
 class RoundedCornerEnforcementTest {
 
+    @get:Rule val setFlagsRule: SetFlagsRule = SetFlagsRule()
+
     @Test
     fun `Widget view has one background`() {
         val mockWidgetView = mock(LauncherAppWidgetHostView::class.java)
@@ -72,14 +79,15 @@
         RoundedCornerEnforcement.computeRoundedRectangle(
             mockWidgetView,
             mockBackgroundView,
-            testRect
+            testRect,
         )
 
         assertEquals(Rect(50, 75, 250, 275), testRect)
     }
 
     @Test
-    fun `Compute system radius`() {
+    @DisableFlags(Flags.FLAG_ENFORCE_SYSTEM_RADIUS_FOR_APP_WIDGETS)
+    fun `Compute system radius when smaller`() {
         val mockContext = mock(Context::class.java)
         val mockRes = mock(Resources::class.java)
 
@@ -94,6 +102,41 @@
         assertEquals(RADIUS, RoundedCornerEnforcement.computeEnforcedRadius(mockContext))
     }
 
+    @Test
+    @DisableFlags(Flags.FLAG_ENFORCE_SYSTEM_RADIUS_FOR_APP_WIDGETS)
+    fun `Compute launcher radius when smaller`() {
+        val mockContext = mock(Context::class.java)
+        val mockRes = mock(Resources::class.java)
+
+        doReturn(mockRes).whenever(mockContext).resources
+        doReturn(LAUNCHER_RADIUS + 8f)
+            .whenever(mockRes)
+            .getDimension(eq(android.R.dimen.system_app_widget_background_radius))
+        doReturn(LAUNCHER_RADIUS)
+            .whenever(mockRes)
+            .getDimension(eq(R.dimen.enforced_rounded_corner_max_radius))
+
+        assertEquals(LAUNCHER_RADIUS, RoundedCornerEnforcement.computeEnforcedRadius(mockContext))
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENFORCE_SYSTEM_RADIUS_FOR_APP_WIDGETS)
+    fun `Compute system radius ignoring launcher radius`() {
+        val mockContext = mock(Context::class.java)
+        val mockRes = mock(Resources::class.java)
+
+        doReturn(mockRes).whenever(mockContext).resources
+        val systemRadius = LAUNCHER_RADIUS + 8f
+        doReturn(systemRadius)
+            .whenever(mockRes)
+            .getDimension(eq(android.R.dimen.system_app_widget_background_radius))
+        doReturn(LAUNCHER_RADIUS)
+            .whenever(mockRes)
+            .getDimension(eq(R.dimen.enforced_rounded_corner_max_radius))
+
+        assertEquals(systemRadius, RoundedCornerEnforcement.computeEnforcedRadius(mockContext))
+    }
+
     companion object {
         const val WIDTH = 200
         const val HEIGHT = 200
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/custom/CustomAppWidgetProviderInfoTest.kt b/tests/multivalentTests/src/com/android/launcher3/widget/custom/CustomAppWidgetProviderInfoTest.kt
index 0a3035a..af2c378 100644
--- a/tests/multivalentTests/src/com/android/launcher3/widget/custom/CustomAppWidgetProviderInfoTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/custom/CustomAppWidgetProviderInfoTest.kt
@@ -17,7 +17,6 @@
 package com.android.launcher3.widget.custom
 
 import android.content.ComponentName
-import android.content.pm.PackageManager
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
 import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
@@ -25,7 +24,6 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.Mockito.mock
 
 @MediumTest
 @RunWith(AndroidJUnit4::class)
@@ -47,7 +45,7 @@
     @Test
     fun get_label() {
         underTest.label = "  TEST_LABEL"
-        assertEquals(LABEL_NAME, underTest.getLabel(mock(PackageManager::class.java)))
+        assertEquals(LABEL_NAME, underTest.getLabel())
     }
 
     companion object {
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/custom/CustomWidgetManagerTest.kt b/tests/multivalentTests/src/com/android/launcher3/widget/custom/CustomWidgetManagerTest.kt
index 4b5710d..1c25db9 100644
--- a/tests/multivalentTests/src/com/android/launcher3/widget/custom/CustomWidgetManagerTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/custom/CustomWidgetManagerTest.kt
@@ -23,19 +23,22 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.android.launcher3.util.DaggerSingletonTracker
 import com.android.launcher3.util.LauncherModelHelper.SandboxModelContext
 import com.android.launcher3.util.PluginManagerWrapper
+import com.android.launcher3.util.SafeCloseable
 import com.android.launcher3.util.WidgetUtils
 import com.android.launcher3.widget.LauncherAppWidgetHostView
 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo
 import com.android.systemui.plugins.CustomWidgetPlugin
-import org.junit.After
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertTrue
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
 import org.mockito.Mock
 import org.mockito.Mockito.mock
 import org.mockito.MockitoAnnotations
@@ -57,17 +60,14 @@
 
     @Mock private lateinit var pluginManager: PluginManagerWrapper
     @Mock private lateinit var mockAppWidgetManager: AppWidgetManager
+    @Mock private lateinit var tracker: DaggerSingletonTracker
+
+    @Captor private lateinit var closableCaptor: ArgumentCaptor<SafeCloseable>
 
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
-        context.putObject(PluginManagerWrapper.INSTANCE, pluginManager)
-        underTest = CustomWidgetManager(context, mockAppWidgetManager)
-    }
-
-    @After
-    fun tearDown() {
-        underTest.close()
+        underTest = CustomWidgetManager(context, pluginManager, mockAppWidgetManager, tracker)
     }
 
     @Test
@@ -78,7 +78,8 @@
 
     @Test
     fun close_widget_manager_should_remove_plugin_listener() {
-        underTest.close()
+        verify(tracker).addCloseable(closableCaptor.capture())
+        closableCaptor.allValues.forEach(SafeCloseable::close)
         verify(pluginManager).removePluginListener(same(underTest))
     }
 
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/model/WidgetsListBaseEntriesBuilderTest.kt b/tests/multivalentTests/src/com/android/launcher3/widget/model/WidgetsListBaseEntriesBuilderTest.kt
index 5df7caa..063ab32 100644
--- a/tests/multivalentTests/src/com/android/launcher3/widget/model/WidgetsListBaseEntriesBuilderTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/model/WidgetsListBaseEntriesBuilderTest.kt
@@ -26,8 +26,8 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.android.launcher3.InvariantDeviceProfile
 import com.android.launcher3.LauncherAppState
-import com.android.launcher3.icons.ComponentWithLabel
 import com.android.launcher3.icons.IconCache
+import com.android.launcher3.icons.cache.CachedObject
 import com.android.launcher3.model.WidgetItem
 import com.android.launcher3.model.data.PackageItemInfo
 import com.android.launcher3.util.ActivityContextWrapper
@@ -66,11 +66,11 @@
         testInvariantProfile = LauncherAppState.getIDP(context)
 
         doAnswer { invocation: InvocationOnMock ->
-                val componentWithLabel = invocation.getArgument<Any>(0) as ComponentWithLabel
+                val componentWithLabel = invocation.getArgument<Any>(0) as CachedObject
                 componentWithLabel.getComponent().shortClassName
             }
             .`when`(iconCache)
-            .getTitleNoCache(any<ComponentWithLabel>())
+            .getTitleNoCache(any<CachedObject>())
         underTest = WidgetsListBaseEntriesBuilder(context)
 
         allWidgets =
@@ -79,14 +79,14 @@
                 packageItemInfoWithTitle(APP_1_PACKAGE_NAME, APP_1_PACKAGE_TITLE) to
                     listOf(
                         createWidgetItem(APP_1_PACKAGE_NAME, APP_1_PROVIDER_1_CLASS_NAME),
-                        createWidgetItem(APP_1_PACKAGE_NAME, APP_1_PROVIDER_2_CLASS_NAME)
+                        createWidgetItem(APP_1_PACKAGE_NAME, APP_1_PROVIDER_2_CLASS_NAME),
                     ),
                 // app 2
                 packageItemInfoWithTitle(APP_2_PACKAGE_NAME, APP_2_PACKAGE_TITLE) to
                     listOf(createWidgetItem(APP_2_PACKAGE_NAME, APP_2_PROVIDER_1_CLASS_NAME)),
                 // app 3
                 packageItemInfoWithTitle(APP_3_PACKAGE_NAME, APP_3_PACKAGE_TITLE) to
-                    listOf(createWidgetItem(APP_3_PACKAGE_NAME, APP_3_PROVIDER_1_CLASS_NAME))
+                    listOf(createWidgetItem(APP_3_PACKAGE_NAME, APP_3_PROVIDER_1_CLASS_NAME)),
             )
     }
 
@@ -96,7 +96,7 @@
             listOf(
                 APP_1_EXPECTED_SECTION_NAME to 2,
                 APP_2_EXPECTED_SECTION_NAME to 1,
-                APP_3_EXPECTED_SECTION_NAME to 1
+                APP_3_EXPECTED_SECTION_NAME to 1,
             )
 
         val entries = underTest.build(allWidgets)
@@ -122,7 +122,7 @@
         val expectedWidgetsCountBySection =
             listOf(
                 APP_1_EXPECTED_SECTION_NAME to 1, // one widget filtered out
-                APP_3_EXPECTED_SECTION_NAME to 1
+                APP_3_EXPECTED_SECTION_NAME to 1,
             )
 
         val entries =
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/picker/WidgetRecommendationCategoryProviderTest.java b/tests/multivalentTests/src/com/android/launcher3/widget/picker/WidgetRecommendationCategoryProviderTest.java
index 3024d26..8b6553f 100644
--- a/tests/multivalentTests/src/com/android/launcher3/widget/picker/WidgetRecommendationCategoryProviderTest.java
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/picker/WidgetRecommendationCategoryProviderTest.java
@@ -30,14 +30,16 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.when;
 
 import android.appwidget.AppWidgetProviderInfo;
 import android.content.ComponentName;
 import android.content.Context;
-import android.content.ContextWrapper;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.LauncherApps;
 import android.os.Process;
@@ -102,13 +104,8 @@
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
-        mContext = new ContextWrapper(getInstrumentation().getTargetContext()) {
-            @Override
-            public Object getSystemService(String name) {
-                return LAUNCHER_APPS_SERVICE.equals(name) ? mLauncherApps : super.getSystemService(
-                        name);
-            }
-        };
+        mContext = spy(getInstrumentation().getTargetContext());
+        doReturn(mLauncherApps).when(mContext).getSystemService(LauncherApps.class);
         mTestAppInfo.flags = FLAG_INSTALLED;
         mTestProfile = new InvariantDeviceProfile();
         mTestProfile.numRows = 5;
@@ -132,7 +129,7 @@
 
             mTestAppInfo.category = testCategory.getKey();
             when(mLauncherApps.getApplicationInfo(/*packageName=*/ eq(TEST_PACKAGE),
-                    /*flags=*/ eq(0),
+                    /*flags=*/ anyInt(),
                     /*user=*/ eq(Process.myUserHandle())))
                     .thenReturn(mTestAppInfo);
 
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinderTest.java b/tests/multivalentTests/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinderTest.java
index d4e061a..c9b6d4f 100644
--- a/tests/multivalentTests/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinderTest.java
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinderTest.java
@@ -42,8 +42,8 @@
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.R;
 import com.android.launcher3.icons.BitmapInfo;
-import com.android.launcher3.icons.ComponentWithLabel;
 import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.icons.cache.CachedObject;
 import com.android.launcher3.model.WidgetItem;
 import com.android.launcher3.model.data.PackageItemInfo;
 import com.android.launcher3.util.ActivityContextWrapper;
@@ -87,7 +87,7 @@
         mTestProfile.numColumns = 5;
 
         doAnswer(invocation -> {
-            ComponentWithLabel componentWithLabel = (ComponentWithLabel) invocation.getArgument(0);
+            CachedObject componentWithLabel = invocation.getArgument(0);
             return componentWithLabel.getComponent().getShortClassName();
         }).when(mIconCache).getTitleNoCache(any());
         mViewHolderBinder = new WidgetsListHeaderViewHolderBinder(
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinderTest.java b/tests/multivalentTests/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinderTest.java
index e1cc010..0d9464a 100644
--- a/tests/multivalentTests/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinderTest.java
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinderTest.java
@@ -45,8 +45,8 @@
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.R;
 import com.android.launcher3.icons.BitmapInfo;
-import com.android.launcher3.icons.ComponentWithLabel;
 import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.icons.cache.CachedObject;
 import com.android.launcher3.model.WidgetItem;
 import com.android.launcher3.model.data.PackageItemInfo;
 import com.android.launcher3.util.ActivityContextWrapper;
@@ -92,7 +92,7 @@
         mTestProfile.numColumns = 5;
 
         doAnswer(invocation -> {
-            ComponentWithLabel componentWithLabel = (ComponentWithLabel) invocation.getArgument(0);
+            CachedObject componentWithLabel = invocation.getArgument(0);
             return componentWithLabel.getComponent().getShortClassName();
         }).when(mIconCache).getTitleNoCache(any());
 
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/picker/model/WidgetPickerDataProviderTest.kt b/tests/multivalentTests/src/com/android/launcher3/widget/picker/model/WidgetPickerDataProviderTest.kt
index 1822639..1da74cb 100644
--- a/tests/multivalentTests/src/com/android/launcher3/widget/picker/model/WidgetPickerDataProviderTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/picker/model/WidgetPickerDataProviderTest.kt
@@ -27,8 +27,8 @@
 import com.android.launcher3.InvariantDeviceProfile
 import com.android.launcher3.LauncherAppState
 import com.android.launcher3.LauncherSettings
-import com.android.launcher3.icons.ComponentWithLabel
 import com.android.launcher3.icons.IconCache
+import com.android.launcher3.icons.cache.CachedObject
 import com.android.launcher3.model.WidgetItem
 import com.android.launcher3.model.data.PackageItemInfo
 import com.android.launcher3.util.ActivityContextWrapper
@@ -81,11 +81,11 @@
         testInvariantProfile = LauncherAppState.getIDP(context)
 
         doAnswer { invocation: InvocationOnMock ->
-                val componentWithLabel = invocation.getArgument<Any>(0) as ComponentWithLabel
+                val componentWithLabel = invocation.getArgument<Any>(0) as CachedObject
                 componentWithLabel.getComponent().shortClassName
             }
             .`when`(iconCache)
-            .getTitleNoCache(any<ComponentWithLabel>())
+            .getTitleNoCache(any<CachedObject>())
 
         appWidgetItem = createWidgetItem()
     }
@@ -113,8 +113,8 @@
             listOf(
                 PendingAddWidgetInfo(
                     appWidgetItem.widgetInfo,
-                    LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION
-                ),
+                    LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION,
+                )
             )
         underTest.setWidgetRecommendations(recommendations)
 
@@ -133,8 +133,8 @@
             listOf(
                 PendingAddWidgetInfo(
                     appWidgetItem.widgetInfo,
-                    LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION
-                ),
+                    LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION,
+                )
             )
         underTest.setWidgetRecommendations(recommendations)
 
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/picker/model/WidgetsListContentEntryTest.java b/tests/multivalentTests/src/com/android/launcher3/widget/picker/model/WidgetsListContentEntryTest.java
index 7552619..6088c8e 100644
--- a/tests/multivalentTests/src/com/android/launcher3/widget/picker/model/WidgetsListContentEntryTest.java
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/picker/model/WidgetsListContentEntryTest.java
@@ -33,8 +33,8 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.icons.ComponentWithLabel;
 import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.icons.cache.CachedObject;
 import com.android.launcher3.model.WidgetItem;
 import com.android.launcher3.model.data.PackageItemInfo;
 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
@@ -81,7 +81,7 @@
         mTestProfile.numColumns = 5;
 
         doAnswer(invocation -> {
-            ComponentWithLabel componentWithLabel = (ComponentWithLabel) invocation.getArgument(0);
+            CachedObject componentWithLabel = invocation.getArgument(0);
             return mWidgetsToLabels.get(componentWithLabel.getComponent());
         }).when(mIconCache).getTitleNoCache(any());
     }
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/picker/model/data/WidgetPickerDataTest.kt b/tests/multivalentTests/src/com/android/launcher3/widget/picker/model/data/WidgetPickerDataTest.kt
index e59e211..deec67a 100644
--- a/tests/multivalentTests/src/com/android/launcher3/widget/picker/model/data/WidgetPickerDataTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/picker/model/data/WidgetPickerDataTest.kt
@@ -27,8 +27,8 @@
 import com.android.launcher3.InvariantDeviceProfile
 import com.android.launcher3.LauncherAppState
 import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION
-import com.android.launcher3.icons.ComponentWithLabel
 import com.android.launcher3.icons.IconCache
+import com.android.launcher3.icons.cache.CachedObject
 import com.android.launcher3.model.WidgetItem
 import com.android.launcher3.model.data.ItemInfo
 import com.android.launcher3.model.data.PackageItemInfo
@@ -86,11 +86,11 @@
         testInvariantProfile = LauncherAppState.getIDP(context)
 
         doAnswer { invocation: InvocationOnMock ->
-                val componentWithLabel = invocation.getArgument<Any>(0) as ComponentWithLabel
+                val componentWithLabel = invocation.getArgument<Any>(0) as CachedObject
                 componentWithLabel.getComponent().shortClassName
             }
             .`when`(iconCache)
-            .getTitleNoCache(any<ComponentWithLabel>())
+            .getTitleNoCache(any<CachedObject>())
 
         app1PackageItemInfo = packageItemInfoWithTitle(APP_1_PACKAGE_NAME, APP_1_PACKAGE_TITLE)
         app2PackageItemInfo = packageItemInfoWithTitle(APP_2_PACKAGE_NAME, APP_2_PACKAGE_TITLE)
@@ -123,7 +123,7 @@
         val widgetPickerData =
             WidgetPickerData(
                 allWidgets = appTwoWidgetsListBaseEntries(),
-                defaultWidgets = appTwoWidgetsListBaseEntries()
+                defaultWidgets = appTwoWidgetsListBaseEntries(),
             )
 
         val newWidgetData =
@@ -143,19 +143,19 @@
                         addAll(appOneWidgetsListBaseEntries())
                         addAll(appTwoWidgetsListBaseEntries())
                     },
-                defaultWidgets = buildList { appTwoWidgetsListBaseEntries() }
+                defaultWidgets = buildList { appTwoWidgetsListBaseEntries() },
             )
         val recommendations: List<ItemInfo> =
             listOf(
                 PendingAddWidgetInfo(
                     app1WidgetItem1.widgetInfo,
                     CONTAINER_WIDGETS_PREDICTION,
-                    CATEGORY_1
+                    CATEGORY_1,
                 ),
                 PendingAddWidgetInfo(
                     app2WidgetItem1.widgetInfo,
                     CONTAINER_WIDGETS_PREDICTION,
-                    CATEGORY_2
+                    CATEGORY_2,
                 ),
             )
 
@@ -175,7 +175,7 @@
                         addAll(appOneWidgetsListBaseEntries())
                         addAll(appTwoWidgetsListBaseEntries())
                     },
-                defaultWidgets = buildList { appTwoWidgetsListBaseEntries() }
+                defaultWidgets = buildList { appTwoWidgetsListBaseEntries() },
             )
         val recommendations: List<ItemInfo> =
             listOf(
@@ -201,7 +201,7 @@
                         addAll(appTwoWidgetsListBaseEntries())
                     },
                 defaultWidgets = buildList { appTwoWidgetsListBaseEntries() },
-                recommendations = mapOf(CATEGORY_1 to listOf(app1WidgetItem1))
+                recommendations = mapOf(CATEGORY_1 to listOf(app1WidgetItem1)),
             )
 
         val updatedData = widgetPickerData.withRecommendedWidgets(listOf())
@@ -242,7 +242,7 @@
                         addAll(appOneWidgetsListBaseEntries())
                         addAll(appTwoWidgetsListBaseEntries())
                     },
-                defaultWidgets = buildList { addAll(appTwoWidgetsListBaseEntries()) }
+                defaultWidgets = buildList { addAll(appTwoWidgetsListBaseEntries()) },
             )
         val app1PackageUserKey = PackageUserKey.fromPackageItemInfo(app1PackageItemInfo)
 
@@ -263,7 +263,7 @@
                         addAll(appTwoWidgetsListBaseEntries())
                     },
                 defaultWidgets =
-                    buildList { addAll(appOneWidgetsListBaseEntries(includeWidgetTwo = false)) }
+                    buildList { addAll(appOneWidgetsListBaseEntries(includeWidgetTwo = false)) },
             )
         val app1PackageUserKey = PackageUserKey.fromPackageItemInfo(app1PackageItemInfo)
 
@@ -271,7 +271,7 @@
             findContentEntryForPackageUser(
                 widgetPickerData = widgetPickerData,
                 packageUserKey = app1PackageUserKey,
-                fromDefaultWidgets = true
+                fromDefaultWidgets = true,
             )
 
         assertThat(contentEntry).isNotNull()
@@ -302,7 +302,7 @@
                         addAll(appTwoWidgetsListBaseEntries())
                     },
                 defaultWidgets =
-                    buildList { addAll(appOneWidgetsListBaseEntries(includeWidgetTwo = false)) }
+                    buildList { addAll(appOneWidgetsListBaseEntries(includeWidgetTwo = false)) },
             )
 
         val widgets = findAllWidgetsForPackageUser(widgetPickerData, app1PackageUserKey)
@@ -314,9 +314,7 @@
     @Test
     fun findAllWidgetsForPackageUser_noMatch_returnsEmptyList() {
         val widgetPickerData =
-            WidgetPickerData(
-                allWidgets = buildList { addAll(appTwoWidgetsListBaseEntries()) },
-            )
+            WidgetPickerData(allWidgets = buildList { addAll(appTwoWidgetsListBaseEntries()) })
         val app1PackageUserKey = PackageUserKey.fromPackageItemInfo(app1PackageItemInfo)
 
         val widgets = findAllWidgetsForPackageUser(widgetPickerData, app1PackageUserKey)
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/picker/search/SimpleWidgetsSearchAlgorithmTest.java b/tests/multivalentTests/src/com/android/launcher3/widget/picker/search/SimpleWidgetsSearchAlgorithmTest.java
index 24d66a3..59f352b 100644
--- a/tests/multivalentTests/src/com/android/launcher3/widget/picker/search/SimpleWidgetsSearchAlgorithmTest.java
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/picker/search/SimpleWidgetsSearchAlgorithmTest.java
@@ -41,8 +41,8 @@
 
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.icons.BitmapInfo;
-import com.android.launcher3.icons.ComponentWithLabel;
 import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.icons.cache.CachedObject;
 import com.android.launcher3.model.WidgetItem;
 import com.android.launcher3.model.data.PackageItemInfo;
 import com.android.launcher3.search.SearchCallback;
@@ -87,7 +87,7 @@
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
         doAnswer(invocation -> {
-            ComponentWithLabel componentWithLabel = (ComponentWithLabel) invocation.getArgument(0);
+            CachedObject componentWithLabel = invocation.getArgument(0);
             return componentWithLabel.getComponent().getShortClassName();
         }).when(mIconCache).getTitleNoCache(any());
         mTestProfile = new InvariantDeviceProfile();
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/picker/util/WidgetsTableUtilsTest.java b/tests/multivalentTests/src/com/android/launcher3/widget/picker/util/WidgetsTableUtilsTest.java
index b2cb266..2f5fcfe 100644
--- a/tests/multivalentTests/src/com/android/launcher3/widget/picker/util/WidgetsTableUtilsTest.java
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/picker/util/WidgetsTableUtilsTest.java
@@ -28,7 +28,6 @@
 import android.appwidget.AppWidgetProviderInfo;
 import android.content.ComponentName;
 import android.content.Context;
-import android.content.pm.PackageManager;
 import android.graphics.Point;
 import android.graphics.drawable.Drawable;
 import android.os.UserHandle;
@@ -39,8 +38,9 @@
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.icons.ComponentWithLabel;
 import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.icons.cache.BaseIconCache;
+import com.android.launcher3.icons.cache.CachedObject;
 import com.android.launcher3.model.WidgetItem;
 import com.android.launcher3.pm.ShortcutConfigActivityInfo;
 import com.android.launcher3.util.ActivityContextWrapper;
@@ -99,7 +99,7 @@
         initTestWidgets();
         initTestShortcuts();
 
-        doAnswer(invocation -> ((ComponentWithLabel) invocation.getArgument(0))
+        doAnswer(invocation -> ((CachedObject) invocation.getArgument(0))
                 .getComponent().getPackageName())
                 .when(mIconCache).getTitleNoCache(any());
     }
@@ -280,32 +280,31 @@
     }
 
     private void initTestShortcuts() {
-        PackageManager packageManager = mContext.getPackageManager();
         mShortcut1 = new WidgetItem(new TestShortcutConfigActivityInfo(
                 ComponentName.createRelative(TEST_PACKAGE, ".shortcut1"), UserHandle.CURRENT),
-                mIconCache, packageManager);
+                mIconCache);
         mShortcut2 = new WidgetItem(new TestShortcutConfigActivityInfo(
                 ComponentName.createRelative(TEST_PACKAGE, ".shortcut2"), UserHandle.CURRENT),
-                mIconCache, packageManager);
+                mIconCache);
         mShortcut3 = new WidgetItem(new TestShortcutConfigActivityInfo(
                 ComponentName.createRelative(TEST_PACKAGE, ".shortcut3"), UserHandle.CURRENT),
-                mIconCache, packageManager);
+                mIconCache);
 
     }
 
     private final class TestShortcutConfigActivityInfo extends ShortcutConfigActivityInfo {
 
         TestShortcutConfigActivityInfo(ComponentName componentName, UserHandle user) {
-            super(componentName, user);
+            super(componentName, user, mContext);
         }
 
         @Override
-        public Drawable getFullResIcon(IconCache cache) {
+        public Drawable getFullResIcon(BaseIconCache cache) {
             return null;
         }
 
         @Override
-        public CharSequence getLabel(PackageManager pm) {
+        public CharSequence getLabel() {
             return null;
         }
     }
diff --git a/tests/res/drawable/test_app_info_icon.xml b/tests/res/drawable/test_app_info_icon.xml
new file mode 100644
index 0000000..2e824ac
--- /dev/null
+++ b/tests/res/drawable/test_app_info_icon.xml
@@ -0,0 +1,22 @@
+<?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.
+-->
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@android:color/white"/>
+    <foreground>
+        <color android:color="#FFFF0000" />
+    </foreground>
+</adaptive-icon>
diff --git a/tests/res/drawable/test_different_activity_icon.xml b/tests/res/drawable/test_different_activity_icon.xml
new file mode 100644
index 0000000..43d3611
--- /dev/null
+++ b/tests/res/drawable/test_different_activity_icon.xml
@@ -0,0 +1,22 @@
+<?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.
+-->
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@android:color/white"/>
+    <foreground>
+        <color android:color="#FFFFFF00" />
+    </foreground>
+</adaptive-icon>
diff --git a/tests/res/drawable/test_wrong_activity_icon.xml b/tests/res/drawable/test_wrong_activity_icon.xml
new file mode 100644
index 0000000..c3ae9f0
--- /dev/null
+++ b/tests/res/drawable/test_wrong_activity_icon.xml
@@ -0,0 +1,22 @@
+<?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.
+-->
+<wrong-adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@android:color/white"/>
+    <foreground>
+        <color android:color="#FFFF0000" />
+    </foreground>
+</wrong-adaptive-icon>
diff --git a/tests/src/com/android/launcher3/LauncherIntentTest.java b/tests/src/com/android/launcher3/LauncherIntentTest.java
index aeeb42a..a3d9614 100644
--- a/tests/src/com/android/launcher3/LauncherIntentTest.java
+++ b/tests/src/com/android/launcher3/LauncherIntentTest.java
@@ -23,21 +23,27 @@
 import android.platform.test.annotations.LargeTest;
 import android.view.KeyEvent;
 
-import androidx.test.runner.AndroidJUnit4;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 
 import com.android.launcher3.allapps.ActivityAllAppsContainerView;
 import com.android.launcher3.allapps.SearchRecyclerView;
-import com.android.launcher3.ui.AbstractLauncherUiTest;
+import com.android.launcher3.util.BaseLauncherActivityTest;
 
+import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
 @LargeTest
 @RunWith(AndroidJUnit4.class)
-public class LauncherIntentTest extends AbstractLauncherUiTest<Launcher> {
+public class LauncherIntentTest extends BaseLauncherActivityTest<Launcher> {
 
     public final Intent allAppsIntent = new Intent(Intent.ACTION_ALL_APPS);
 
+    @Before
+    public void setUp() {
+        loadLauncherSync();
+    }
+
     @Test
     public void testAllAppsIntent() {
         // Try executing ALL_APPS intent
@@ -45,7 +51,6 @@
         // A-Z view with Main adapter should be loaded
         assertOnMainAdapterAToZView();
 
-
         // Try Moving to search view now
         moveToSearchView();
         // Try executing ALL_APPS intent
@@ -63,12 +68,14 @@
         // Search view should be in focus
         waitForLauncherCondition("Search view is not in focus.",
                 launcher -> launcher.getAppsView().getSearchView().hasFocus());
-        mLauncher.pressAndHoldKeyCode(KeyEvent.KEYCODE_C, 0);
+
+        injectKeyEvent(KeyEvent.KEYCODE_C, true);
         // Upon key press, search recycler view should be loaded
         waitForLauncherCondition("Search view not active.",
                 launcher -> launcher.getAppsView().getActiveRecyclerView()
                         instanceof SearchRecyclerView);
-        mLauncher.unpressKeyCode(KeyEvent.KEYCODE_C, 0);
+
+        injectKeyEvent(KeyEvent.KEYCODE_C, false);
     }
 
     // Checks if main adapter view is selected, search bar is out of focus and scroller is at start.
diff --git a/tests/src/com/android/launcher3/allapps/KeyboardFocusTest.java b/tests/src/com/android/launcher3/allapps/KeyboardFocusTest.java
new file mode 100644
index 0000000..1e21ee5
--- /dev/null
+++ b/tests/src/com/android/launcher3/allapps/KeyboardFocusTest.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.allapps;
+
+import android.view.KeyEvent;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.util.BaseLauncherActivityTest;
+import com.android.launcher3.util.rule.ScreenRecordRule.ScreenRecord;
+import com.android.launcher3.views.ActivityContext;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class KeyboardFocusTest extends BaseLauncherActivityTest<Launcher> {
+
+    @Test
+    public void testAllAppsFocusApp() {
+        loadLauncherSync();
+        goToState(LauncherState.ALL_APPS);
+        freezeAllApps();
+
+        injectKeyEvent(KeyEvent.KEYCODE_DPAD_DOWN, true);
+        injectKeyEvent(KeyEvent.KEYCODE_DPAD_DOWN, false);
+        waitForLauncherCondition("No focused child", launcher ->
+                launcher.getAppsView().getActiveRecyclerView().getApps().getFocusedChild()
+                        != null);
+    }
+
+    @Test
+    public void testAllAppsExitSearchAndFocusApp() {
+        loadLauncherSync();
+        goToState(LauncherState.ALL_APPS);
+        freezeAllApps();
+
+        executeOnLauncher(launcher -> launcher.getAppsView().getSearchView().requestFocus());
+        waitForLauncherCondition("Search view does not have focus.",
+                launcher -> launcher.getAppsView().getSearchView().hasFocus());
+
+        injectKeyEvent(KeyEvent.KEYCODE_DPAD_DOWN, true);
+        injectKeyEvent(KeyEvent.KEYCODE_DPAD_DOWN, false);
+        waitForLauncherCondition("No focused child", launcher ->
+                launcher.getAppsView().getActiveRecyclerView().getApps().getFocusedChild()
+                        != null);
+    }
+
+    @Test
+    @ScreenRecord  //b/378167329
+    public void testAllAppsExitSearchAndFocusSearchResults() {
+        loadLauncherSync();
+        goToState(LauncherState.ALL_APPS);
+        freezeAllApps();
+
+        executeOnLauncher(launcher -> launcher.getAppsView().getSearchView().requestFocus());
+        waitForLauncherCondition("Search view does not have focus.",
+                launcher -> launcher.getAppsView().getSearchView().hasFocus());
+
+        injectKeyEvent(KeyEvent.KEYCODE_C, true);
+        waitForLauncherCondition("Search view not active.",
+                launcher -> launcher.getAppsView().getActiveRecyclerView()
+                        instanceof SearchRecyclerView);
+        injectKeyEvent(KeyEvent.KEYCODE_C, false);
+
+        executeOnLauncher(launcher -> launcher.getAppsView().getSearchUiManager().getEditText()
+                .hideKeyboard(/* clearFocus= */ false));
+        waitForLauncherCondition("Keyboard still visible.",
+                ActivityContext::isSoftwareKeyboardHidden);
+
+        injectKeyEvent(KeyEvent.KEYCODE_DPAD_DOWN, true);
+        injectKeyEvent(KeyEvent.KEYCODE_DPAD_DOWN, false);
+        waitForLauncherCondition("No focused child", launcher ->
+                launcher.getAppsView().getActiveRecyclerView().getApps().getFocusedChild()
+                        != null);
+    }
+}
diff --git a/tests/src/com/android/launcher3/allapps/TaplKeyboardFocusTest.java b/tests/src/com/android/launcher3/allapps/TaplKeyboardFocusTest.java
deleted file mode 100644
index 4e627a9..0000000
--- a/tests/src/com/android/launcher3/allapps/TaplKeyboardFocusTest.java
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.allapps;
-
-import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL;
-import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT;
-
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-
-import android.view.KeyEvent;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherState;
-import com.android.launcher3.tapl.HomeAllApps;
-import com.android.launcher3.ui.AbstractLauncherUiTest;
-import com.android.launcher3.util.rule.TestStabilityRule;
-import com.android.launcher3.views.ActivityContext;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class TaplKeyboardFocusTest extends AbstractLauncherUiTest<Launcher> {
-
-    @Test
-    public void testAllAppsFocusApp() {
-        final HomeAllApps allApps = mLauncher.goHome().switchToAllApps();
-        assertTrue("Launcher internal state is not All Apps",
-                isInState(() -> LauncherState.ALL_APPS));
-        allApps.freeze();
-        try {
-            mLauncher.pressAndHoldKeyCode(KeyEvent.KEYCODE_DPAD_DOWN, 0);
-            executeOnLauncher(launcher -> assertNotNull("No focused child.",
-                    launcher.getAppsView().getActiveRecyclerView().getApps().getFocusedChild()));
-        } finally {
-            allApps.unfreeze();
-        }
-    }
-
-    @Test
-    public void testAllAppsExitSearchAndFocusApp() {
-        final HomeAllApps allApps = mLauncher.goHome().switchToAllApps();
-        assertTrue("Launcher internal state is not All Apps",
-                isInState(() -> LauncherState.ALL_APPS));
-        allApps.freeze();
-        try {
-            executeOnLauncher(launcher -> launcher.getAppsView().getSearchView().requestFocus());
-            waitForLauncherCondition("Search view does not have focus.",
-                    launcher -> launcher.getAppsView().getSearchView().hasFocus());
-
-            mLauncher.pressAndHoldKeyCode(KeyEvent.KEYCODE_DPAD_DOWN, 0);
-            executeOnLauncher(launcher -> assertNotNull("No focused child.",
-                    launcher.getAppsView().getActiveRecyclerView().getApps().getFocusedChild()));
-        } finally {
-            allApps.unfreeze();
-        }
-    }
-
-    @Test
-    public void testAllAppsExitSearchAndFocusSearchResults() {
-        final HomeAllApps allApps = mLauncher.goHome().switchToAllApps();
-        assertTrue("Launcher internal state is not All Apps",
-                isInState(() -> LauncherState.ALL_APPS));
-        allApps.freeze();
-        try {
-            executeOnLauncher(launcher -> launcher.getAppsView().getSearchView().requestFocus());
-            waitForLauncherCondition("Search view does not have focus.",
-                    launcher -> launcher.getAppsView().getSearchView().hasFocus());
-
-            mLauncher.pressAndHoldKeyCode(KeyEvent.KEYCODE_C, 0);
-            waitForLauncherCondition("Search view not active.",
-                    launcher -> launcher.getAppsView().getActiveRecyclerView()
-                            instanceof SearchRecyclerView);
-            mLauncher.unpressKeyCode(KeyEvent.KEYCODE_C, 0);
-
-            executeOnLauncher(launcher -> launcher.getAppsView().getSearchUiManager().getEditText()
-                    .hideKeyboard(/* clearFocus= */ false));
-            waitForLauncherCondition("Keyboard still visible.",
-                    ActivityContext::isSoftwareKeyboardHidden);
-
-            mLauncher.pressAndHoldKeyCode(KeyEvent.KEYCODE_DPAD_DOWN, 0);
-            mLauncher.unpressKeyCode(KeyEvent.KEYCODE_DPAD_DOWN, 0);
-            waitForLauncherCondition("No focused child", launcher ->
-                    launcher.getAppsView().getActiveRecyclerView().getApps().getFocusedChild()
-                            != null);
-        } finally {
-            allApps.unfreeze();
-        }
-    }
-}
diff --git a/tests/src/com/android/launcher3/backuprestore/BackupAndRestoreDBSelectionTest.kt b/tests/src/com/android/launcher3/backuprestore/BackupAndRestoreDBSelectionTest.kt
index 479b201..b4ee090 100644
--- a/tests/src/com/android/launcher3/backuprestore/BackupAndRestoreDBSelectionTest.kt
+++ b/tests/src/com/android/launcher3/backuprestore/BackupAndRestoreDBSelectionTest.kt
@@ -16,6 +16,8 @@
 
 package com.android.launcher3.backuprestore
 
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
 import android.platform.test.flag.junit.SetFlagsRule
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
@@ -23,6 +25,7 @@
 import com.android.launcher3.Flags
 import com.android.launcher3.LauncherPrefs
 import com.android.launcher3.model.ModelDbController
+import com.android.launcher3.provider.RestoreDbTask
 import com.android.launcher3.util.Executors.MODEL_EXECUTOR
 import com.android.launcher3.util.TestUtil
 import com.android.launcher3.util.rule.BackAndRestoreRule
@@ -51,10 +54,24 @@
         setFlagsRule.setFlags(true, Flags.FLAG_ENABLE_NARROW_GRID_RESTORE)
     }
 
+    @EnableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR)
+    fun oldDatabasesNotPresentAfterRestoreRefactorFlagEnabled() {
+        oldDatabasesNotPresentAfterRestore()
+    }
+
+    @DisableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR)
+    fun oldDatabasesNotPresentAfterRestoreRefactorFlagDisabled() {
+        oldDatabasesNotPresentAfterRestore()
+    }
+
     @Test
     fun oldDatabasesNotPresentAfterRestore() {
         val dbController = ModelDbController(getInstrumentation().targetContext)
-        dbController.tryMigrateDB(null)
+        if (Flags.gridMigrationRefactor()) {
+            dbController.attemptMigrateDb(null)
+        } else {
+            dbController.tryMigrateDB(null)
+        }
         TestUtil.runOnExecutorSync(MODEL_EXECUTOR) {
             assert(backAndRestoreRule.getDatabaseFiles().size == 1) {
                 "There should only be one database after restoring, the last one used. Actual databases ${backAndRestoreRule.getDatabaseFiles()}"
@@ -67,4 +84,13 @@
             }
         }
     }
+
+    @Test
+    fun testExistingDbsAndRemovingDbs() {
+        var existingDbs = RestoreDbTask.existingDbs(getInstrumentation().targetContext)
+        assert(existingDbs.size == 4)
+        RestoreDbTask.removeOldDBs(getInstrumentation().targetContext, "launcher_4_by_4.db")
+        existingDbs = RestoreDbTask.existingDbs(getInstrumentation().targetContext)
+        assert(existingDbs.size == 1)
+    }
 }
diff --git a/tests/src/com/android/launcher3/compat/TaplPromiseIconUiTest.java b/tests/src/com/android/launcher3/compat/PromiseIconUiTest.java
similarity index 73%
rename from tests/src/com/android/launcher3/compat/TaplPromiseIconUiTest.java
rename to tests/src/com/android/launcher3/compat/PromiseIconUiTest.java
index 1500538..34b292c 100644
--- a/tests/src/com/android/launcher3/compat/TaplPromiseIconUiTest.java
+++ b/tests/src/com/android/launcher3/compat/PromiseIconUiTest.java
@@ -1,18 +1,19 @@
 /*
  * Copyright (C) 2019 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
+ * 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
+ *      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.
+ * 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.compat;
 
 import static com.android.launcher3.Flags.FLAG_ENABLE_SUPPORT_FOR_ARCHIVING;
@@ -27,32 +28,31 @@
 import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.text.TextUtils;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.LargeTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
-import com.android.launcher3.tapl.AllApps;
-import com.android.launcher3.ui.AbstractLauncherUiTest;
+import com.android.launcher3.util.BaseLauncherActivityTest;
 import com.android.launcher3.util.LauncherBindableItemsContainer.ItemOperator;
 import com.android.launcher3.util.TestUtil;
-import com.android.launcher3.util.rule.ViewCaptureRule;
 
 import org.junit.After;
+import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
 import java.io.IOException;
+import java.util.Arrays;
 import java.util.UUID;
 
-
 /**
  * Test to verify promise icon flow.
  */
 @LargeTest
 @RunWith(AndroidJUnit4.class)
-public class TaplPromiseIconUiTest extends AbstractLauncherUiTest<Launcher> {
+public class PromiseIconUiTest extends BaseLauncherActivityTest<Launcher> {
 
     @Rule
     public final CheckFlagsRule mCheckFlagsRule =
@@ -64,19 +64,17 @@
 
     private int mSessionId = -1;
 
-    @Override
+    @Before
     public void setUp() throws Exception {
-        super.setUp();
-        mDevice.pressHome();
-        waitForLauncherCondition("Launcher didn't start", launcher -> launcher != null);
-        waitForState("Launcher internal state didn't switch to Home", () -> LauncherState.NORMAL);
+        loadLauncherSync();
+        goToState(LauncherState.NORMAL);
         mSessionId = -1;
     }
 
     @After
     public void tearDown() throws IOException {
         if (mSessionId > -1) {
-            mTargetContext.getPackageManager().getPackageInstaller().abandonSession(mSessionId);
+            targetContext().getPackageManager().getPackageInstaller().abandonSession(mSessionId);
         }
         TestUtil.uninstallDummyApp();
     }
@@ -90,7 +88,7 @@
         params.setAppLabel(label);
         params.setAppIcon(icon);
         params.setInstallReason(PackageManager.INSTALL_REASON_USER);
-        return mTargetContext.getPackageManager().getPackageInstaller().createSession(params);
+        return targetContext().getPackageManager().getPackageInstaller().createSession(params);
     }
 
     @Test
@@ -108,7 +106,7 @@
                 launcher.getWorkspace().getFirstMatch(findPromiseApp) != null);
 
         // Remove session
-        mTargetContext.getPackageManager().getPackageInstaller().abandonSession(mSessionId);
+        targetContext().getPackageManager().getPackageInstaller().abandonSession(mSessionId);
         mSessionId = -1;
 
         // Verify promise icon is removed
@@ -117,7 +115,6 @@
     }
 
     @Test
-    @ViewCaptureRule.MayProduceNoFrames
     public void testPromiseIcon_notAddedFromIneligibleSession() throws Throwable {
         final String appLabel = "Test Promise App " + UUID.randomUUID().toString();
         final ItemOperator findPromiseApp = (info, view) ->
@@ -138,7 +135,8 @@
     @RequiresFlagsEnabled(FLAG_ENABLE_SUPPORT_FOR_ARCHIVING)
     public void testPromiseIcon_addedArchivedApp() throws Throwable {
         installDummyAppAndWaitForUIUpdate();
-        assertThat(mDevice.executeShellCommand(String.format("pm archive %s", DUMMY_PACKAGE)))
+        assertThat(executeShellCommand(
+                String.format("pm archive %s", DUMMY_PACKAGE)))
                 .isEqualTo("Success\n");
 
         // Create and add test session
@@ -148,28 +146,19 @@
         // Verify promise icon is added to all apps view. The icon may not be added to the
         // workspace even if there might be no icon present for archived app. But icon will
         // always be in all apps view. In case an icon is not added, an exception would be thrown.
-        final AllApps allApps = mLauncher.getWorkspace().switchToAllApps();
+        goToState(LauncherState.ALL_APPS);
 
         // Wait for the promise icon to be added.
         waitForLauncherCondition(
                 DUMMY_PACKAGE + " app was not found on all apps after being archived",
-                launcher -> {
-                    try {
-                        allApps.getAppIcon(DUMMY_LABEL);
-                    } catch (Throwable t) {
-                        return false;
-                    }
-                    return true;
-                });
-
-        // Remove session
-        mTargetContext.getPackageManager().getPackageInstaller().abandonSession(mSessionId);
-        mSessionId = -1;
+                launcher -> Arrays.stream(launcher.getAppsView().getAppsStore().getApps())
+                        .filter(info -> DUMMY_LABEL.equals(info.title.toString()))
+                        .findAny()
+                        .isPresent());
     }
 
     private void installDummyAppAndWaitForUIUpdate() throws IOException {
         TestUtil.installDummyApp();
-        mLauncher.waitForModelQueueCleared();
-        mLauncher.waitForLauncherInitialized();
+        loadLauncherSync();
     }
 }
diff --git a/tests/src/com/android/launcher3/dragging/TaplDragTest.java b/tests/src/com/android/launcher3/dragging/TaplDragTest.java
index 8fe77ac..e2f9feb9a 100644
--- a/tests/src/com/android/launcher3/dragging/TaplDragTest.java
+++ b/tests/src/com/android/launcher3/dragging/TaplDragTest.java
@@ -15,7 +15,6 @@
  */
 package com.android.launcher3.dragging;
 
-import static com.android.launcher3.testing.shared.TestProtocol.TEST_DRAG_APP_ICON_TO_MULTIPLE_WORKSPACES_FAILURE;
 import static com.android.launcher3.util.TestConstants.AppNames.GMAIL_APP_NAME;
 import static com.android.launcher3.util.TestConstants.AppNames.MAPS_APP_NAME;
 import static com.android.launcher3.util.TestConstants.AppNames.PHOTOS_APP_NAME;
@@ -65,7 +64,6 @@
     @Test
     @PortraitLandscape
     @PlatinumTest(focusArea = "launcher")
-    @ScreenRecordRule.ScreenRecord // b/353600888
     public void testDragToFolder() {
         // TODO: add the use case to drag an icon to an existing folder. Currently it either fails
         // on tablets or phones due to difference in resolution.
@@ -98,7 +96,6 @@
      * icon left.
      */
     @Test
-    @ScreenRecordRule.ScreenRecord // b/353600888
     public void testDragOutOfFolder() {
         final HomeAppIcon playStoreIcon = createShortcutIfNotExist(STORE_APP_NAME, 0, 1);
         final HomeAppIcon photosIcon = createShortcutInCenterIfNotExist(PHOTOS_APP_NAME);
@@ -229,11 +226,6 @@
         final HomeAppIcon launcherTestAppIcon = createShortcutInCenterIfNotExist(TEST_APP_NAME);
         for (Point target : targets) {
             startTime = SystemClock.uptimeMillis();
-            Log.d(TEST_DRAG_APP_ICON_TO_MULTIPLE_WORKSPACES_FAILURE,
-                    "TaplDragTest.java.testDragAppIconToMultipleWorkspaceCells: shortcut name: "
-                            + launcherTestAppIcon.getIconName()
-                            + " | target cell coordinates: (" + target.x + ", " + target.y
-                            + ") | start time: " + startTime);
             launcherTestAppIcon.dragToWorkspace(target.x, target.y);
             endTime = SystemClock.uptimeMillis();
             elapsedTime = endTime - startTime;
diff --git a/tests/src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java b/tests/src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java
index 7c87c65..1816030 100644
--- a/tests/src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java
+++ b/tests/src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java
@@ -16,14 +16,11 @@
 package com.android.launcher3.dragging;
 
 import static com.android.launcher3.testing.shared.TestProtocol.ICON_MISSING;
-import static com.android.launcher3.testing.shared.TestProtocol.UIOBJECT_STALE_ELEMENT;
 import static com.android.launcher3.util.TestConstants.AppNames.DUMMY_APP_NAME;
 import static com.android.launcher3.util.TestConstants.AppNames.GMAIL_APP_NAME;
 import static com.android.launcher3.util.TestConstants.AppNames.MAPS_APP_NAME;
 import static com.android.launcher3.util.TestConstants.AppNames.STORE_APP_NAME;
 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 com.google.common.truth.Truth.assertThat;
 
@@ -40,7 +37,6 @@
 import com.android.launcher3.util.TestUtil;
 import com.android.launcher3.util.Wait;
 import com.android.launcher3.util.rule.ScreenRecordRule;
-import com.android.launcher3.util.rule.TestStabilityRule;
 
 import org.junit.Test;
 
@@ -73,8 +69,7 @@
     private void verifyAppUninstalledFromAllApps(Workspace workspace, String appName) {
         final HomeAllApps allApps = workspace.switchToAllApps();
         Wait.atMost(appName + " app was found on all apps after being uninstalled",
-                () -> allApps.tryGetAppIcon(appName) == null,
-                DEFAULT_UI_TIMEOUT, mLauncher);
+                () -> allApps.tryGetAppIcon(appName) == null, mLauncher);
     }
 
     private void installDummyAppAndWaitForUIUpdate() throws IOException {
@@ -150,7 +145,6 @@
                     0, Math.min(gridPositions.length, appNameCandidates.length));
 
             for (int i = 0; i < appNames.length; ++i) {
-                Log.d(UIOBJECT_STALE_ELEMENT, "creatingShortcut for: " + appNames[i]);
                 createShortcutIfNotExist(appNames[i], gridPositions[i]);
             }
 
diff --git a/tests/src/com/android/launcher3/icons/IconProviderTest.kt b/tests/src/com/android/launcher3/icons/IconProviderTest.kt
new file mode 100644
index 0000000..5517fce
--- /dev/null
+++ b/tests/src/com/android/launcher3/icons/IconProviderTest.kt
@@ -0,0 +1,207 @@
+/*
+ * 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.icons
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.pm.ActivityInfo
+import android.content.pm.ApplicationInfo
+import android.content.pm.LauncherActivityInfo
+import android.content.pm.LauncherApps
+import android.content.pm.PackageItemInfo
+import android.content.pm.PackageManager
+import android.graphics.drawable.AdaptiveIconDrawable
+import android.graphics.drawable.Drawable
+import android.os.Parcel
+import android.os.Parcelable.Creator
+import android.os.Process
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.launcher3.model.data.AppInfo
+import com.android.launcher3.widget.WidgetManagerHelper
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** Tests for IconProvider */
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class IconProviderTest {
+
+    lateinit var context: Context
+    lateinit var pm: PackageManager
+    lateinit var iconProvider: IconProvider
+
+    lateinit var testContext: Context
+
+    @Before
+    fun setup() {
+        context = InstrumentationRegistry.getInstrumentation().targetContext
+        pm = context.packageManager
+        iconProvider = IconProvider(context)
+
+        testContext = InstrumentationRegistry.getInstrumentation().context
+    }
+
+    @Test
+    fun launcherActivityInfo_activity_icon() {
+        val icon = iconProvider.getIcon(getLauncherActivityInfo(DiffIconActivity).activityInfo)
+        assertNotNull(icon)
+        verifyIconResName(icon, ICON_DIFFERENT_ACTIVITY)
+    }
+
+    @Test
+    fun packageActivityInfo_activity_icon() {
+        val icon = iconProvider.getIcon(getPackageActivityInfo(DiffIconActivity))
+        assertNotNull(icon)
+        verifyIconResName(icon, ICON_DIFFERENT_ACTIVITY)
+    }
+
+    @Test
+    fun launcherActivityInfo_wrong_icon() {
+        val ai =
+            getLauncherActivityInfo(WrongIconActivity)
+                .activityInfo
+                .overrideAppIcon(ActivityInfo.CREATOR)
+        assertEquals(ai.icon.toResourceName(), ICON_WRONG_DRAWABLE)
+        val icon = iconProvider.getIcon(ai)
+        assertNotNull(icon)
+        // App icon is loaded if the drawable is not found
+        verifyIconResName(icon, ICON_APP_INFO)
+    }
+
+    @Test
+    fun packageActivityInfo_wrong_icon() {
+        val ai = getPackageActivityInfo(WrongIconActivity)
+        assertEquals(ai.icon.toResourceName(), ICON_WRONG_DRAWABLE)
+        assertNotEquals(ai.icon, 0)
+        val icon = iconProvider.getIcon(ai)
+        assertNotNull(icon)
+        // App icon is loaded if the drawable is not found
+        verifyIconResName(icon, ICON_APP_INFO)
+    }
+
+    @Test
+    fun launcherActivityInfo_fallback_to_icon() {
+        val ai =
+            getLauncherActivityInfo(AppIconActivity)
+                .activityInfo
+                .overrideAppIcon(ActivityInfo.CREATOR)
+        assertEquals(ai.icon, 0)
+        val icon = iconProvider.getIcon(ai)
+        assertNotNull(icon)
+        // App icon is loaded if component icon is not defined
+        verifyIconResName(icon, ICON_APP_INFO)
+    }
+
+    @Test
+    fun packageActivityInfo_fallback_to_icon() {
+        val ai = getPackageActivityInfo(AppIconActivity)
+        assertEquals(ai.icon, 0)
+        val icon = iconProvider.getIcon(ai)
+        assertNotNull(icon)
+        // App icon is loaded if component icon is not defined
+        verifyIconResName(icon, ICON_APP_INFO)
+    }
+
+    @Test
+    fun applicationInfo_icon() {
+        val appInfo =
+            getLauncherActivityInfo(AppIconActivity)
+                .applicationInfo
+                .overrideAppIcon(ApplicationInfo.CREATOR)
+        val icon = iconProvider.getIcon(appInfo)
+        assertNotNull(icon)
+        verifyIconResName(icon, ICON_APP_INFO)
+    }
+
+    @Test
+    fun applicationInfo_wrong_icon() {
+        val appInfo =
+            getLauncherActivityInfo(AppIconActivity)
+                .applicationInfo
+                .overrideAppIcon(ApplicationInfo.CREATOR)
+        appInfo.icon = 0
+
+        val icon = iconProvider.getIcon(appInfo)
+        assertNotNull(icon)
+        // Fallback is loaded if the drawable is defined
+        assertTrue(pm.isDefaultApplicationIcon(icon))
+    }
+
+    @Test
+    fun appwidgetProviderInfo_icon() {
+        val widgetInfo =
+            WidgetManagerHelper(context)
+                .findProvider(ComponentName(testContext, AppWidgetNoConfig), Process.myUserHandle())
+        assertNotNull(widgetInfo)
+
+        val icon = iconProvider.getIcon(widgetInfo.activityInfo)
+        assertNotNull(icon)
+        verifyIconResName(icon, ICON_WIDGET_NO_CONFIG)
+    }
+
+    private fun verifyIconResName(icon: Drawable, resName: String) {
+        assertTrue(icon is AdaptiveIconDrawable)
+        assertEquals(resName, (icon as AdaptiveIconDrawable).sourceDrawableResId.toResourceName())
+    }
+
+    private fun Int.toResourceName() = testContext.resources.getResourceEntryName(this)
+
+    private fun getLauncherActivityInfo(className: String): LauncherActivityInfo =
+        context
+            .getSystemService(LauncherApps::class.java)!!
+            .resolveActivity(getActivityIntent(className), Process.myUserHandle())
+
+    private fun getPackageActivityInfo(className: String): ActivityInfo =
+        pm.resolveActivity(getActivityIntent(className), 0)!!
+            .activityInfo
+            .overrideAppIcon(ActivityInfo.CREATOR)
+
+    private fun <T : PackageItemInfo> PackageItemInfo.overrideAppIcon(creator: Creator<T>): T {
+        // Clone the obj since it may have been cached by the system
+        val p = Parcel.obtain()
+        writeToParcel(p, 0)
+        p.setDataPosition(0)
+        val result = creator.createFromParcel(p)
+        p.recycle()
+        result.applicationInfo.icon =
+            testContext.resources.getIdentifier(ICON_APP_INFO, "drawable", testContext.packageName)
+        return result
+    }
+
+    private fun getActivityIntent(className: String) =
+        AppInfo.makeLaunchIntent(ComponentName(testContext, className))
+
+    companion object {
+        private const val AppIconActivity = "com.android.launcher3.tests.AppIconActivity"
+        private const val DiffIconActivity = "com.android.launcher3.tests.DiffIconActivity"
+        private const val WrongIconActivity = "com.android.launcher3.tests.WrongIconActivity"
+        private const val AppWidgetNoConfig =
+            "com.android.launcher3.testcomponent.AppWidgetNoConfig"
+
+        private const val ICON_DIFFERENT_ACTIVITY = "test_different_activity_icon"
+        private const val ICON_APP_INFO = "test_app_info_icon"
+        private const val ICON_WRONG_DRAWABLE = "test_wrong_activity_icon"
+        private const val ICON_WIDGET_NO_CONFIG = "test_widget_no_config_icon"
+    }
+}
diff --git a/tests/src/com/android/launcher3/model/GridMigrationTest.kt b/tests/src/com/android/launcher3/model/GridMigrationTest.kt
index 15222a4..379e98d 100644
--- a/tests/src/com/android/launcher3/model/GridMigrationTest.kt
+++ b/tests/src/com/android/launcher3/model/GridMigrationTest.kt
@@ -52,11 +52,15 @@
             phoneContext,
             dbFileName,
             { UserCache.INSTANCE.get(phoneContext).getSerialNumberForUser(it) },
-            {}
+            {},
         )
 
-    fun readEntries(): List<GridSizeMigrationUtil.DbEntry> =
-        GridSizeMigrationUtil.readAllEntries(dbHelper.readableDatabase, TABLE_NAME, phoneContext)
+    fun readEntries(): List<DbEntry> =
+        GridSizeMigrationDBController.readAllEntries(
+            dbHelper.readableDatabase,
+            TABLE_NAME,
+            phoneContext,
+        )
 }
 
 /**
@@ -80,7 +84,7 @@
         TestToPhoneFileCopier(
             src = "databases/GridMigrationTest/$DB_FILE",
             dest = "databases/$DB_FILE",
-            removeOnFinish = true
+            removeOnFinish = true,
         )
 
     @Before
@@ -89,13 +93,26 @@
     }
 
     private fun migrate(src: GridMigrationData, dst: GridMigrationData) {
-        GridSizeMigrationUtil.migrateGridIfNeeded(
-            phoneContext,
-            src.gridState,
-            dst.gridState,
-            dst.dbHelper,
-            src.dbHelper.readableDatabase
-        )
+        if (Flags.gridMigrationRefactor()) {
+            val gridSizeMigrationLogic = GridSizeMigrationLogic()
+            gridSizeMigrationLogic.migrateGrid(
+                phoneContext,
+                src.gridState,
+                dst.gridState,
+                dst.dbHelper,
+                src.dbHelper.readableDatabase,
+                false,
+            )
+        } else {
+            GridSizeMigrationDBController.migrateGridIfNeeded(
+                phoneContext,
+                src.gridState,
+                dst.gridState,
+                dst.dbHelper,
+                src.dbHelper.readableDatabase,
+                false,
+            )
+        }
     }
 
     /**
@@ -115,10 +132,8 @@
     }
 
     private fun compare(dst: GridMigrationData, target: GridMigrationData) {
-        val sort = compareBy<GridSizeMigrationUtil.DbEntry>({ it.cellX }, { it.cellY })
-        val mapF = { it: GridSizeMigrationUtil.DbEntry ->
-            EntryData(it.cellX, it.cellY, it.spanX, it.spanY, it.rank)
-        }
+        val sort = compareBy<DbEntry>({ it.cellX }, { it.cellY })
+        val mapF = { it: DbEntry -> EntryData(it.cellX, it.cellY, it.spanX, it.spanY, it.rank) }
         val entriesDst = dst.readEntries().sortedWith(sort).map(mapF)
         val entriesTarget = target.readEntries().sortedWith(sort).map(mapF)
 
@@ -149,7 +164,7 @@
         TestToPhoneFileCopier(
             src = "databases/GridMigrationTest/result5x5to3x3.db",
             dest = "databases/result5x5to3x3.db",
-            removeOnFinish = true
+            removeOnFinish = true,
         )
 
     @Test
@@ -160,10 +175,10 @@
                 GridMigrationData(
                     null, // in memory db, to download a new db change null for the filename of the
                     // db name to store it. Do not use existing names.
-                    DeviceGridState(3, 3, 3, TYPE_PHONE, "")
+                    DeviceGridState(3, 3, 3, TYPE_PHONE, ""),
                 ),
             target =
-                GridMigrationData("result5x5to3x3.db", DeviceGridState(3, 3, 3, TYPE_PHONE, ""))
+                GridMigrationData("result5x5to3x3.db", DeviceGridState(3, 3, 3, TYPE_PHONE, "")),
         )
 
     @JvmField
@@ -172,7 +187,7 @@
         TestToPhoneFileCopier(
             src = "databases/GridMigrationTest/result5x5to4x7.db",
             dest = "databases/result5x5to4x7.db",
-            removeOnFinish = true
+            removeOnFinish = true,
         )
 
     @Test
@@ -183,10 +198,10 @@
                 GridMigrationData(
                     null, // in memory db, to download a new db change null for the filename of the
                     // db name to store it. Do not use existing names.
-                    DeviceGridState(4, 7, 4, TYPE_PHONE, "")
+                    DeviceGridState(4, 7, 4, TYPE_PHONE, ""),
                 ),
             target =
-                GridMigrationData("result5x5to4x7.db", DeviceGridState(4, 7, 4, TYPE_PHONE, ""))
+                GridMigrationData("result5x5to4x7.db", DeviceGridState(4, 7, 4, TYPE_PHONE, "")),
         )
 
     @JvmField
@@ -195,7 +210,7 @@
         TestToPhoneFileCopier(
             src = "databases/GridMigrationTest/result5x5to5x8.db",
             dest = "databases/result5x5to5x8.db",
-            removeOnFinish = true
+            removeOnFinish = true,
         )
 
     @Test
@@ -206,10 +221,10 @@
                 GridMigrationData(
                     null, // in memory db, to download a new db change null for the filename of the
                     // db name to store it. Do not use existing names.
-                    DeviceGridState(5, 8, 5, TYPE_PHONE, "")
+                    DeviceGridState(5, 8, 5, TYPE_PHONE, ""),
                 ),
             target =
-                GridMigrationData("result5x5to5x8.db", DeviceGridState(5, 8, 5, TYPE_PHONE, ""))
+                GridMigrationData("result5x5to5x8.db", DeviceGridState(5, 8, 5, TYPE_PHONE, "")),
         )
 
     @JvmField
@@ -218,7 +233,7 @@
         TestToPhoneFileCopier(
             src = "databases/GridMigrationTest/flagged_result5x5to5x8.db",
             dest = "databases/flagged_result5x5to5x8.db",
-            removeOnFinish = true
+            removeOnFinish = true,
         )
 
     @Test
@@ -230,13 +245,13 @@
                 GridMigrationData(
                     null, // in memory db, to download a new db change null for the filename of the
                     // db name to store it. Do not use existing names.
-                    DeviceGridState(5, 8, 5, TYPE_PHONE, "")
+                    DeviceGridState(5, 8, 5, TYPE_PHONE, ""),
                 ),
             target =
                 GridMigrationData(
                     "flagged_result5x5to5x8.db",
-                    DeviceGridState(5, 8, 5, TYPE_PHONE, "")
-                )
+                    DeviceGridState(5, 8, 5, TYPE_PHONE, ""),
+                ),
         )
     }
 }
diff --git a/tests/src/com/android/launcher3/model/LoaderTaskTest.kt b/tests/src/com/android/launcher3/model/LoaderTaskTest.kt
index d16674c..882061f 100644
--- a/tests/src/com/android/launcher3/model/LoaderTaskTest.kt
+++ b/tests/src/com/android/launcher3/model/LoaderTaskTest.kt
@@ -3,6 +3,8 @@
 import android.appwidget.AppWidgetManager
 import android.content.Intent
 import android.os.UserHandle
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
 import android.platform.test.flag.junit.SetFlagsRule
 import android.provider.Settings
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -22,9 +24,11 @@
 import com.android.launcher3.util.Executors.MODEL_EXECUTOR
 import com.android.launcher3.util.LauncherModelHelper.SandboxModelContext
 import com.android.launcher3.util.LooperIdleLock
+import com.android.launcher3.util.TestUtil
 import com.android.launcher3.util.UserIconInfo
 import com.google.common.truth.Truth
 import java.util.concurrent.CountDownLatch
+import java.util.function.Predicate
 import junit.framework.Assert.assertEquals
 import org.junit.After
 import org.junit.Before
@@ -32,17 +36,15 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentCaptor
-import org.mockito.ArgumentMatchers.any
-import org.mockito.ArgumentMatchers.anyInt
-import org.mockito.ArgumentMatchers.anyList
-import org.mockito.ArgumentMatchers.anyMap
 import org.mockito.Mock
 import org.mockito.Mockito
+import org.mockito.Mockito.mock
 import org.mockito.Mockito.times
 import org.mockito.Mockito.`when`
 import org.mockito.MockitoAnnotations
 import org.mockito.MockitoSession
 import org.mockito.Spy
+import org.mockito.kotlin.any
 import org.mockito.kotlin.anyOrNull
 import org.mockito.kotlin.doReturn
 import org.mockito.kotlin.spy
@@ -66,7 +68,7 @@
             installedHotseatItems = mutableSetOf("installedHotseatItem"),
             installedWorkspaceItems = mutableSetOf("installedWorkspaceItem"),
             firstScreenInstalledWidgets = mutableSetOf("installedFirstScreenWidget"),
-            secondaryScreenInstalledWidgets = mutableSetOf("installedSecondaryScreenWidget")
+            secondaryScreenInstalledWidgets = mutableSetOf("installedSecondaryScreenWidget"),
         )
     private lateinit var mockitoSession: MockitoSession
 
@@ -74,7 +76,8 @@
     @Mock private lateinit var bgAllAppsList: AllAppsList
     @Mock private lateinit var modelDelegate: ModelDelegate
     @Mock private lateinit var launcherBinder: BaseLauncherBinder
-    @Mock private lateinit var launcherModel: LauncherModel
+    private lateinit var launcherModel: LauncherModel
+    @Mock private lateinit var widgetsFilterDataProvider: WidgetsFilterDataProvider
     @Mock private lateinit var transaction: LoaderTransaction
     @Mock private lateinit var iconCache: IconCache
     @Mock private lateinit var idleLock: LooperIdleLock
@@ -88,6 +91,8 @@
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
+        setFlagsRule.enableFlags(Flags.FLAG_ENABLE_TIERED_WIDGETS_BY_DEFAULT_IN_PICKER)
+        launcherModel = mock(LauncherModel::class.java)
         mockitoSession =
             ExtendedMockito.mockitoSession()
                 .strictness(Strictness.LENIENT)
@@ -104,18 +109,22 @@
 
         doReturn(TestViewHelpers.findWidgetProvider(false))
             .`when`(context.spyService(AppWidgetManager::class.java))
-            .getAppWidgetInfo(anyInt())
+            .getAppWidgetInfo(any())
         `when`(app.context).thenReturn(context)
         `when`(app.model).thenReturn(launcherModel)
-        `when`(launcherModel.beginLoader(any(LoaderTask::class.java))).thenReturn(transaction)
+
+        `when`(launcherModel.beginLoader(any())).thenReturn(transaction)
         `when`(app.iconCache).thenReturn(iconCache)
         `when`(launcherModel.modelDbController)
             .thenReturn(FactitiousDbController(context, INSERTION_STATEMENT_FILE))
         `when`(app.invariantDeviceProfile).thenReturn(idp)
-        `when`(launcherBinder.newIdleLock(any(LoaderTask::class.java))).thenReturn(idleLock)
+        `when`(launcherBinder.newIdleLock(any())).thenReturn(idleLock)
         `when`(idleLock.awaitLocked(1000)).thenReturn(false)
         `when`(iconCache.updateHandler).thenReturn(iconCacheUpdateHandler)
+        `when`(widgetsFilterDataProvider.getDefaultWidgetsFilter()).thenReturn(Predicate { true })
         context.putObject(UserCache.INSTANCE, userCache)
+
+        TestUtil.grantWriteSecurePermission()
     }
 
     @After
@@ -131,27 +140,43 @@
             val mockUserHandles = arrayListOf<UserHandle>(MAIN_HANDLE)
             `when`(userCache.userProfiles).thenReturn(mockUserHandles)
             `when`(userCache.getUserInfo(MAIN_HANDLE)).thenReturn(UserIconInfo(MAIN_HANDLE, 1))
-            LoaderTask(app, bgAllAppsList, this, modelDelegate, launcherBinder)
+            LoaderTask(
+                    app,
+                    bgAllAppsList,
+                    this,
+                    modelDelegate,
+                    launcherBinder,
+                    widgetsFilterDataProvider,
+                )
                 .runSyncOnBackgroundThread()
             Truth.assertThat(workspaceItems.size).isAtLeast(25)
             Truth.assertThat(appWidgets.size).isAtLeast(7)
             Truth.assertThat(collections.size()).isAtLeast(8)
             Truth.assertThat(itemsIdMap.size()).isAtLeast(40)
+            Truth.assertThat(widgetsModel.defaultWidgetsFilter).isNotNull()
         }
 
     @Test
     fun bindsLoadedDataCorrectly() {
-        LoaderTask(app, bgAllAppsList, BgDataModel(), modelDelegate, launcherBinder)
+        LoaderTask(
+                app,
+                bgAllAppsList,
+                BgDataModel(),
+                modelDelegate,
+                launcherBinder,
+                widgetsFilterDataProvider,
+            )
             .runSyncOnBackgroundThread()
 
         verify(launcherBinder).bindWorkspace(true, false)
         verify(modelDelegate).workspaceLoadComplete()
-        verify(modelDelegate).loadAndBindAllAppsItems(any(), any(), any())
+        verify(modelDelegate).loadAndBindAllAppsItems(any(), anyOrNull(), any())
         verify(launcherBinder).bindAllApps()
         verify(iconCacheUpdateHandler, times(4)).updateIcons(any(), any<CachingLogic<Any>>(), any())
         verify(launcherBinder).bindDeepShortcuts()
+        verify(widgetsFilterDataProvider).initPeriodicDataRefresh(any())
         verify(launcherBinder).bindWidgets()
-        verify(modelDelegate).loadAndBindOtherItems(any())
+        verify(modelDelegate).loadAndBindOtherItems(anyOrNull())
         verify(iconCacheUpdateHandler).finish()
         verify(modelDelegate).modelLoadComplete()
         verify(transaction).commit()
@@ -167,7 +192,15 @@
             `when`(userManagerState?.isUserQuiet(MAIN_HANDLE)).thenReturn(true)
             `when`(userCache.getUserInfo(MAIN_HANDLE)).thenReturn(UserIconInfo(MAIN_HANDLE, 1))
 
-            LoaderTask(app, bgAllAppsList, this, modelDelegate, launcherBinder, userManagerState)
+            LoaderTask(
+                    app,
+                    bgAllAppsList,
+                    this,
+                    modelDelegate,
+                    launcherBinder,
+                    widgetsFilterDataProvider,
+                    userManagerState,
+                )
                 .runSyncOnBackgroundThread()
 
             verify(bgAllAppsList)
@@ -188,7 +221,15 @@
             `when`(userManagerState?.isUserQuiet(MAIN_HANDLE)).thenReturn(true)
             `when`(userCache.getUserInfo(MAIN_HANDLE)).thenReturn(UserIconInfo(MAIN_HANDLE, 3))
 
-            LoaderTask(app, bgAllAppsList, this, modelDelegate, launcherBinder, userManagerState)
+            LoaderTask(
+                    app,
+                    bgAllAppsList,
+                    this,
+                    modelDelegate,
+                    launcherBinder,
+                    widgetsFilterDataProvider,
+                    userManagerState,
+                )
                 .runSyncOnBackgroundThread()
 
             verify(bgAllAppsList)
@@ -200,16 +241,17 @@
         }
 
     @Test
-    fun `When launcher_broadcast_installed_apps and is restore then send installed item broadcast`() {
+    @DisableFlags(Flags.FLAG_ENABLE_FIRST_SCREEN_BROADCAST_ARCHIVING_EXTRAS)
+    fun `When secure setting true and is restore then send installed item broadcast`() {
         // Given
         val spyContext = spy(context)
         `when`(app.context).thenReturn(spyContext)
         whenever(
                 FirstScreenBroadcastHelper.createModelsForFirstScreenBroadcast(
-                    anyOrNull(),
-                    anyList(),
-                    anyMap(),
-                    anyList()
+                    any(),
+                    any(),
+                    any(),
+                    any(),
                 )
             )
             .thenReturn(listOf(expectedBroadcastModel))
@@ -217,7 +259,7 @@
         whenever(
                 FirstScreenBroadcastHelper.sendBroadcastsForModels(
                     spyContext,
-                    listOf(expectedBroadcastModel)
+                    listOf(expectedBroadcastModel),
                 )
             )
             .thenCallRealMethod()
@@ -226,7 +268,14 @@
         RestoreDbTask.setPending(spyContext)
 
         // When
-        LoaderTask(app, bgAllAppsList, BgDataModel(), modelDelegate, launcherBinder)
+        LoaderTask(
+                app,
+                bgAllAppsList,
+                BgDataModel(),
+                modelDelegate,
+                launcherBinder,
+                widgetsFilterDataProvider,
+            )
             .runSyncOnBackgroundThread()
 
         // Then
@@ -236,48 +285,49 @@
         assertEquals(expectedBroadcastModel.installerPackage, actualBroadcastIntent.`package`)
         assertEquals(
             ArrayList(expectedBroadcastModel.installedWorkspaceItems),
-            actualBroadcastIntent.getStringArrayListExtra("workspaceInstalledItems")
+            actualBroadcastIntent.getStringArrayListExtra("workspaceInstalledItems"),
         )
         assertEquals(
             ArrayList(expectedBroadcastModel.installedHotseatItems),
-            actualBroadcastIntent.getStringArrayListExtra("hotseatInstalledItems")
+            actualBroadcastIntent.getStringArrayListExtra("hotseatInstalledItems"),
         )
         assertEquals(
             ArrayList(
                 expectedBroadcastModel.firstScreenInstalledWidgets +
                     expectedBroadcastModel.secondaryScreenInstalledWidgets
             ),
-            actualBroadcastIntent.getStringArrayListExtra("widgetInstalledItems")
+            actualBroadcastIntent.getStringArrayListExtra("widgetInstalledItems"),
         )
         assertEquals(
             ArrayList(expectedBroadcastModel.pendingCollectionItems),
-            actualBroadcastIntent.getStringArrayListExtra("folderItem")
+            actualBroadcastIntent.getStringArrayListExtra("folderItem"),
         )
         assertEquals(
             ArrayList(expectedBroadcastModel.pendingWorkspaceItems),
-            actualBroadcastIntent.getStringArrayListExtra("workspaceItem")
+            actualBroadcastIntent.getStringArrayListExtra("workspaceItem"),
         )
         assertEquals(
             ArrayList(expectedBroadcastModel.pendingHotseatItems),
-            actualBroadcastIntent.getStringArrayListExtra("hotseatItem")
+            actualBroadcastIntent.getStringArrayListExtra("hotseatItem"),
         )
         assertEquals(
             ArrayList(expectedBroadcastModel.pendingWidgetItems),
-            actualBroadcastIntent.getStringArrayListExtra("widgetItem")
+            actualBroadcastIntent.getStringArrayListExtra("widgetItem"),
         )
     }
 
     @Test
-    fun `When not a restore then installed item broadcast not sent`() {
+    @EnableFlags(Flags.FLAG_ENABLE_FIRST_SCREEN_BROADCAST_ARCHIVING_EXTRAS)
+    fun `When broadcast flag true and is restore then send installed item broadcast`() {
         // Given
         val spyContext = spy(context)
         `when`(app.context).thenReturn(spyContext)
         whenever(
                 FirstScreenBroadcastHelper.createModelsForFirstScreenBroadcast(
-                    anyOrNull(),
-                    anyList(),
-                    anyMap(),
-                    anyList()
+                    any(),
+                    any(),
+                    any(),
+                    any(),
                 )
             )
             .thenReturn(listOf(expectedBroadcastModel))
@@ -285,40 +335,7 @@
         whenever(
                 FirstScreenBroadcastHelper.sendBroadcastsForModels(
                     spyContext,
-                    listOf(expectedBroadcastModel)
-                )
-            )
-            .thenCallRealMethod()
-
-        Settings.Secure.putInt(spyContext.contentResolver, "launcher_broadcast_installed_apps", 1)
-
-        // When
-        LoaderTask(app, bgAllAppsList, BgDataModel(), modelDelegate, launcherBinder)
-            .runSyncOnBackgroundThread()
-
-        // Then
-        verify(spyContext, times(0)).sendBroadcast(any(Intent::class.java))
-    }
-
-    @Test
-    fun `When launcher_broadcast_installed_apps false then installed item broadcast not sent`() {
-        // Given
-        val spyContext = spy(context)
-        `when`(app.context).thenReturn(spyContext)
-        whenever(
-                FirstScreenBroadcastHelper.createModelsForFirstScreenBroadcast(
-                    anyOrNull(),
-                    anyList(),
-                    anyMap(),
-                    anyList()
-                )
-            )
-            .thenReturn(listOf(expectedBroadcastModel))
-
-        whenever(
-                FirstScreenBroadcastHelper.sendBroadcastsForModels(
-                    spyContext,
-                    listOf(expectedBroadcastModel)
+                    listOf(expectedBroadcastModel),
                 )
             )
             .thenCallRealMethod()
@@ -327,11 +344,135 @@
         RestoreDbTask.setPending(spyContext)
 
         // When
-        LoaderTask(app, bgAllAppsList, BgDataModel(), modelDelegate, launcherBinder)
+        LoaderTask(
+                app,
+                bgAllAppsList,
+                BgDataModel(),
+                modelDelegate,
+                launcherBinder,
+                widgetsFilterDataProvider,
+            )
             .runSyncOnBackgroundThread()
 
         // Then
-        verify(spyContext, times(0)).sendBroadcast(any(Intent::class.java))
+        val argumentCaptor = ArgumentCaptor.forClass(Intent::class.java)
+        verify(spyContext).sendBroadcast(argumentCaptor.capture())
+        val actualBroadcastIntent = argumentCaptor.value
+        assertEquals(expectedBroadcastModel.installerPackage, actualBroadcastIntent.`package`)
+        assertEquals(
+            ArrayList(expectedBroadcastModel.installedWorkspaceItems),
+            actualBroadcastIntent.getStringArrayListExtra("workspaceInstalledItems"),
+        )
+        assertEquals(
+            ArrayList(expectedBroadcastModel.installedHotseatItems),
+            actualBroadcastIntent.getStringArrayListExtra("hotseatInstalledItems"),
+        )
+        assertEquals(
+            ArrayList(
+                expectedBroadcastModel.firstScreenInstalledWidgets +
+                    expectedBroadcastModel.secondaryScreenInstalledWidgets
+            ),
+            actualBroadcastIntent.getStringArrayListExtra("widgetInstalledItems"),
+        )
+        assertEquals(
+            ArrayList(expectedBroadcastModel.pendingCollectionItems),
+            actualBroadcastIntent.getStringArrayListExtra("folderItem"),
+        )
+        assertEquals(
+            ArrayList(expectedBroadcastModel.pendingWorkspaceItems),
+            actualBroadcastIntent.getStringArrayListExtra("workspaceItem"),
+        )
+        assertEquals(
+            ArrayList(expectedBroadcastModel.pendingHotseatItems),
+            actualBroadcastIntent.getStringArrayListExtra("hotseatItem"),
+        )
+        assertEquals(
+            ArrayList(expectedBroadcastModel.pendingWidgetItems),
+            actualBroadcastIntent.getStringArrayListExtra("widgetItem"),
+        )
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_FIRST_SCREEN_BROADCAST_ARCHIVING_EXTRAS)
+    fun `When not a restore then installed item broadcast not sent`() {
+        // Given
+        val spyContext = spy(context)
+        `when`(app.context).thenReturn(spyContext)
+        whenever(
+                FirstScreenBroadcastHelper.createModelsForFirstScreenBroadcast(
+                    any(),
+                    any(),
+                    any(),
+                    any(),
+                )
+            )
+            .thenReturn(listOf(expectedBroadcastModel))
+
+        whenever(
+                FirstScreenBroadcastHelper.sendBroadcastsForModels(
+                    spyContext,
+                    listOf(expectedBroadcastModel),
+                )
+            )
+            .thenCallRealMethod()
+
+        Settings.Secure.putInt(spyContext.contentResolver, "launcher_broadcast_installed_apps", 1)
+
+        // When
+        LoaderTask(
+                app,
+                bgAllAppsList,
+                BgDataModel(),
+                modelDelegate,
+                launcherBinder,
+                widgetsFilterDataProvider,
+            )
+            .runSyncOnBackgroundThread()
+
+        // Then
+        verify(spyContext, times(0)).sendBroadcast(any())
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_ENABLE_FIRST_SCREEN_BROADCAST_ARCHIVING_EXTRAS)
+    fun `When broadcast flag and secure setting false then installed item broadcast not sent`() {
+        // Given
+        val spyContext = spy(context)
+        `when`(app.context).thenReturn(spyContext)
+        whenever(
+                FirstScreenBroadcastHelper.createModelsForFirstScreenBroadcast(
+                    any(),
+                    any(),
+                    any(),
+                    any(),
+                )
+            )
+            .thenReturn(listOf(expectedBroadcastModel))
+
+        whenever(
+                FirstScreenBroadcastHelper.sendBroadcastsForModels(
+                    spyContext,
+                    listOf(expectedBroadcastModel),
+                )
+            )
+            .thenCallRealMethod()
+
+        Settings.Secure.putInt(spyContext.contentResolver, "launcher_broadcast_installed_apps", 0)
+        RestoreDbTask.setPending(spyContext)
+
+        // When
+        LoaderTask(
+                app,
+                bgAllAppsList,
+                BgDataModel(),
+                modelDelegate,
+                launcherBinder,
+                widgetsFilterDataProvider,
+            )
+            .runSyncOnBackgroundThread()
+
+        // Then
+        verify(spyContext, times(0)).sendBroadcast(any())
     }
 }
 
diff --git a/tests/src/com/android/launcher3/model/PackageUpdatedTaskTest.kt b/tests/src/com/android/launcher3/model/PackageUpdatedTaskTest.kt
index 05f626d..d9af07a 100644
--- a/tests/src/com/android/launcher3/model/PackageUpdatedTaskTest.kt
+++ b/tests/src/com/android/launcher3/model/PackageUpdatedTaskTest.kt
@@ -58,8 +58,7 @@
 @RunWith(AndroidJUnit4::class)
 class PackageUpdatedTaskTest {
 
-    @get:Rule(order = 0) val setFlagsRule = SetFlagsRule()
-    @get:Rule(order = 1) val modelTestRule = ModelTestRule()
+    @get:Rule val setFlagsRule = SetFlagsRule()
 
     private val mUser = UserHandle(0)
     private val mDataModel: BgDataModel = BgDataModel()
diff --git a/tests/src/com/android/launcher3/model/WorkspaceItemProcessorExtraTest.kt b/tests/src/com/android/launcher3/model/WorkspaceItemProcessorExtraTest.kt
index b93c305..d553f47 100644
--- a/tests/src/com/android/launcher3/model/WorkspaceItemProcessorExtraTest.kt
+++ b/tests/src/com/android/launcher3/model/WorkspaceItemProcessorExtraTest.kt
@@ -20,6 +20,7 @@
 import android.content.ComponentName
 import android.content.Context
 import android.content.Intent
+import android.content.pm.ApplicationInfo
 import android.content.pm.LauncherApps
 import android.content.pm.PackageInstaller
 import android.content.pm.ShortcutInfo
@@ -28,14 +29,13 @@
 import android.util.LongSparseArray
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.android.dx.mockito.inline.extended.ExtendedMockito
 import com.android.launcher3.Flags.FLAG_ENABLE_SUPPORT_FOR_ARCHIVING
 import com.android.launcher3.LauncherAppState
 import com.android.launcher3.LauncherSettings.Favorites
 import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP
 import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION
 import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET
-import com.android.launcher3.Utilities
+import com.android.launcher3.icons.CacheableShortcutInfo
 import com.android.launcher3.model.data.IconRequestInfo
 import com.android.launcher3.model.data.LauncherAppWidgetInfo
 import com.android.launcher3.model.data.LauncherAppWidgetInfo.FLAG_RESTORE_STARTED
@@ -48,7 +48,6 @@
 import com.android.launcher3.util.PackageUserKey
 import com.android.launcher3.util.UserIconInfo
 import com.android.launcher3.widget.WidgetInflater
-import com.android.launcher3.widget.WidgetSections
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
 import org.junit.Test
@@ -63,7 +62,6 @@
 import org.mockito.kotlin.eq
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.whenever
-import org.mockito.quality.Strictness
 
 @RunWith(AndroidJUnit4::class)
 class WorkspaceItemProcessorExtraTest {
@@ -87,7 +85,7 @@
     private var mUnlockedUsersArray: LongSparseArray<Boolean> = LongSparseArray()
     private var mKeyToPinnedShortcutsMap: MutableMap<ShortcutKey, ShortcutInfo> = mutableMapOf()
     private var mInstallingPkgs: HashMap<PackageUserKey, PackageInstaller.SessionInfo> = hashMapOf()
-    private var mAllDeepShortcuts: MutableList<ShortcutInfo> = mutableListOf()
+    private var mAllDeepShortcuts: MutableList<CacheableShortcutInfo> = mutableListOf()
     private var mWidgetProvidersMap: MutableMap<ComponentKey, AppWidgetProviderInfo?> =
         mutableMapOf()
     private var mPendingPackages: MutableSet<PackageUserKey> = mutableSetOf()
@@ -108,11 +106,17 @@
                 `package` = "pkg"
                 putExtra(ShortcutKey.EXTRA_SHORTCUT_ID, "")
             }
+        mockLauncherApps =
+            mock<LauncherApps>().apply {
+                whenever(isPackageEnabled("package", mUserHandle)).thenReturn(true)
+                whenever(isActivityEnabled(mComponentName, mUserHandle)).thenReturn(true)
+            }
         mockContext =
             mock<Context>().apply {
                 whenever(packageManager).thenReturn(mock())
                 whenever(packageManager.getUserBadgedLabel(any(), any())).thenReturn("")
                 whenever(applicationContext).thenReturn(ApplicationProvider.getApplicationContext())
+                whenever(getSystemService(LauncherApps::class.java)).thenReturn(mockLauncherApps)
             }
         mockAppState =
             mock<LauncherAppState>().apply {
@@ -125,11 +129,6 @@
                 whenever(getAppLaunchIntent(mComponentName.packageName, mUserHandle))
                     .thenReturn(intent)
             }
-        mockLauncherApps =
-            mock<LauncherApps>().apply {
-                whenever(isPackageEnabled("package", mUserHandle)).thenReturn(true)
-                whenever(isActivityEnabled(mComponentName, mUserHandle)).thenReturn(true)
-            }
         mockCursor =
             Mockito.mock(LoaderCursor::class.java, RETURNS_DEEP_STUBS).apply {
                 user = mUserHandle
@@ -163,138 +162,116 @@
 
     @Test
     fun `When Pending App Widget has not started restore then update db and add item`() {
-
-        val mockitoSession =
-            ExtendedMockito.mockitoSession()
-                .strictness(Strictness.LENIENT)
-                .mockStatic(WidgetSections::class.java)
-                .startMocking()
-        try {
-            // Given
-            val expectedProvider = "com.google.android.testApp/com.android.testApp.testAppProvider"
-            val expectedComponentName =
-                ComponentName.unflattenFromString(expectedProvider)!!.flattenToString()
-            val expectedRestoreStatus = FLAG_UI_NOT_READY or FLAG_RESTORE_STARTED
-            val expectedAppWidgetId = 0
-            mockCursor.apply {
-                itemType = ITEM_TYPE_APPWIDGET
-                user = mUserHandle
-                restoreFlag = FLAG_UI_NOT_READY
-                container = CONTAINER_DESKTOP
-                whenever(isOnWorkspaceOrHotseat).thenCallRealMethod()
-                whenever(appWidgetProvider).thenReturn(expectedProvider)
-                whenever(appWidgetId).thenReturn(expectedAppWidgetId)
-                whenever(spanX).thenReturn(2)
-                whenever(spanY).thenReturn(1)
-                whenever(options).thenReturn(0)
-                whenever(appWidgetSource).thenReturn(20)
-                whenever(applyCommonProperties(any())).thenCallRealMethod()
-                whenever(
-                        updater()
-                            .put(Favorites.APPWIDGET_PROVIDER, expectedComponentName)
-                            .put(Favorites.APPWIDGET_ID, expectedAppWidgetId)
-                            .put(Favorites.RESTORED, expectedRestoreStatus)
-                            .commit()
-                    )
-                    .thenReturn(1)
-            }
-            val inflationResult =
-                WidgetInflater.InflationResult(
-                    type = WidgetInflater.TYPE_PENDING,
-                    widgetInfo = null
-                )
-            mockWidgetInflater =
-                mock<WidgetInflater>().apply {
-                    whenever(inflateAppWidget(any())).thenReturn(inflationResult)
-                }
-            val packageUserKey = PackageUserKey("com.google.android.testApp", mUserHandle)
-            mInstallingPkgs[packageUserKey] = PackageInstaller.SessionInfo()
-
-            // When
-            itemProcessorUnderTest =
-                createWorkspaceItemProcessorUnderTest(widgetProvidersMap = mWidgetProvidersMap)
-            itemProcessorUnderTest.processItem()
-
-            // Then
-            val expectedWidgetInfo =
-                LauncherAppWidgetInfo().apply {
-                    appWidgetId = expectedAppWidgetId
-                    providerName = ComponentName.unflattenFromString(expectedProvider)
-                    restoreStatus = expectedRestoreStatus
-                }
-            verify(
-                    mockCursor
-                        .updater()
-                        .put(Favorites.APPWIDGET_PROVIDER, expectedProvider)
+        // Given
+        val expectedProvider = "com.google.android.testApp/com.android.testApp.testAppProvider"
+        val expectedComponentName =
+            ComponentName.unflattenFromString(expectedProvider)!!.flattenToString()
+        val expectedRestoreStatus = FLAG_UI_NOT_READY or FLAG_RESTORE_STARTED
+        val expectedAppWidgetId = 0
+        mockCursor.apply {
+            itemType = ITEM_TYPE_APPWIDGET
+            user = mUserHandle
+            restoreFlag = FLAG_UI_NOT_READY
+            container = CONTAINER_DESKTOP
+            whenever(isOnWorkspaceOrHotseat).thenCallRealMethod()
+            whenever(appWidgetProvider).thenReturn(expectedProvider)
+            whenever(appWidgetId).thenReturn(expectedAppWidgetId)
+            whenever(spanX).thenReturn(2)
+            whenever(spanY).thenReturn(1)
+            whenever(options).thenReturn(0)
+            whenever(appWidgetSource).thenReturn(20)
+            whenever(applyCommonProperties(any())).thenCallRealMethod()
+            whenever(
+                    updater()
+                        .put(Favorites.APPWIDGET_PROVIDER, expectedComponentName)
                         .put(Favorites.APPWIDGET_ID, expectedAppWidgetId)
                         .put(Favorites.RESTORED, expectedRestoreStatus)
+                        .commit()
                 )
-                .commit()
-            val widgetInfoCaptor = ArgumentCaptor.forClass(LauncherAppWidgetInfo::class.java)
-            verify(mockCursor).checkAndAddItem(widgetInfoCaptor.capture(), eq(mockBgDataModel))
-            val actualWidgetInfo = widgetInfoCaptor.value
-            with(actualWidgetInfo) {
-                assertThat(providerName).isEqualTo(expectedWidgetInfo.providerName)
-                assertThat(restoreStatus).isEqualTo(expectedWidgetInfo.restoreStatus)
-                assertThat(targetComponent).isEqualTo(expectedWidgetInfo.targetComponent)
-                assertThat(appWidgetId).isEqualTo(expectedWidgetInfo.appWidgetId)
+                .thenReturn(1)
+        }
+        val inflationResult =
+            WidgetInflater.InflationResult(type = WidgetInflater.TYPE_PENDING, widgetInfo = null)
+        mockWidgetInflater =
+            mock<WidgetInflater>().apply {
+                whenever(inflateAppWidget(any())).thenReturn(inflationResult)
             }
-        } finally {
-            mockitoSession.finishMocking()
+        val packageUserKey = PackageUserKey("com.google.android.testApp", mUserHandle)
+        mInstallingPkgs[packageUserKey] = PackageInstaller.SessionInfo()
+
+        // When
+        itemProcessorUnderTest =
+            createWorkspaceItemProcessorUnderTest(widgetProvidersMap = mWidgetProvidersMap)
+        itemProcessorUnderTest.processItem()
+
+        // Then
+        val expectedWidgetInfo =
+            LauncherAppWidgetInfo().apply {
+                appWidgetId = expectedAppWidgetId
+                providerName = ComponentName.unflattenFromString(expectedProvider)
+                restoreStatus = expectedRestoreStatus
+            }
+        verify(
+                mockCursor
+                    .updater()
+                    .put(Favorites.APPWIDGET_PROVIDER, expectedProvider)
+                    .put(Favorites.APPWIDGET_ID, expectedAppWidgetId)
+                    .put(Favorites.RESTORED, expectedRestoreStatus)
+            )
+            .commit()
+        val widgetInfoCaptor = ArgumentCaptor.forClass(LauncherAppWidgetInfo::class.java)
+        verify(mockCursor).checkAndAddItem(widgetInfoCaptor.capture(), eq(mockBgDataModel))
+        val actualWidgetInfo = widgetInfoCaptor.value
+        with(actualWidgetInfo) {
+            assertThat(providerName).isEqualTo(expectedWidgetInfo.providerName)
+            assertThat(restoreStatus).isEqualTo(expectedWidgetInfo.restoreStatus)
+            assertThat(targetComponent).isEqualTo(expectedWidgetInfo.targetComponent)
+            assertThat(appWidgetId).isEqualTo(expectedWidgetInfo.appWidgetId)
         }
     }
 
     @Test
     @EnableFlags(FLAG_ENABLE_SUPPORT_FOR_ARCHIVING)
     fun `When Archived Pending App Widget then checkAndAddItem`() {
-        val mockitoSession =
-            ExtendedMockito.mockitoSession().mockStatic(Utilities::class.java).startMocking()
-        try {
-            // Given
-            val expectedProvider = "com.google.android.testApp/com.android.testApp.testAppProvider"
-            val expectedComponentName = ComponentName.unflattenFromString(expectedProvider)
-            val expectedPackage = expectedComponentName!!.packageName
-            mockPmHelper =
-                mock<PackageManagerHelper>().apply {
-                    whenever(isAppArchived(expectedPackage)).thenReturn(true)
-                }
-            mockCursor =
-                mock<LoaderCursor>().apply {
-                    itemType = ITEM_TYPE_APPWIDGET
-                    id = 1
-                    user = UserHandle(1)
-                    restoreFlag = FLAG_UI_NOT_READY
-                    container = CONTAINER_DESKTOP
-                    whenever(isOnWorkspaceOrHotseat).thenCallRealMethod()
-                    whenever(appWidgetProvider).thenReturn(expectedProvider)
-                    whenever(appWidgetId).thenReturn(0)
-                    whenever(spanX).thenReturn(2)
-                    whenever(spanY).thenReturn(1)
-                    whenever(options).thenReturn(0)
-                    whenever(appWidgetSource).thenReturn(20)
-                    whenever(applyCommonProperties(any())).thenCallRealMethod()
-                }
-            mInstallingPkgs = hashMapOf()
-            val inflationResult =
-                WidgetInflater.InflationResult(
-                    type = WidgetInflater.TYPE_PENDING,
-                    widgetInfo = null
-                )
-            mockWidgetInflater =
-                mock<WidgetInflater>().apply {
-                    whenever(inflateAppWidget(any())).thenReturn(inflationResult)
-                }
-            itemProcessorUnderTest =
-                createWorkspaceItemProcessorUnderTest(widgetProvidersMap = mWidgetProvidersMap)
+        // Given
+        val expectedProvider = "com.google.android.testApp/com.android.testApp.testAppProvider"
+        val expectedComponentName = ComponentName.unflattenFromString(expectedProvider)
+        val expectedPackage = expectedComponentName!!.packageName
+        val expectedUser = UserHandle(1)
 
-            // When
-            itemProcessorUnderTest.processItem()
+        whenever(mockLauncherApps.getApplicationInfo(eq(expectedPackage), any(), eq(expectedUser)))
+            .thenReturn(ApplicationInfo().apply { isArchived = true })
+        mockCursor =
+            mock<LoaderCursor>().apply {
+                itemType = ITEM_TYPE_APPWIDGET
+                id = 1
+                user = expectedUser
+                restoreFlag = FLAG_UI_NOT_READY
+                container = CONTAINER_DESKTOP
+                whenever(isOnWorkspaceOrHotseat).thenCallRealMethod()
+                whenever(appWidgetProvider).thenReturn(expectedProvider)
+                whenever(appWidgetId).thenReturn(0)
+                whenever(spanX).thenReturn(2)
+                whenever(spanY).thenReturn(1)
+                whenever(options).thenReturn(0)
+                whenever(appWidgetSource).thenReturn(20)
+                whenever(applyCommonProperties(any())).thenCallRealMethod()
+            }
+        mInstallingPkgs = hashMapOf()
+        val inflationResult =
+            WidgetInflater.InflationResult(type = WidgetInflater.TYPE_PENDING, widgetInfo = null)
+        mockWidgetInflater =
+            mock<WidgetInflater>().apply {
+                whenever(inflateAppWidget(any())).thenReturn(inflationResult)
+            }
+        itemProcessorUnderTest =
+            createWorkspaceItemProcessorUnderTest(widgetProvidersMap = mWidgetProvidersMap)
 
-            // Then
-            verify(mockCursor).checkAndAddItem(any(), any())
-        } finally {
-            mockitoSession.finishMocking()
-        }
+        // When
+        itemProcessorUnderTest.processItem()
+
+        // Then
+        verify(mockCursor).checkAndAddItem(any(), any())
     }
 
     private fun createWorkspaceItemProcessorUnderTest(
@@ -314,7 +291,7 @@
         pendingPackages: MutableSet<PackageUserKey> = mPendingPackages,
         unlockedUsers: LongSparseArray<Boolean> = mUnlockedUsersArray,
         installingPkgs: HashMap<PackageUserKey, PackageInstaller.SessionInfo> = mInstallingPkgs,
-        allDeepShortcuts: MutableList<ShortcutInfo> = mAllDeepShortcuts
+        allDeepShortcuts: MutableList<CacheableShortcutInfo> = mAllDeepShortcuts,
     ) =
         WorkspaceItemProcessor(
             c = cursor,
@@ -333,6 +310,6 @@
             isSdCardReady = isSdCardReady,
             shortcutKeyToPinnedShortcuts = shortcutKeyToPinnedShortcuts,
             installingPkgs = installingPkgs,
-            allDeepShortcuts = allDeepShortcuts
+            allDeepShortcuts = allDeepShortcuts,
         )
 }
diff --git a/tests/src/com/android/launcher3/model/gridmigration/ValidGridMigrationUnitTest.kt b/tests/src/com/android/launcher3/model/gridmigration/ValidGridMigrationUnitTest.kt
index 03d0195..b96dbcd 100644
--- a/tests/src/com/android/launcher3/model/gridmigration/ValidGridMigrationUnitTest.kt
+++ b/tests/src/com/android/launcher3/model/gridmigration/ValidGridMigrationUnitTest.kt
@@ -20,17 +20,21 @@
 import android.database.sqlite.SQLiteDatabase
 import android.graphics.Point
 import android.os.Process
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
 import android.util.Log
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import androidx.test.platform.app.InstrumentationRegistry
+import com.android.launcher3.Flags
 import com.android.launcher3.InvariantDeviceProfile
 import com.android.launcher3.LauncherSettings.Favorites
 import com.android.launcher3.celllayout.testgenerator.ValidGridMigrationTestCaseGenerator
 import com.android.launcher3.celllayout.testgenerator.generateItemsForTest
 import com.android.launcher3.model.DatabaseHelper
 import com.android.launcher3.model.DeviceGridState
-import com.android.launcher3.model.GridSizeMigrationUtil
+import com.android.launcher3.model.GridSizeMigrationDBController
+import com.android.launcher3.model.GridSizeMigrationLogic
 import com.android.launcher3.pm.UserCache
 import com.android.launcher3.provider.LauncherDbUtils
 import com.android.launcher3.util.rule.TestStabilityRule
@@ -130,22 +134,52 @@
         addItemsToDb(dbHelper.writableDatabase, dstGrid)
 
         LauncherDbUtils.SQLiteTransaction(dbHelper.writableDatabase).use {
-            GridSizeMigrationUtil.migrate(
-                dbHelper,
-                GridSizeMigrationUtil.DbReader(it.db, srcGrid.tableName, context),
-                GridSizeMigrationUtil.DbReader(it.db, dstGrid.tableName, context),
-                dstGrid.size.x,
-                dstGrid.size,
-                srcGrid.toGridState(),
-                dstGrid.toGridState(),
-            )
+            if (Flags.gridMigrationRefactor()) {
+                val gridSizeMigrationLogic = GridSizeMigrationLogic()
+                val idsInUse = mutableListOf<Int>()
+                gridSizeMigrationLogic.migrateHotseat(
+                    dstGrid.size.x,
+                    GridSizeMigrationDBController.DbReader(it.db, srcGrid.tableName, context),
+                    GridSizeMigrationDBController.DbReader(it.db, dstGrid.tableName, context),
+                    dbHelper,
+                    idsInUse,
+                )
+                gridSizeMigrationLogic.migrateWorkspace(
+                    GridSizeMigrationDBController.DbReader(it.db, srcGrid.tableName, context),
+                    GridSizeMigrationDBController.DbReader(it.db, dstGrid.tableName, context),
+                    dbHelper,
+                    dstGrid.size,
+                    idsInUse,
+                )
+            } else {
+                GridSizeMigrationDBController.migrate(
+                    dbHelper,
+                    GridSizeMigrationDBController.DbReader(it.db, srcGrid.tableName, context),
+                    GridSizeMigrationDBController.DbReader(it.db, dstGrid.tableName, context),
+                    dstGrid.size.x,
+                    dstGrid.size,
+                    srcGrid.toGridState(),
+                    dstGrid.toGridState(),
+                )
+            }
             it.commit()
         }
         return readDb(dstGrid.tableName, dbHelper.readableDatabase)
     }
 
     @Test
-    fun runTestCase() {
+    @EnableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR)
+    fun runTestCaseRefactorFlagEnabled() {
+        runTestCase()
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR)
+    fun runTestCaseRefactorFlagDisabled() {
+        runTestCase()
+    }
+
+    private fun runTestCase() {
         val caseGenerator = ValidGridMigrationTestCaseGenerator(Random(SEED.toLong()))
         for (i in 0..SMALL_TEST_SIZE) {
             val testCase = caseGenerator.generateTestCase(isDestEmpty = true)
@@ -163,7 +197,18 @@
     }
 
     @Test
-    fun mergeBoards() {
+    @EnableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR)
+    fun mergeBoardsRefactorFlagEnabled() {
+        mergeBoards()
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR)
+    fun mergeBoardsRefactorFlagDisabled() {
+        mergeBoards()
+    }
+
+    private fun mergeBoards() {
         val caseGenerator = ValidGridMigrationTestCaseGenerator(Random(SEED.toLong()))
         for (i in 0..SMALL_TEST_SIZE) {
             val testCase = caseGenerator.generateTestCase(isDestEmpty = false)
@@ -187,7 +232,20 @@
     // This test takes about 4 minutes, there is no need to run it in presubmit.
     @Stability(flavors = TestStabilityRule.LOCAL or TestStabilityRule.PLATFORM_POSTSUBMIT)
     @Test
-    fun runExtensiveTestCases() {
+    @EnableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR)
+    fun runExtensiveTestCasesRefactorFlagEnabled() {
+        runExtensiveTestCases()
+    }
+
+    // This test takes about 4 minutes, there is no need to run it in presubmit.
+    @Stability(flavors = TestStabilityRule.LOCAL or TestStabilityRule.PLATFORM_POSTSUBMIT)
+    @Test
+    @DisableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR)
+    fun runExtensiveTestCasesRefactorFlagDisabled() {
+        runExtensiveTestCases()
+    }
+
+    private fun runExtensiveTestCases() {
         val caseGenerator = ValidGridMigrationTestCaseGenerator(Random(SEED.toLong()))
         for (i in 0..LARGE_TEST_SIZE) {
             val testCase = caseGenerator.generateTestCase(isDestEmpty = true)
diff --git a/tests/src/com/android/launcher3/nonquickstep/DeviceProfileDumpTest.kt b/tests/src/com/android/launcher3/nonquickstep/DeviceProfileDumpTest.kt
index 60385a7..2e2b6cd 100644
--- a/tests/src/com/android/launcher3/nonquickstep/DeviceProfileDumpTest.kt
+++ b/tests/src/com/android/launcher3/nonquickstep/DeviceProfileDumpTest.kt
@@ -20,6 +20,7 @@
 import com.android.launcher3.DeviceProfile
 import com.android.launcher3.Flags
 import com.android.launcher3.InvariantDeviceProfile
+import com.android.launcher3.util.rule.setFlags
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -35,11 +36,11 @@
 
     @Before
     fun setUp() {
-        if (instance.decoupleDepth) {
-            setFlagsRule.enableFlags(Flags.FLAG_ENABLE_SCALING_REVEAL_HOME_ANIMATION)
-        } else {
-            setFlagsRule.disableFlags(Flags.FLAG_ENABLE_SCALING_REVEAL_HOME_ANIMATION)
-        }
+        setFlagsRule.setFlags(
+            instance.decoupleDepth,
+            Flags.FLAG_ENABLE_SCALING_REVEAL_HOME_ANIMATION,
+        )
+        setFlagsRule.setFlags(false, Flags.FLAG_ONE_GRID_SPECS)
     }
 
     @Test
@@ -105,13 +106,13 @@
                 initializeVarsForTablet(
                     deviceSpec = deviceSpec,
                     isLandscape = isLandscape,
-                    isGestureMode = isGestureMode
+                    isGestureMode = isGestureMode,
                 )
             else ->
                 initializeVarsForPhone(
                     deviceSpec = deviceSpec,
                     isVerticalBar = isLandscape,
-                    isGestureMode = isGestureMode
+                    isGestureMode = isGestureMode,
                 )
         }
     }
@@ -136,7 +137,7 @@
                     "twopanel-tablet",
                     gridName = "4_by_4",
                     isTaskbarPresentInApps = true,
-                    decoupleDepth = true
+                    decoupleDepth = true,
                 ),
             )
         }
@@ -145,7 +146,7 @@
             val deviceName: String,
             val gridName: String,
             val isTaskbarPresentInApps: Boolean = false,
-            val decoupleDepth: Boolean = false
+            val decoupleDepth: Boolean = false,
         ) {
             fun filename(testName: String = ""): String {
                 val device =
diff --git a/tests/src/com/android/launcher3/pm/InstallSessionHelperTest.kt b/tests/src/com/android/launcher3/pm/InstallSessionHelperTest.kt
index 3dd8dbc..ca2ef42 100644
--- a/tests/src/com/android/launcher3/pm/InstallSessionHelperTest.kt
+++ b/tests/src/com/android/launcher3/pm/InstallSessionHelperTest.kt
@@ -18,6 +18,7 @@
 
 import android.content.pm.ApplicationInfo
 import android.content.pm.ApplicationInfo.FLAG_INSTALLED
+import android.content.pm.ApplicationInfo.FLAG_SYSTEM
 import android.content.pm.LauncherApps
 import android.content.pm.PackageInstaller
 import android.content.pm.PackageManager
@@ -35,7 +36,9 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.kotlin.any
 import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.spy
 import org.mockito.kotlin.whenever
@@ -126,13 +129,10 @@
     fun `isTrustedPackage returns true if LauncherApps finds ApplicationInfo`() {
         // Given
         val expectedApplicationInfo =
-            ApplicationInfo().apply {
-                flags = flags or FLAG_INSTALLED
-                enabled = true
-            }
+            ApplicationInfo().apply { flags = FLAG_SYSTEM or FLAG_INSTALLED }
         doReturn(expectedApplicationInfo)
             .whenever(launcherApps)
-            .getApplicationInfo(expectedAppPackage, ApplicationInfo.FLAG_SYSTEM, UserHandle(0))
+            .getApplicationInfo(eq(expectedAppPackage), any(), eq(UserHandle(0)))
         // When
         val actualResult = installSessionHelper.isTrustedPackage(expectedAppPackage, UserHandle(0))
         // Then
diff --git a/tests/src/com/android/launcher3/popup/SystemShortcutTest.java b/tests/src/com/android/launcher3/popup/SystemShortcutTest.java
index f54668c..ae54e95 100644
--- a/tests/src/com/android/launcher3/popup/SystemShortcutTest.java
+++ b/tests/src/com/android/launcher3/popup/SystemShortcutTest.java
@@ -62,6 +62,8 @@
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.R;
 import com.android.launcher3.allapps.PrivateProfileManager;
+import com.android.launcher3.dagger.LauncherAppComponent;
+import com.android.launcher3.dagger.LauncherAppSingleton;
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.logging.StatsLogManager.StatsLogger;
 import com.android.launcher3.model.data.AppInfo;
@@ -79,6 +81,9 @@
 import com.android.launcher3.widget.picker.model.WidgetPickerDataProvider;
 import com.android.launcher3.widget.picker.model.data.WidgetPickerData;
 
+import dagger.BindsInstance;
+import dagger.Component;
+
 import org.junit.After;
 import org.junit.Assert;
 import org.junit.Before;
@@ -115,8 +120,10 @@
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
+        mSandboxContext.initDaggerComponent(
+                DaggerSystemShortcutTest_TestComponent.builder().bindApiWrapper(
+                        ApiWrapper.INSTANCE.get(mSandboxContext)));
         mSandboxContext.putObject(UserCache.INSTANCE, mUserCache);
-        mSandboxContext.putObject(ApiWrapper.INSTANCE, mApiWrapper);
         mTestContext = new TestSandboxModelContextWrapper(mSandboxContext) {
             @Override
             public StatsLogManager getStatsLogManager() {
@@ -405,4 +412,16 @@
         systemShortcut.onClick(mView);
         verify(mSandboxContext).startActivity(any());
     }
+
+    @LauncherAppSingleton
+    @Component
+    interface TestComponent extends LauncherAppComponent {
+        @Component.Builder
+        interface Builder extends LauncherAppComponent.Builder {
+            @BindsInstance Builder bindApiWrapper(ApiWrapper wrapper);
+
+            @Override
+            TestComponent build();
+        }
+    }
 }
diff --git a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
index 68004bb..2b1fddc 100644
--- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
+++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
@@ -20,20 +20,18 @@
 import static androidx.test.InstrumentationRegistry.getInstrumentation;
 
 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.TestUtil.resolveSystemAppInfo;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
+import android.app.ActivityManager;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
-import android.content.pm.ActivityInfo;
-import android.content.pm.LauncherActivityInfo;
-import android.content.pm.LauncherApps;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.graphics.Point;
@@ -97,12 +95,12 @@
  */
 public abstract class AbstractLauncherUiTest<LAUNCHER_TYPE extends Launcher> {
 
-    public static final long DEFAULT_ACTIVITY_TIMEOUT = TimeUnit.SECONDS.toMillis(10);
     public static final long DEFAULT_BROADCAST_TIMEOUT_SECS = 10;
 
-    public static final long DEFAULT_UI_TIMEOUT = TestUtil.DEFAULT_UI_TIMEOUT;
     private static final String TAG = "AbstractLauncherUiTest";
 
+    private static final long BYTES_PER_MEGABYTE = 1 << 20;
+
     private static boolean sDumpWasGenerated = false;
     private static boolean sActivityLeakReported = false;
     private static boolean sSeenKeyguard = false;
@@ -124,6 +122,10 @@
     protected String mTargetPackage;
     private int mLauncherPid;
 
+    private final ActivityManager.MemoryInfo mMemoryInfo = new ActivityManager.MemoryInfo();
+    private final ActivityManager mActivityManager;
+    private long mMemoryBefore;
+
     /** Detects activity leaks and throws an exception if a leak is found. */
     public static void checkDetectedLeaks(LauncherInstrumentation launcher) {
         checkDetectedLeaks(launcher, false);
@@ -145,7 +147,7 @@
                     launcher.forceGc();
                     return MAIN_EXECUTOR.submit(
                             () -> launcher.noLeakedActivities(requireOneActiveActivity)).get();
-                }, DEFAULT_UI_TIMEOUT, launcher);
+                }, launcher);
     }
 
     public static String getAppPackageName() {
@@ -192,6 +194,8 @@
     }
 
     protected AbstractLauncherUiTest() {
+        mActivityManager = InstrumentationRegistry.getContext()
+                .getSystemService(ActivityManager.class);
         mLauncher.enableCheckEventsForSuccessfulGestures();
         mLauncher.setAnomalyChecker(AbstractLauncherUiTest::verifyKeyguardInvisible);
         try {
@@ -249,7 +253,7 @@
 
     protected TestRule getRulesInsideActivityMonitor() {
         final ViewCaptureRule viewCaptureRule = new ViewCaptureRule(
-                Launcher.ACTIVITY_TRACKER::getCreatedActivity);
+                Launcher.ACTIVITY_TRACKER::getCreatedContext);
         final RuleChain inner = RuleChain
                 .outerRule(new PortraitLandscapeRunner<LAUNCHER_TYPE>(this))
                 .around(new FailureWatcher(mLauncher, viewCaptureRule::getViewCaptureData))
@@ -311,6 +315,26 @@
         initialize(this);
     }
 
+    private long getAvailableMemory() {
+        mActivityManager.getMemoryInfo(mMemoryInfo);
+
+        return Math.divideExact(mMemoryInfo.availMem,  BYTES_PER_MEGABYTE);
+    }
+
+    @Before
+    public void saveMemoryBefore() {
+        mMemoryBefore = getAvailableMemory();
+    }
+
+    @After
+    public void logMemoryAfter() {
+        long memoryAfter = getAvailableMemory();
+
+        Log.d(TAG, "Available memory: before=" + mMemoryBefore
+                + "MB, after=" + memoryAfter
+                + "MB, delta=" + (memoryAfter - mMemoryBefore) + "MB");
+    }
+
     /** Method that should be called when a test starts. */
     public static void onTestStart() {
         waitForSetupWizardDismissal();
@@ -383,9 +407,19 @@
     }
 
     @After
+    public void resetFreezeRecentTaskList() {
+        try {
+            mDevice.executeShellCommand("wm reset-freeze-recent-tasks");
+        } catch (IOException e) {
+            Log.e(TAG, "Failed to reset fozen recent tasks list", e);
+        }
+    }
+
+    @After
     public void verifyLauncherState() {
         try {
             // Limits UI tests affecting tests running after them.
+            mDevice.pressHome();
             mLauncher.waitForLauncherInitialized();
             if (mLauncherPid != 0) {
                 assertEquals("Launcher crashed, pid mismatch:",
@@ -414,7 +448,7 @@
      */
     protected <T> T getOnUiThread(final Callable<T> callback) {
         try {
-            return mMainThreadExecutor.submit(callback).get(DEFAULT_UI_TIMEOUT,
+            return mMainThreadExecutor.submit(callback).get(TestUtil.DEFAULT_UI_TIMEOUT,
                     TimeUnit.MILLISECONDS);
         } catch (TimeoutException e) {
             Log.e(TAG, "Timeout in getOnUiThread, sending SIGABRT", e);
@@ -427,7 +461,7 @@
 
     protected <T> T getFromLauncher(Function<LAUNCHER_TYPE, T> f) {
         if (!TestHelpers.isInLauncherProcess()) return null;
-        return getOnUiThread(() -> f.apply(Launcher.ACTIVITY_TRACKER.getCreatedActivity()));
+        return getOnUiThread(() -> f.apply(Launcher.ACTIVITY_TRACKER.getCreatedContext()));
     }
 
     protected void executeOnLauncher(Consumer<LAUNCHER_TYPE> f) {
@@ -469,13 +503,7 @@
     // flakiness.
     protected void waitForLauncherCondition(String
             message, Function<LAUNCHER_TYPE, Boolean> condition) {
-        waitForLauncherCondition(message, condition, DEFAULT_ACTIVITY_TIMEOUT);
-    }
-
-    // Cannot be used in TaplTests after injecting any gesture using Tapl because this can hide
-    // flakiness.
-    protected <O> O getOnceNotNull(String message, Function<LAUNCHER_TYPE, O> f) {
-        return getOnceNotNull(message, f, DEFAULT_ACTIVITY_TIMEOUT);
+        waitForLauncherCondition(message, condition, TestUtil.DEFAULT_UI_TIMEOUT);
     }
 
     // Cannot be used in TaplTests after injecting any gesture using Tapl because this can hide
@@ -484,12 +512,12 @@
             String message, Function<LAUNCHER_TYPE, Boolean> condition, long timeout) {
         verifyKeyguardInvisible();
         if (!TestHelpers.isInLauncherProcess()) return;
-        Wait.atMost(message, () -> getFromLauncher(condition), timeout, mLauncher);
+        Wait.atMost(message, () -> getFromLauncher(condition), mLauncher, timeout);
     }
 
     // Cannot be used in TaplTests after injecting any gesture using Tapl because this can hide
     // flakiness.
-    protected <T> T getOnceNotNull(String message, Function<LAUNCHER_TYPE, T> f, long timeout) {
+    protected <T> T getOnceNotNull(String message, Function<LAUNCHER_TYPE, T> f) {
         if (!TestHelpers.isInLauncherProcess()) return null;
 
         final Object[] output = new Object[1];
@@ -497,7 +525,7 @@
             final Object fromLauncher = getFromLauncher(f);
             output[0] = fromLauncher;
             return fromLauncher != null;
-        }, timeout, mLauncher);
+        }, mLauncher);
         return (T) output[0];
     }
 
@@ -511,12 +539,7 @@
         Wait.atMost(message, () -> {
             testThreadAction.run();
             return getFromLauncher(condition);
-        }, timeout, mLauncher);
-    }
-
-    protected LauncherActivityInfo getSettingsApp() {
-        return mTargetContext.getSystemService(LauncherApps.class)
-                .getActivityList("com.android.settings", Process.myUserHandle()).get(0);
+        }, mLauncher, timeout);
     }
 
     /**
@@ -534,23 +557,13 @@
 
         @Override
         public void onReceive(Context context, Intent intent) {
-            Log.d(WIDGET_CONFIG_NULL_EXTRA_INTENT, intent == null
-                    ? "AbstractLauncherUiTest.onReceive(): inputted intent NULL"
-                    : "AbstractLauncherUiTest.onReceive(): inputted intent NOT NULL");
             mIntent = intent;
             latch.countDown();
-            Log.d(WIDGET_CONFIG_NULL_EXTRA_INTENT,
-                    "AbstractLauncherUiTest.onReceive() Countdown Latch started");
         }
 
         public Intent blockingGetIntent() throws InterruptedException {
-            Log.d(WIDGET_CONFIG_NULL_EXTRA_INTENT,
-                    "AbstractLauncherUiTest.blockingGetIntent()");
             assertTrue("Timed Out", latch.await(DEFAULT_BROADCAST_TIMEOUT_SECS, TimeUnit.SECONDS));
             mTargetContext.unregisterReceiver(this);
-            Log.d(WIDGET_CONFIG_NULL_EXTRA_INTENT, mIntent == null
-                    ? "AbstractLauncherUiTest.onReceive(): mIntent NULL"
-                    : "AbstractLauncherUiTest.onReceive(): mIntent NOT NULL");
             return mIntent;
         }
 
@@ -614,20 +627,13 @@
         }
         getInstrumentation().getTargetContext().startActivity(intent);
         assertTrue("App didn't start: " + selector,
-                TestHelpers.wait(Until.hasObject(selector), DEFAULT_UI_TIMEOUT));
+                TestHelpers.wait(Until.hasObject(selector), TestUtil.DEFAULT_UI_TIMEOUT));
 
         // Wait for the Launcher to stop.
         final LauncherInstrumentation launcherInstrumentation = new LauncherInstrumentation();
         Wait.atMost("Launcher activity didn't stop",
                 () -> !launcherInstrumentation.isLauncherActivityStarted(),
-                DEFAULT_ACTIVITY_TIMEOUT, launcherInstrumentation);
-    }
-
-    public static ActivityInfo resolveSystemAppInfo(String category) {
-        return getInstrumentation().getContext().getPackageManager().resolveActivity(
-                new Intent(Intent.ACTION_MAIN).addCategory(category),
-                PackageManager.MATCH_SYSTEM_ONLY).
-                activityInfo;
+                launcherInstrumentation);
     }
 
 
@@ -643,8 +649,7 @@
                 launcher.finish();
             }
         });
-        waitForLauncherCondition(
-                "Launcher still active", launcher -> launcher == null, DEFAULT_UI_TIMEOUT);
+        waitForLauncherCondition("Launcher still active", launcher -> launcher == null);
     }
 
     protected boolean isInLaunchedApp(LAUNCHER_TYPE launcher) {
diff --git a/tests/src/com/android/launcher3/ui/TaplWorkProfileTest.java b/tests/src/com/android/launcher3/ui/WorkProfileTest.java
similarity index 76%
rename from tests/src/com/android/launcher3/ui/TaplWorkProfileTest.java
rename to tests/src/com/android/launcher3/ui/WorkProfileTest.java
index b38dd4b..d866a9f 100644
--- a/tests/src/com/android/launcher3/ui/TaplWorkProfileTest.java
+++ b/tests/src/com/android/launcher3/ui/WorkProfileTest.java
@@ -17,9 +17,7 @@
 
 import static com.android.launcher3.LauncherPrefs.WORK_EDU_STEP;
 import static com.android.launcher3.LauncherState.ALL_APPS;
-import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.allapps.AllAppsStore.DEFER_UPDATES_TEST;
-import static com.android.launcher3.testing.shared.TestProtocol.NORMAL_STATE_ORDINAL;
 import static com.android.launcher3.util.TestUtil.installDummyAppForUser;
 import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL;
 import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT;
@@ -43,37 +41,39 @@
 import com.android.launcher3.allapps.WorkEduCard;
 import com.android.launcher3.allapps.WorkPausedCard;
 import com.android.launcher3.allapps.WorkProfileManager;
-import com.android.launcher3.tapl.LauncherInstrumentation;
+import com.android.launcher3.util.BaseLauncherActivityTest;
 import com.android.launcher3.util.TestUtil;
+import com.android.launcher3.util.rule.ScreenRecordRule;
+import com.android.launcher3.util.rule.TestStabilityRule;
 import com.android.launcher3.util.rule.TestStabilityRule.Stability;
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
-import java.io.IOException;
-import java.util.Objects;
 import java.util.function.Predicate;
 
 @LargeTest
 @RunWith(AndroidJUnit4.class)
-public class TaplWorkProfileTest extends AbstractLauncherUiTest<Launcher> {
+public class WorkProfileTest extends BaseLauncherActivityTest<Launcher> {
 
     private static final int WORK_PAGE = ActivityAllAppsContainerView.AdapterHolder.WORK;
+    public static final int WAIT_TIME_MS = 30000;
+
+    @Rule
+    public ScreenRecordRule mScreenRecordRule = new ScreenRecordRule();
+    @Rule
+    public TestStabilityRule mTestStabilityRule = new TestStabilityRule();
 
     private int mProfileUserId;
     private boolean mWorkProfileSetupSuccessful;
-    private final String TAG = "WorkProfileTest";
+    private static final String TAG = "WorkProfileTest";
 
     @Before
-    @Override
     public void setUp() throws Exception {
-        super.setUp();
-        initialize(this);
-        String output =
-                mDevice.executeShellCommand(
-                        "pm create-user --profileOf 0 --managed TestProfile");
+        String output = executeShellCommand("pm create-user --profileOf 0 --managed TestProfile");
         updateWorkProfileSetupSuccessful("pm create-user", output);
 
         String[] tokens = output.split("\\s+");
@@ -89,36 +89,15 @@
             return; // no need to setup launcher since all tests will skip.
         }
 
-        mDevice.pressHome();
-        waitForLauncherCondition("Launcher didn't start", Objects::nonNull);
-        waitForStateTransitionToEnd("Launcher internal state didn't switch to Normal",
-                () -> NORMAL);
-        waitForResumed("Launcher internal state is still Background");
-        mLauncher.getWorkspace().switchToAllApps();
-        waitForStateTransitionToEnd("Launcher internal state didn't switch to All Apps",
-                () -> ALL_APPS);
+        loadLauncherSync();
+        goToState(ALL_APPS);
+        waitForState("Launcher internal state didn't switch to All Apps", () -> ALL_APPS);
     }
 
     @After
     public void removeWorkProfile() throws Exception {
-        executeOnLauncherInTearDown(launcher -> {
-            if (launcher.getAppsView() == null) {
-                return;
-            }
-            launcher.getAppsView().getAppsStore().disableDeferUpdates(DEFER_UPDATES_TEST);
-        });
         TestUtil.uninstallDummyApp();
-
-        mLauncher.runToState(
-                () -> {
-                    try {
-                        mDevice.executeShellCommand("pm remove-user --wait " + mProfileUserId);
-                    } catch (IOException e) {
-                        throw new RuntimeException(e);
-                    }
-                },
-                NORMAL_STATE_ORDINAL,
-                "executing pm 'remove-user' command");
+        executeShellCommand("pm remove-user --wait " + mProfileUserId);
     }
 
     private void waitForWorkTabSetup() {
@@ -128,7 +107,7 @@
                 return true;
             }
             return false;
-        }, LauncherInstrumentation.WAIT_TIME_MS);
+        }, WAIT_TIME_MS);
     }
 
     @Test
@@ -138,10 +117,10 @@
         waitForWorkTabSetup();
         waitForLauncherCondition("Personal tab is missing",
                 launcher -> launcher.getAppsView().isPersonalTabVisible(),
-                LauncherInstrumentation.WAIT_TIME_MS);
+                WAIT_TIME_MS);
         waitForLauncherCondition("Work tab is missing",
                 launcher -> launcher.getAppsView().isWorkTabVisible(),
-                LauncherInstrumentation.WAIT_TIME_MS);
+                WAIT_TIME_MS);
     }
 
     // Staging; will be promoted to presubmit if stable
@@ -157,24 +136,23 @@
 
         WorkProfileManager manager = getFromLauncher(l -> l.getAppsView().getWorkManager());
 
-
         waitForLauncherCondition("work profile initial state check failed", launcher ->
-                        manager.getWorkModeSwitch() != null
+                        manager.getWorkUtilityView() != null
                                 && manager.getCurrentState() == WorkProfileManager.STATE_ENABLED
-                                && manager.getWorkModeSwitch().isEnabled(),
-                LauncherInstrumentation.WAIT_TIME_MS);
+                                && manager.getWorkUtilityView().isEnabled(),
+                WAIT_TIME_MS);
 
         //start work profile toggle OFF test
         executeOnLauncher(l -> {
             // Ensure updates are not deferred so notification happens when apps pause.
             l.getAppsView().getAppsStore().disableDeferUpdates(DEFER_UPDATES_TEST);
-            l.getAppsView().getWorkManager().getWorkModeSwitch().performClick();
+            l.getAppsView().getWorkManager().getWorkUtilityView().performClick();
         });
 
         waitForLauncherCondition("Work profile toggle OFF failed", launcher -> {
             manager.reset(); // pulls current state from system
             return manager.getCurrentState() == WorkProfileManager.STATE_DISABLED;
-        }, LauncherInstrumentation.WAIT_TIME_MS);
+        }, WAIT_TIME_MS);
 
         waitForWorkCard("Work paused card not shown", view -> view instanceof WorkPausedCard);
 
@@ -189,7 +167,7 @@
         waitForLauncherCondition("Work profile toggle ON failed", launcher -> {
             manager.reset(); // pulls current state from system
             return manager.getCurrentState() == WorkProfileManager.STATE_ENABLED;
-        }, LauncherInstrumentation.WAIT_TIME_MS);
+        }, WAIT_TIME_MS);
 
     }
 
@@ -216,7 +194,7 @@
             } finally {
                 l.getAppsView().getAppsStore().enableDeferUpdates(DEFER_UPDATES_TEST);
             }
-        }, LauncherInstrumentation.WAIT_TIME_MS);
+        }, WAIT_TIME_MS);
     }
 
     private void updateWorkProfileSetupSuccessful(String cli, String output) {
diff --git a/tests/src/com/android/launcher3/ui/widget/TaplAddConfigWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/TaplAddConfigWidgetTest.java
index e6e02b4..7845222 100644
--- a/tests/src/com/android/launcher3/ui/widget/TaplAddConfigWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/TaplAddConfigWidgetTest.java
@@ -93,7 +93,7 @@
         WidgetConfigStartupMonitor monitor = new WidgetConfigStartupMonitor();
         mLauncher.getWorkspace()
                 .openAllWidgets()
-                .getWidget(mWidgetInfo.getLabel(mTargetContext.getPackageManager()))
+                .getWidget(mWidgetInfo.getLabel())
                 .dragToWorkspace(true, false);
         // Widget id for which the config activity was opened
         mWidgetId = monitor.getWidgetId();
@@ -103,12 +103,12 @@
 
         setResultAndWaitForAnimation(acceptConfig);
         if (acceptConfig) {
-            Wait.atMost("", new WidgetSearchCondition(), DEFAULT_ACTIVITY_TIMEOUT, mLauncher);
+            Wait.atMost("", new WidgetSearchCondition(), mLauncher);
             assertNotNull(mAppWidgetManager.getAppWidgetInfo(mWidgetId));
         } else {
             // Verify that the widget id is deleted.
             Wait.atMost("", () -> mAppWidgetManager.getAppWidgetInfo(mWidgetId) == null,
-                    DEFAULT_ACTIVITY_TIMEOUT, mLauncher);
+                    mLauncher);
         }
     }
 
@@ -136,7 +136,7 @@
         @Override
         public boolean isTrue() throws Throwable {
             return mMainThreadExecutor.submit(() -> {
-                Launcher l = Launcher.ACTIVITY_TRACKER.getCreatedActivity();
+                Launcher l = Launcher.ACTIVITY_TRACKER.getCreatedContext();
                 return l != null && l.getWorkspace().getFirstMatch(this) != null;
             }).get();
         }
diff --git a/tests/src/com/android/launcher3/ui/widget/TaplAddWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/TaplAddWidgetTest.java
index 9c916fa..460ffc4 100644
--- a/tests/src/com/android/launcher3/ui/widget/TaplAddWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/TaplAddWidgetTest.java
@@ -29,6 +29,7 @@
 import com.android.launcher3.ui.AbstractLauncherUiTest;
 import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
 import com.android.launcher3.ui.TestViewHelpers;
+import com.android.launcher3.util.TestUtil;
 import com.android.launcher3.util.rule.ShellCommandRule;
 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 
@@ -61,14 +62,14 @@
         WidgetResizeFrame resizeFrame = mLauncher
                 .getWorkspace()
                 .openAllWidgets()
-                .getWidget(widgetInfo.getLabel(mTargetContext.getPackageManager()))
+                .getWidget(widgetInfo.getLabel())
                 .dragWidgetToWorkspace();
 
         assertNotNull("Widget resize frame not shown after widget add", resizeFrame);
         resizeFrame.dismiss();
 
         final Widget widget = mLauncher.getWorkspace().tryGetWidget(widgetInfo.label,
-                DEFAULT_UI_TIMEOUT);
+                TestUtil.DEFAULT_UI_TIMEOUT);
         assertNotNull("Widget not found on the workspace", widget);
         widget.launch(getAppPackageName());
         mLauncher.disableDebugTracing(); // b/289161193
@@ -111,7 +112,7 @@
         WidgetResizeFrame resizeFrame = mLauncher
                 .getWorkspace()
                 .openAllWidgets()
-                .getWidget(widgetInfo.getLabel(mTargetContext.getPackageManager()))
+                .getWidget(widgetInfo.getLabel())
                 .dragWidgetToWorkspace();
 
         assertNotNull("Widget resize frame not shown after widget add", resizeFrame);
diff --git a/tests/src/com/android/launcher3/ui/widget/TaplBindWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/TaplBindWidgetTest.java
index d40d3bc..4cdbd96 100644
--- a/tests/src/com/android/launcher3/ui/widget/TaplBindWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/TaplBindWidgetTest.java
@@ -53,6 +53,7 @@
 import com.android.launcher3.tapl.Workspace;
 import com.android.launcher3.ui.AbstractLauncherUiTest;
 import com.android.launcher3.ui.TestViewHelpers;
+import com.android.launcher3.util.TestUtil;
 import com.android.launcher3.util.rule.ShellCommandRule;
 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.WidgetManagerHelper;
@@ -233,13 +234,15 @@
     }
 
     private void verifyWidgetPresent(LauncherAppWidgetProviderInfo info) {
-        final Widget widget = mLauncher.getWorkspace().tryGetWidget(info.label, DEFAULT_UI_TIMEOUT);
+        final Widget widget = mLauncher.getWorkspace().tryGetWidget(info.label,
+                TestUtil.DEFAULT_UI_TIMEOUT);
         assertTrue("Widget is not present",
                 widget != null);
     }
 
     private void verifyPendingWidgetPresent() {
-        final Widget widget = mLauncher.getWorkspace().tryGetPendingWidget(DEFAULT_UI_TIMEOUT);
+        final Widget widget = mLauncher.getWorkspace().tryGetPendingWidget(
+                TestUtil.DEFAULT_UI_TIMEOUT);
         assertTrue("Pending widget is not present",
                 widget != null);
     }
diff --git a/tests/src/com/android/launcher3/ui/widget/TaplRequestPinItemTest.java b/tests/src/com/android/launcher3/ui/widget/TaplRequestPinItemTest.java
index 74047f0..fe3b2ee 100644
--- a/tests/src/com/android/launcher3/ui/widget/TaplRequestPinItemTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/TaplRequestPinItemTest.java
@@ -169,8 +169,7 @@
 
         // Go back to home
         mLauncher.goHome();
-        Wait.atMost("", new ItemSearchCondition(itemMatcher), DEFAULT_ACTIVITY_TIMEOUT,
-                mLauncher);
+        Wait.atMost("", new ItemSearchCondition(itemMatcher), mLauncher);
     }
 
     /**
@@ -187,7 +186,7 @@
         @Override
         public boolean isTrue() throws Throwable {
             return mMainThreadExecutor.submit(() -> {
-                Launcher l = Launcher.ACTIVITY_TRACKER.getCreatedActivity();
+                Launcher l = Launcher.ACTIVITY_TRACKER.getCreatedContext();
                 return l != null && l.getWorkspace().getFirstMatch(mOp) != null;
             }).get();
         }
diff --git a/tests/src/com/android/launcher3/ui/workspace/TaplThemeIconsTest.java b/tests/src/com/android/launcher3/ui/workspace/TaplThemeIconsTest.java
deleted file mode 100644
index a148744..0000000
--- a/tests/src/com/android/launcher3/ui/workspace/TaplThemeIconsTest.java
+++ /dev/null
@@ -1,183 +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.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;
-import static org.junit.Assert.assertTrue;
-
-import android.content.ContentProviderClient;
-import android.content.ContentResolver;
-import android.content.ContentValues;
-import android.net.Uri;
-import android.view.View;
-import android.view.ViewGroup;
-
-import androidx.test.filters.LargeTest;
-
-import com.android.launcher3.BubbleTextView;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.icons.ThemedIconDrawable;
-import com.android.launcher3.tapl.HomeAllApps;
-import com.android.launcher3.tapl.HomeAppIcon;
-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;
-
-import java.util.ArrayDeque;
-import java.util.Queue;
-
-/**
- * Tests for theme icon support in Launcher
- *
- * Note running these tests will clear the workspace on the device.
- */
-@LargeTest
-public class TaplThemeIconsTest extends AbstractLauncherUiTest<Launcher> {
-
-    private static final String APP_NAME = "IconThemedActivity";
-    private static final String SHORTCUT_NAME = "Shortcut 1";
-
-    @Test
-    public void testIconWithoutTheme() throws Exception {
-        setThemeEnabled(false);
-        initialize(this);
-
-        HomeAllApps allApps = mLauncher.getWorkspace().switchToAllApps();
-        allApps.freeze();
-
-        try {
-            HomeAppIcon icon = allApps.getAppIcon(APP_NAME);
-            executeOnLauncher(l -> verifyIconTheme(APP_NAME, l.getAppsView(), false));
-            icon.dragToWorkspace(false, false);
-            executeOnLauncher(l -> verifyIconTheme(APP_NAME, l.getWorkspace(), false));
-        } finally {
-            allApps.unfreeze();
-        }
-    }
-
-    @Test
-    public void testShortcutIconWithoutTheme() throws Exception {
-        setThemeEnabled(false);
-        initialize(this);
-
-        HomeAllApps allApps = mLauncher.getWorkspace().switchToAllApps();
-        allApps.freeze();
-
-        try {
-            HomeAppIcon icon = allApps.getAppIcon(TEST_APP_NAME);
-            HomeAppIconMenuItem shortcutItem =
-                    (HomeAppIconMenuItem) icon.openDeepShortcutMenu().getMenuItem(SHORTCUT_NAME);
-            shortcutItem.dragToWorkspace(false, false);
-            executeOnLauncher(l -> verifyIconTheme(SHORTCUT_NAME, l.getWorkspace(), false));
-        } finally {
-            allApps.unfreeze();
-        }
-    }
-
-    @Test
-    public void testIconWithTheme() throws Exception {
-        setThemeEnabled(true);
-        initialize(this);
-
-        HomeAllApps allApps = mLauncher.getWorkspace().switchToAllApps();
-        allApps.freeze();
-
-        try {
-            HomeAppIcon icon = allApps.getAppIcon(APP_NAME);
-            executeOnLauncher(l -> verifyIconTheme(APP_NAME, l.getAppsView(), false));
-            icon.dragToWorkspace(false, false);
-            executeOnLauncher(l -> verifyIconTheme(APP_NAME, l.getWorkspace(), true));
-        } finally {
-            allApps.unfreeze();
-        }
-    }
-
-    @Test
-    @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/350557998
-    @ScreenRecordRule.ScreenRecord // b/350557998
-    public void testShortcutIconWithTheme() throws Exception {
-        setThemeEnabled(true);
-        initialize(this);
-
-        HomeAllApps allApps = mLauncher.getWorkspace().switchToAllApps();
-        allApps.freeze();
-
-        try {
-            HomeAppIcon icon = allApps.getAppIcon(TEST_APP_NAME);
-            HomeAppIconMenuItem shortcutItem =
-                    (HomeAppIconMenuItem) icon.openDeepShortcutMenu().getMenuItem(SHORTCUT_NAME);
-            shortcutItem.dragToWorkspace(false, false);
-            executeOnLauncher(l -> verifyIconTheme(SHORTCUT_NAME, l.getWorkspace(), true));
-        } finally {
-            allApps.unfreeze();
-        }
-    }
-
-    private void verifyIconTheme(String title, ViewGroup parent, boolean isThemed) {
-        // Wait for Launcher model to be completed
-        try {
-            Executors.MODEL_EXECUTOR.submit(() -> { }).get();
-        } catch (Exception e) {
-            throw new RuntimeException(e);
-        }
-
-        // Find the app icon
-        Queue<View> viewQueue = new ArrayDeque<>();
-        viewQueue.add(parent);
-        BubbleTextView icon = null;
-        while (!viewQueue.isEmpty()) {
-            View view = viewQueue.poll();
-            if (view instanceof ViewGroup) {
-                parent = (ViewGroup) view;
-                for (int i = parent.getChildCount() - 1; i >= 0; i--) {
-                    viewQueue.add(parent.getChildAt(i));
-                }
-            } else if (view instanceof BubbleTextView btv) {
-                if (title.equals(btv.getContentDescription().toString())) {
-                    icon = btv;
-                    break;
-                }
-            }
-        }
-
-        assertNotNull(icon.getIcon());
-        assertEquals(isThemed, icon.getIcon() instanceof ThemedIconDrawable);
-    }
-
-    private void setThemeEnabled(boolean isEnabled) throws Exception {
-        Uri uri = new Uri.Builder()
-                .scheme(ContentResolver.SCHEME_CONTENT)
-                .authority(mTargetPackage + ".grid_control")
-                .appendPath("set_icon_themed")
-                .build();
-        ContentValues values = new ContentValues();
-        values.put("boolean_value", isEnabled);
-        try (ContentProviderClient client = mTargetContext.getContentResolver()
-                .acquireContentProviderClient(uri)) {
-            int result = client.update(uri, values, null);
-            assertTrue(result > 0);
-        }
-    }
-}
diff --git a/tests/src/com/android/launcher3/ui/workspace/TaplWorkspaceTest.java b/tests/src/com/android/launcher3/ui/workspace/TaplWorkspaceTest.java
index 490cff2..237f2a9 100644
--- a/tests/src/com/android/launcher3/ui/workspace/TaplWorkspaceTest.java
+++ b/tests/src/com/android/launcher3/ui/workspace/TaplWorkspaceTest.java
@@ -32,7 +32,6 @@
 import com.android.launcher3.util.rule.ScreenRecordRule.ScreenRecord;
 
 import org.junit.After;
-import org.junit.Before;
 import org.junit.Test;
 
 /**
@@ -50,12 +49,6 @@
         return launcher.getWorkspace().getCurrentPage();
     }
 
-    @Before
-    public void setUp() throws Exception {
-        super.setUp();
-        initialize(this);
-    }
-
     @After
     public void tearDown() throws Exception {
         if (mLauncherLayout != null) {
diff --git a/tests/src/com/android/launcher3/ui/workspace/ThemeIconsTest.java b/tests/src/com/android/launcher3/ui/workspace/ThemeIconsTest.java
new file mode 100644
index 0000000..cfc0a6b
--- /dev/null
+++ b/tests/src/com/android/launcher3/ui/workspace/ThemeIconsTest.java
@@ -0,0 +1,203 @@
+/*
+ * 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.ui.workspace;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static com.android.launcher3.AbstractFloatingView.TYPE_ACTION_POPUP;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.util.TestConstants.AppNames.TEST_APP_NAME;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.content.ContentProviderClient;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.net.Uri;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.test.filters.LargeTest;
+import androidx.test.uiautomator.UiDevice;
+
+import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.BubbleTextView;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.allapps.AllAppsRecyclerView;
+import com.android.launcher3.celllayout.FavoriteItemsTransaction;
+import com.android.launcher3.icons.ThemedIconDrawable;
+import com.android.launcher3.popup.ArrowPopup;
+import com.android.launcher3.util.BaseLauncherActivityTest;
+import com.android.launcher3.util.Executors;
+import com.android.launcher3.util.TestUtil;
+
+import org.junit.Test;
+
+import java.util.ArrayDeque;
+import java.util.Queue;
+
+/**
+ * Tests for theme icon support in Launcher
+ *
+ * Note running these tests will clear the workspace on the device.
+ */
+@LargeTest
+public class ThemeIconsTest extends BaseLauncherActivityTest<Launcher> {
+
+    private static final String APP_NAME = "IconThemedActivity";
+    private static final String SHORTCUT_NAME = "Shortcut 1";
+
+    @Test
+    public void testIconWithoutTheme() throws Exception {
+        setThemeEnabled(false);
+        new FavoriteItemsTransaction(targetContext()).commit();
+        loadLauncherSync();
+        goToState(LauncherState.ALL_APPS);
+        freezeAllApps();
+
+        scrollToAppIcon(APP_NAME);
+        BubbleTextView btv = getFromLauncher(
+                l -> verifyIconTheme(APP_NAME, l.getAppsView(), false));
+        addToWorkspace(btv);
+        executeOnLauncher(l -> verifyIconTheme(APP_NAME, l.getWorkspace(), false));
+    }
+
+    @Test
+    public void testShortcutIconWithoutTheme() throws Exception {
+        setThemeEnabled(false);
+        new FavoriteItemsTransaction(targetContext()).commit();
+        loadLauncherSync();
+        goToState(LauncherState.ALL_APPS);
+        freezeAllApps();
+
+        scrollToAppIcon(TEST_APP_NAME);
+        BubbleTextView btv = getFromLauncher(l -> findBtv(TEST_APP_NAME, l.getAppsView()));
+        TestUtil.runOnExecutorSync(MAIN_EXECUTOR, btv::performLongClick);
+
+        BubbleTextView menuItem = getOnceNotNull("Popup menu not open", l ->
+                (AbstractFloatingView.getOpenView(l, TYPE_ACTION_POPUP) instanceof ArrowPopup ap)
+                        ? findBtv(SHORTCUT_NAME, ap) : null);
+        addToWorkspace(menuItem);
+        executeOnLauncher(l -> verifyIconTheme(SHORTCUT_NAME, l.getWorkspace(), false));
+    }
+
+    @Test
+    public void testIconWithTheme() throws Exception {
+        setThemeEnabled(true);
+        new FavoriteItemsTransaction(targetContext()).commit();
+        loadLauncherSync();
+        goToState(LauncherState.ALL_APPS);
+        freezeAllApps();
+
+        scrollToAppIcon(APP_NAME);
+        BubbleTextView btv = getFromLauncher(l ->
+                verifyIconTheme(APP_NAME, l.getAppsView(), false));
+        addToWorkspace(btv);
+        executeOnLauncher(l -> verifyIconTheme(APP_NAME, l.getWorkspace(), true));
+    }
+
+    @Test
+    public void testShortcutIconWithTheme() throws Exception {
+        setThemeEnabled(true);
+        loadLauncherSync();
+        goToState(LauncherState.ALL_APPS);
+        freezeAllApps();
+
+        scrollToAppIcon(TEST_APP_NAME);
+        BubbleTextView btv = getFromLauncher(l -> findBtv(TEST_APP_NAME, l.getAppsView()));
+        TestUtil.runOnExecutorSync(MAIN_EXECUTOR, btv::performLongClick);
+
+        BubbleTextView menuItem = getOnceNotNull("Popup menu not open", l ->
+                (AbstractFloatingView.getOpenView(l, TYPE_ACTION_POPUP) instanceof ArrowPopup ap)
+                        ? findBtv(SHORTCUT_NAME, ap) : null);
+        addToWorkspace(menuItem);
+        executeOnLauncher(l -> verifyIconTheme(SHORTCUT_NAME, l.getWorkspace(), true));
+    }
+
+    private BubbleTextView findBtv(String title, ViewGroup parent) {
+        // Wait for Launcher model to be completed
+        try {
+            Executors.MODEL_EXECUTOR.submit(() -> { }).get();
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+
+        // Find the app icon
+        Queue<View> viewQueue = new ArrayDeque<>();
+        viewQueue.add(parent);
+        BubbleTextView icon = null;
+        while (!viewQueue.isEmpty()) {
+            View view = viewQueue.poll();
+            if (view instanceof ViewGroup) {
+                parent = (ViewGroup) view;
+                for (int i = parent.getChildCount() - 1; i >= 0; i--) {
+                    viewQueue.add(parent.getChildAt(i));
+                }
+            } else if (view instanceof BubbleTextView btv) {
+                if (btv.getContentDescription() != null
+                        && title.equals(btv.getContentDescription().toString())) {
+                    icon = btv;
+                    break;
+                }
+            }
+        }
+        return icon;
+    }
+
+    private BubbleTextView verifyIconTheme(String title, ViewGroup parent, boolean isThemed) {
+        BubbleTextView icon = findBtv(title, parent);
+        assertNotNull(icon.getIcon());
+        assertEquals(isThemed, icon.getIcon() instanceof ThemedIconDrawable);
+        return icon;
+    }
+
+    private void setThemeEnabled(boolean isEnabled) throws Exception {
+        Uri uri = new Uri.Builder()
+                .scheme(ContentResolver.SCHEME_CONTENT)
+                .authority(targetContext().getPackageName() + ".grid_control")
+                .appendPath("set_icon_themed")
+                .build();
+        ContentValues values = new ContentValues();
+        values.put("boolean_value", isEnabled);
+        try (ContentProviderClient client = targetContext().getContentResolver()
+                .acquireContentProviderClient(uri)) {
+            int result = client.update(uri, values, null);
+            assertTrue(result > 0);
+        }
+    }
+
+    private void scrollToAppIcon(String appName) {
+        executeOnLauncher(l -> {
+            l.hideKeyboard();
+            AllAppsRecyclerView rv = l.getAppsView().getActiveRecyclerView();
+            int pos = rv.getApps().getAdapterItems().indexOf(rv.getApps().getAdapterItems().stream()
+                    .filter(i -> i.itemInfo != null && appName.equals(i.itemInfo.title.toString()))
+                    .findFirst()
+                    .get());
+            rv.getLayoutManager().scrollToPosition(pos);
+        });
+    }
+
+    private void addToWorkspace(View btv) {
+        TestUtil.runOnExecutorSync(MAIN_EXECUTOR, () ->
+                btv.getAccessibilityDelegate().performAccessibilityAction(
+                        btv, com.android.launcher3.R.id.action_add_to_workspace, null));
+        UiDevice.getInstance(getInstrumentation()).waitForIdle();
+    }
+}
diff --git a/tests/src/com/android/launcher3/util/BaseLauncherActivityTest.kt b/tests/src/com/android/launcher3/util/BaseLauncherActivityTest.kt
new file mode 100644
index 0000000..476e497
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/BaseLauncherActivityTest.kt
@@ -0,0 +1,153 @@
+/*
+ * 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.os.SystemClock
+import android.view.InputDevice
+import android.view.KeyCharacterMap
+import android.view.KeyEvent
+import android.view.MotionEvent
+import androidx.lifecycle.Lifecycle.State.RESUMED
+import androidx.test.core.app.ActivityScenario
+import androidx.test.core.app.ActivityScenario.ActivityAction
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import androidx.test.uiautomator.UiDevice
+import com.android.launcher3.Launcher
+import com.android.launcher3.LauncherAppState
+import com.android.launcher3.LauncherState
+import com.android.launcher3.allapps.AllAppsStore.DEFER_UPDATES_TEST
+import com.android.launcher3.tapl.TestHelpers
+import com.android.launcher3.util.ModelTestExtensions.loadModelSync
+import com.android.launcher3.util.Wait.atMost
+import java.util.function.Function
+import java.util.function.Supplier
+import org.junit.After
+
+/**
+ * Base class for tests which use Launcher activity with some utility methods.
+ *
+ * This should instead be a rule, but is kept as a base class for easier migration from TAPL
+ */
+open class BaseLauncherActivityTest<LAUNCHER_TYPE : Launcher> {
+
+    private var currentScenario: ActivityScenario<LAUNCHER_TYPE>? = null
+
+    val scenario: ActivityScenario<LAUNCHER_TYPE>
+        get() =
+            currentScenario
+                ?: ActivityScenario.launch<LAUNCHER_TYPE>(
+                        TestHelpers.getHomeIntentInPackage(targetContext()),
+                        null,
+                    )
+                    .also { currentScenario = it }
+
+    @After
+    fun closeCurrentActivity() {
+        currentScenario?.close()
+        currentScenario = null
+    }
+
+    protected fun loadLauncherSync() {
+        LauncherAppState.getInstance(targetContext()).model.loadModelSync()
+        scenario.moveToState(RESUMED)
+    }
+
+    protected fun targetContext(): Context = getInstrumentation().targetContext
+
+    protected fun goToState(state: LauncherState) {
+        executeOnLauncher { it.stateManager.goToState(state, 0) }
+        UiDevice.getInstance(getInstrumentation()).waitForIdle()
+    }
+
+    protected fun executeOnLauncher(f: ActivityAction<LAUNCHER_TYPE>) = scenario.onActivity(f)
+
+    protected fun <T> getFromLauncher(f: Function<in LAUNCHER_TYPE, out T?>): T? {
+        var result: T? = null
+        executeOnLauncher { result = f.apply(it) }
+        return result
+    }
+
+    protected fun isInState(state: Supplier<LauncherState>): Boolean =
+        getFromLauncher { it.stateManager.state == state.get() }!!
+
+    protected fun waitForState(message: String, state: Supplier<LauncherState>) =
+        waitForLauncherCondition(message) { it.stateManager.currentStableState === state.get() }
+
+    protected fun waitForLauncherCondition(
+        message: String,
+        condition: Function<LAUNCHER_TYPE, Boolean>,
+    ) = atMost(message, { getFromLauncher(condition)!! })
+
+    protected fun waitForLauncherCondition(
+        message: String,
+        condition: Function<LAUNCHER_TYPE, Boolean>,
+        timeout: Long,
+    ) = atMost(message, { getFromLauncher(condition)!! }, null, timeout)
+
+    protected fun <T> getOnceNotNull(message: String, f: Function<LAUNCHER_TYPE, T?>): T? {
+        var output: T? = null
+        atMost(
+            message,
+            {
+                val fromLauncher = getFromLauncher<T>(f)
+                output = fromLauncher
+                fromLauncher != null
+            },
+        )
+        return output
+    }
+
+    protected fun getAllAppsScroll(launcher: LAUNCHER_TYPE) =
+        launcher.appsView.activeRecyclerView.computeVerticalScrollOffset()
+
+    @JvmOverloads
+    protected fun injectKeyEvent(keyCode: Int, actionDown: Boolean, metaState: Int = 0) {
+        val eventTime = SystemClock.uptimeMillis()
+        val event =
+            KeyEvent.obtain(
+                eventTime,
+                eventTime,
+                if (actionDown) KeyEvent.ACTION_DOWN else MotionEvent.ACTION_UP,
+                keyCode,
+                /* repeat= */ 0,
+                metaState,
+                KeyCharacterMap.VIRTUAL_KEYBOARD,
+                /* scancode= */ 0,
+                /* flags= */ 0,
+                InputDevice.SOURCE_KEYBOARD,
+                /* characters =*/ null,
+            )
+        executeOnLauncher { it.dispatchKeyEvent(event) }
+        event.recycle()
+    }
+
+    fun startAppFast(packageName: String) {
+        val intent = targetContext().packageManager.getLaunchIntentForPackage(packageName)!!
+        intent.addCategory(Intent.CATEGORY_LAUNCHER)
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
+        targetContext().startActivity(intent)
+        UiDevice.getInstance(getInstrumentation()).waitForIdle()
+    }
+
+    fun freezeAllApps() = executeOnLauncher {
+        it.appsView.appsStore.enableDeferUpdates(DEFER_UPDATES_TEST)
+    }
+
+    fun executeShellCommand(cmd: String) =
+        UiDevice.getInstance(getInstrumentation()).executeShellCommand(cmd)
+}
diff --git a/tests/src/com/android/launcher3/util/RoboApiWrapper.kt b/tests/src/com/android/launcher3/util/RoboApiWrapper.kt
index 583652d..7f74e56 100644
--- a/tests/src/com/android/launcher3/util/RoboApiWrapper.kt
+++ b/tests/src/com/android/launcher3/util/RoboApiWrapper.kt
@@ -24,12 +24,10 @@
 
 object RoboApiWrapper {
 
-    fun initialize() {}
-
     fun registerInputStream(
         contentResolver: ContentResolver,
         uri: Uri,
-        inputStreamSupplier: Supplier<InputStream>
+        inputStreamSupplier: Supplier<InputStream>,
     ) {}
 
     fun waitForLooperSync(looper: Looper) {}
diff --git a/tests/src/com/android/launcher3/util/Wait.java b/tests/src/com/android/launcher3/util/Wait.java
deleted file mode 100644
index 50bc32e..0000000
--- a/tests/src/com/android/launcher3/util/Wait.java
+++ /dev/null
@@ -1,66 +0,0 @@
-package com.android.launcher3.util;
-
-import android.os.SystemClock;
-import android.util.Log;
-
-import com.android.launcher3.tapl.LauncherInstrumentation;
-
-import org.junit.Assert;
-
-import java.util.function.Supplier;
-
-/**
- * A utility class for waiting for a condition to be true.
- */
-public class Wait {
-
-    private static final long DEFAULT_SLEEP_MS = 200;
-
-    public static void atMost(String message, Condition condition, long timeout,
-            LauncherInstrumentation launcher) {
-        atMost(() -> message, condition, timeout, DEFAULT_SLEEP_MS, launcher);
-    }
-
-    public static void atMost(Supplier<String> message, Condition condition, long timeout,
-            LauncherInstrumentation launcher) {
-        atMost(message, condition, timeout, DEFAULT_SLEEP_MS, launcher);
-    }
-
-    public static void atMost(Supplier<String> message, Condition condition, long timeout,
-            long sleepMillis,
-            LauncherInstrumentation launcher) {
-        final long startTime = SystemClock.uptimeMillis();
-        long endTime = startTime + timeout;
-        Log.d("Wait", "atMost: " + startTime + " - " + endTime);
-        while (SystemClock.uptimeMillis() < endTime) {
-            try {
-                if (condition.isTrue()) {
-                    return;
-                }
-            } catch (Throwable t) {
-                throw new RuntimeException(t);
-            }
-            SystemClock.sleep(sleepMillis);
-        }
-
-        // Check once more before returning false.
-        try {
-            if (condition.isTrue()) {
-                return;
-            }
-        } catch (Throwable t) {
-            throw new RuntimeException(t);
-        }
-        Log.d("Wait", "atMost: timed out: " + SystemClock.uptimeMillis());
-        launcher.checkForAnomaly(false, false);
-        Assert.fail(message.get());
-    }
-
-    /**
-     * Interface representing a generic condition
-     */
-    public interface Condition {
-
-        boolean isTrue() throws Throwable;
-    }
-}
diff --git a/tests/src/com/android/launcher3/util/Wait.kt b/tests/src/com/android/launcher3/util/Wait.kt
new file mode 100644
index 0000000..1e5af54
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/Wait.kt
@@ -0,0 +1,79 @@
+/*
+ * 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.os.SystemClock
+import android.util.Log
+import com.android.launcher3.tapl.LauncherInstrumentation
+import java.util.function.Supplier
+import org.junit.Assert
+
+/** A utility class for waiting for a condition to be true. */
+object Wait {
+    private const val DEFAULT_SLEEP_MS: Long = 200
+
+    @JvmStatic
+    @JvmOverloads
+    fun atMost(
+        message: String,
+        condition: Condition,
+        launcherInstrumentation: LauncherInstrumentation? = null,
+        timeout: Long = TestUtil.DEFAULT_UI_TIMEOUT,
+    ) {
+        atMost({ message }, condition, launcherInstrumentation, timeout)
+    }
+
+    @JvmStatic
+    @JvmOverloads
+    fun atMost(
+        message: Supplier<String>,
+        condition: Condition,
+        launcherInstrumentation: LauncherInstrumentation? = null,
+        timeout: Long = TestUtil.DEFAULT_UI_TIMEOUT,
+    ) {
+        val startTime = SystemClock.uptimeMillis()
+        val endTime = startTime + timeout
+        Log.d("Wait", "atMost: $startTime - $endTime")
+        while (SystemClock.uptimeMillis() < endTime) {
+            try {
+                if (condition.isTrue()) {
+                    return
+                }
+            } catch (t: Throwable) {
+                throw RuntimeException(t)
+            }
+            SystemClock.sleep(DEFAULT_SLEEP_MS)
+        }
+
+        // Check once more before returning false.
+        try {
+            if (condition.isTrue()) {
+                return
+            }
+        } catch (t: Throwable) {
+            throw RuntimeException(t)
+        }
+        Log.d("Wait", "atMost: timed out: " + SystemClock.uptimeMillis())
+        launcherInstrumentation?.checkForAnomaly(false, false)
+        Assert.fail(message.get())
+    }
+
+    /** Interface representing a generic condition */
+    fun interface Condition {
+
+        @Throws(Throwable::class) fun isTrue(): Boolean
+    }
+}
diff --git a/tests/src/com/android/launcher3/util/rule/ExtendedLongPressTimeoutRule.java b/tests/src/com/android/launcher3/util/rule/ExtendedLongPressTimeoutRule.java
index 702988c..8a9ff3e 100644
--- a/tests/src/com/android/launcher3/util/rule/ExtendedLongPressTimeoutRule.java
+++ b/tests/src/com/android/launcher3/util/rule/ExtendedLongPressTimeoutRule.java
@@ -16,6 +16,9 @@
 
 package com.android.launcher3.util.rule;
 
+import static com.android.launcher3.util.TestUtil.grantWriteSecurePermission;
+
+import android.app.Instrumentation;
 import android.content.ContentResolver;
 import android.provider.Settings;
 import android.util.Log;
@@ -51,6 +54,7 @@
                 try {
                     Log.d(TAG, "In try-block: Setting long press timeout from "
                             + prevLongPressTimeout + "ms to " + newLongPressTimeout + "ms");
+                    grantWriteSecurePermission();
                     Settings.Secure.putInt(
                             contentResolver,
                             Settings.Secure.LONG_PRESS_TIMEOUT,
@@ -63,6 +67,7 @@
                 } finally {
                     Log.d(TAG, "In finally-block: resetting long press timeout to "
                             + prevLongPressTimeout + "ms");
+                    grantWriteSecurePermission();
                     Settings.Secure.putInt(
                             contentResolver,
                             Settings.Secure.LONG_PRESS_TIMEOUT,
diff --git a/tests/src_deviceless/com/android/launcher3/util/RoboApiWrapper.kt b/tests/src_deviceless/com/android/launcher3/util/RoboApiWrapper.kt
index 9232268..a2b8303 100644
--- a/tests/src_deviceless/com/android/launcher3/util/RoboApiWrapper.kt
+++ b/tests/src_deviceless/com/android/launcher3/util/RoboApiWrapper.kt
@@ -16,70 +16,19 @@
 
 package com.android.launcher3.util
 
-import android.content.ComponentName
 import android.content.ContentResolver
-import android.content.Intent
-import android.content.IntentFilter
-import android.content.pm.ApplicationInfo
-import android.content.pm.LauncherActivityInfo
-import android.content.pm.LauncherApps
 import android.net.Uri
 import android.os.Looper
-import android.os.Process
-import androidx.test.platform.app.InstrumentationRegistry
 import java.io.InputStream
 import java.util.function.Supplier
-import org.mockito.Mockito
-import org.mockito.kotlin.whenever
-import org.robolectric.RuntimeEnvironment
 import org.robolectric.Shadows
 
 object RoboApiWrapper {
 
-    fun initialize() {
-        Shadows.shadowOf(
-                RuntimeEnvironment.getApplication().getSystemService(LauncherApps::class.java)
-            )
-            .addEnabledPackage(
-                Process.myUserHandle(),
-                InstrumentationRegistry.getInstrumentation().context.packageName
-            )
-        LauncherModelHelper.ACTIVITY_LIST.forEach {
-            installApp(ComponentName(InstrumentationRegistry.getInstrumentation().context, it))
-        }
-    }
-
-    private fun installApp(componentName: ComponentName) {
-        val app = RuntimeEnvironment.getApplication()
-        val user = Process.myUserHandle()
-
-        val pm = Shadows.shadowOf(app.packageManager)
-        val ai = pm.addActivityIfNotPresent(componentName)
-        pm.addIntentFilterForActivity(
-            componentName,
-            IntentFilter(Intent.ACTION_MAIN).apply { addCategory(Intent.CATEGORY_LAUNCHER) }
-        )
-
-        val li = Mockito.mock(LauncherActivityInfo::class.java)
-        val appInfo = ApplicationInfo().apply { flags = 0 }
-        Mockito.doReturn(ai).whenever(li).activityInfo
-        Mockito.doReturn(appInfo).whenever(li).applicationInfo
-        Mockito.doReturn(user).whenever(li).user
-        Mockito.doReturn(1f).whenever(li).loadingProgress
-        Mockito.doReturn(componentName).whenever(li).componentName
-
-        Shadows.shadowOf(app.getSystemService(LauncherApps::class.java)).apply {
-            addActivity(user, li)
-            addEnabledPackage(user, componentName.packageName)
-            setActivityEnabled(user, componentName)
-            addApplicationInfo(user, componentName.packageName, ai.applicationInfo)
-        }
-    }
-
     fun registerInputStream(
         contentResolver: ContentResolver,
         uri: Uri,
-        inputStreamSupplier: Supplier<InputStream>
+        inputStreamSupplier: Supplier<InputStream>,
     ) {
         Shadows.shadowOf(contentResolver).registerInputStreamSupplier(uri, inputStreamSupplier)
     }
diff --git a/tests/tapl/com/android/launcher3/tapl/AppIcon.java b/tests/tapl/com/android/launcher3/tapl/AppIcon.java
index 02a862d..9294755 100644
--- a/tests/tapl/com/android/launcher3/tapl/AppIcon.java
+++ b/tests/tapl/com/android/launcher3/tapl/AppIcon.java
@@ -16,10 +16,8 @@
 
 package com.android.launcher3.tapl;
 
-import static com.android.launcher3.testing.shared.TestProtocol.TEST_DRAG_APP_ICON_TO_MULTIPLE_WORKSPACES_FAILURE;
 
 import android.graphics.Point;
-import android.util.Log;
 import android.widget.TextView;
 
 import androidx.annotation.NonNull;
@@ -99,8 +97,6 @@
 
     @Override
     protected void waitForLongPressConfirmation() {
-        Log.d(TEST_DRAG_APP_ICON_TO_MULTIPLE_WORKSPACES_FAILURE,
-                "AppIcon.waitForLongPressConfirmation, resName: popupContainer");
         mLauncher.waitForLauncherObject("popup_container");
     }
 
diff --git a/tests/tapl/com/android/launcher3/tapl/Background.java b/tests/tapl/com/android/launcher3/tapl/Background.java
index b7ebfcd..512db39 100644
--- a/tests/tapl/com/android/launcher3/tapl/Background.java
+++ b/tests/tapl/com/android/launcher3/tapl/Background.java
@@ -29,6 +29,7 @@
 
 import com.android.launcher3.tapl.LauncherInstrumentation.NavigationModel;
 import com.android.launcher3.tapl.LauncherInstrumentation.TrackpadGestureType;
+import com.android.launcher3.tapl.OverviewTask.TaskViewType;
 import com.android.launcher3.testing.shared.TestProtocol;
 
 import java.util.List;
@@ -121,12 +122,31 @@
                         if (mLauncher.isTablet()) {
                             List<UiObject2> tasks = mLauncher.getDevice().findObjects(
                                     TASK_SELECTOR);
+
                             final int centerX = mLauncher.getDevice().getDisplayWidth() / 2;
-                            mLauncher.assertTrue(
-                                    "All tasks not to the left of the swiped task",
-                                    tasks.stream()
-                                            .allMatch(
-                                                    t -> t.getVisibleBounds().right < centerX));
+                            UiObject2 centerTask = tasks.stream()
+                                    .filter(t -> t.getVisibleCenter().x == centerX)
+                                    .findFirst()
+                                    .orElse(null);
+
+                            if (centerTask != null) {
+                                mLauncher.assertTrue(
+                                        "Task(s) found to the right of the swiped task",
+                                        tasks.stream()
+                                                .filter(t -> t != centerTask
+                                                        && OverviewTask.getType(t)
+                                                        != TaskViewType.DESKTOP)
+                                                .allMatch(t -> t.getVisibleBounds().right
+                                                        < centerTask.getVisibleBounds().left));
+                                mLauncher.assertTrue(
+                                        "DesktopTask(s) found to the left of the swiped task",
+                                        tasks.stream()
+                                                .filter(t -> t != centerTask
+                                                        && OverviewTask.getType(t)
+                                                        == TaskViewType.DESKTOP)
+                                                .allMatch(t -> t.getVisibleBounds().left
+                                                        > centerTask.getVisibleBounds().right));
+                            }
                         }
 
                     }
diff --git a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
index 0edcfea..b15afc1 100644
--- a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
+++ b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
@@ -369,7 +369,6 @@
         }
     }
 
-
     int getTaskCount() {
         return getTasks().size();
     }
@@ -441,7 +440,7 @@
                     "Not expecting an actions bar: device is tablet and task is not centered");
             return false;
         }
-        if (task.isTaskSplit() && (!mLauncher.isAppPairsEnabled() || !isTablet)) {
+        if (task.isGrouped() && (!mLauncher.isAppPairsEnabled() || !isTablet)) {
             testLogD(TAG, "Not expecting an actions bar: device is phone and task is split");
             // Overview actions aren't visible for split screen tasks, except for save app pair
             // button on tablets.
@@ -504,11 +503,11 @@
                 "want to assert overview actions view visibility="
                         + isActionsViewVisible()
                         + ", focused task is "
-                        + (task == null ? "null" : (task.isTaskSplit() ? "split" : "not split"))
+                        + (task == null ? "null" : (task.isGrouped() ? "split" : "not split"))
                 )) {
 
             if (isActionsViewVisible()) {
-                if (task.isTaskSplit()) {
+                if (task.isGrouped()) {
                     mLauncher.waitForOverviewObject("action_save_app_pair");
                 } else {
                     mLauncher.waitForOverviewObject("action_buttons");
@@ -537,6 +536,10 @@
         int focusedTaskHeight = focusTaskSize.height();
         for (UiObject2 task : taskViews) {
             OverviewTask overviewTask = new OverviewTask(mLauncher, task, this);
+            // Desktop tasks can't be focused tasks, but are the same size.
+            if (overviewTask.isDesktop()) {
+                continue;
+            }
             if (overviewTask.getVisibleHeight() == focusedTaskHeight) {
                 return overviewTask;
             }
diff --git a/tests/tapl/com/android/launcher3/tapl/KeyboardQuickSwitch.java b/tests/tapl/com/android/launcher3/tapl/KeyboardQuickSwitch.java
index 7ff55fe..7cb2614 100644
--- a/tests/tapl/com/android/launcher3/tapl/KeyboardQuickSwitch.java
+++ b/tests/tapl/com/android/launcher3/tapl/KeyboardQuickSwitch.java
@@ -163,6 +163,32 @@
     }
 
     /**
+     * Dismisses the Keyboard Quick Switch view by going home. After the Keyboard Quick Switch view
+     * gets hidden, it unpresses ALT key, which is generally used to keep the view visible.
+     */
+    public Workspace dismissByGoingHome() {
+        try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
+                "verifying keyboard quick switch view is shown")) {
+            mLauncher.waitForLauncherObject(KEYBOARD_QUICK_SWITCH_RES_ID);
+        }
+
+        mLauncher.goHome();
+
+        try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
+                "waiting for keyboard quick switch dismissal");
+             LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
+            mLauncher.waitUntilLauncherObjectGone(KEYBOARD_QUICK_SWITCH_RES_ID);
+        }
+
+        try (LauncherInstrumentation.Closable c2 = mLauncher.addContextLayer(
+                "get workspace after releasing ALT key")) {
+            mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_HOME_ALT_LEFT_UP);
+            mLauncher.unpressKeyCode(KeyEvent.KEYCODE_ALT_LEFT, 0);
+            return mLauncher.getWorkspace();
+        }
+    }
+
+    /**
      * Launches the currently-focused app task.
      * <p>
      * This method should only be used if the focused task is for a recent running app, otherwise
diff --git a/tests/tapl/com/android/launcher3/tapl/Launchable.java b/tests/tapl/com/android/launcher3/tapl/Launchable.java
index 9d3bc6e..c40e5a9 100644
--- a/tests/tapl/com/android/launcher3/tapl/Launchable.java
+++ b/tests/tapl/com/android/launcher3/tapl/Launchable.java
@@ -17,10 +17,8 @@
 package com.android.launcher3.tapl;
 
 import static com.android.launcher3.testing.shared.TestProtocol.SPRING_LOADED_STATE_ORDINAL;
-import static com.android.launcher3.testing.shared.TestProtocol.TEST_DRAG_APP_ICON_TO_MULTIPLE_WORKSPACES_FAILURE;
 
 import android.graphics.Point;
-import android.util.Log;
 import android.view.MotionEvent;
 
 import androidx.test.uiautomator.UiObject2;
@@ -115,10 +113,6 @@
                 iconCenter.y - getStartDragThreshold());
 
         if (runToSpringLoadedState) {
-            Log.d(TEST_DRAG_APP_ICON_TO_MULTIPLE_WORKSPACES_FAILURE,
-                    "Launchable.startDrag: actionName: long-pressing and triggering drag start"
-                            + " iconCenter: " + iconCenter + " dragStartCenter: "
-                            + dragStartCenter);
             mLauncher.runToState(() -> movePointerForStartDrag(
                             downTime,
                             iconCenter,
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index 78627e5..fac73d3 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -31,7 +31,6 @@
 import static com.android.launcher3.testing.shared.TestProtocol.NORMAL_STATE_ORDINAL;
 import static com.android.launcher3.testing.shared.TestProtocol.REQUEST_GET_SPLIT_SELECTION_ACTIVE;
 import static com.android.launcher3.testing.shared.TestProtocol.REQUEST_NUM_ALL_APPS_COLUMNS;
-import static com.android.launcher3.testing.shared.TestProtocol.TEST_DRAG_APP_ICON_TO_MULTIPLE_WORKSPACES_FAILURE;
 import static com.android.launcher3.testing.shared.TestProtocol.TEST_INFO_RESPONSE_FIELD;
 
 import android.app.ActivityManager;
@@ -1212,11 +1211,6 @@
                 log("Hierarchy before clicking home:");
                 dumpViewHierarchy();
                 action = "clicking home button";
-                Log.d(TEST_DRAG_APP_ICON_TO_MULTIPLE_WORKSPACES_FAILURE,
-                        "LauncherInstrumentation.goHome: isThreeFingerTrackpadGesture: "
-                                + isThreeFingerTrackpadGesture
-                                + "getNavigationModel() == NavigationModel.ZERO_BUTTON: " + (
-                                getNavigationModel() == NavigationModel.ZERO_BUTTON));
                 runToState(
                         getHomeButton()::click,
                         NORMAL_STATE_ORDINAL,
@@ -1567,8 +1561,6 @@
 
     @NonNull
     UiObject2 waitForLauncherObject(String resName) {
-        Log.d(TEST_DRAG_APP_ICON_TO_MULTIPLE_WORKSPACES_FAILURE,
-                "LauncherInstrumentation.waitForLauncherObject");
         return waitForObjectBySelector(getLauncherObjectSelector(resName));
     }
 
@@ -1598,16 +1590,12 @@
 
     @NonNull
     List<UiObject2> waitForObjectsBySelector(BySelector selector) {
-        Log.d(TEST_DRAG_APP_ICON_TO_MULTIPLE_WORKSPACES_FAILURE,
-                "LauncherInstrumentation.waitForObjectsBySelector");
         final List<UiObject2> objects = mDevice.wait(Until.findObjects(selector), WAIT_TIME_MS);
         assertNotNull("Can't find any view in Launcher, selector: " + selector, objects);
         return objects;
     }
 
     UiObject2 waitForObjectBySelector(BySelector selector) {
-        Log.d(TEST_DRAG_APP_ICON_TO_MULTIPLE_WORKSPACES_FAILURE,
-                "LauncherInstrumentation.waitForObjectBySelector");
         final UiObject2 object = mDevice.wait(Until.findObject(selector), WAIT_TIME_MS);
         assertNotNull("Can't find a view in Launcher, selector: " + selector, object);
         return object;
@@ -1650,9 +1638,6 @@
 
     void runToState(Runnable command, int expectedState, boolean requireEvent, String actionName) {
         if (requireEvent) {
-            Log.d(TEST_DRAG_APP_ICON_TO_MULTIPLE_WORKSPACES_FAILURE,
-                    "LauncherInstrumentation.runToState: command: " + command + " expectedState: "
-                            + expectedState + " actionName: " + actionName + "requireEvent: true");
             runToState(command, expectedState, actionName);
         } else {
             command.run();
@@ -1743,6 +1728,27 @@
         scrollDownByDistance(container, distance, appsListBottomPadding);
     }
 
+    /** Scrolls up by given distance within the container. */
+    void scrollUpByDistance(UiObject2 container, int distance) {
+        scrollUpByDistance(container, distance, 0);
+    }
+
+    /** Scrolls up by given distance within the container considering the given bottom padding. */
+    void scrollUpByDistance(UiObject2 container, int distance, int bottomPadding) {
+        final Rect containerRect = getVisibleBounds(container);
+        final int bottomGestureMarginInContainer = getBottomGestureMarginInContainer(container);
+        scroll(
+                container,
+                Direction.UP,
+                new Rect(
+                        0,
+                        containerRect.height() - bottomGestureMarginInContainer - distance,
+                        0,
+                        bottomGestureMarginInContainer + bottomPadding),
+                /* steps= */ 10,
+                /* slowDown= */ true);
+    }
+
     void scrollDownByDistance(UiObject2 container, int distance) {
         scrollDownByDistance(container, distance, 0);
     }
@@ -2052,15 +2058,11 @@
                     mPointerCount = 1;
                     pointerCount = mPointerCount;
                 }
-                Log.d(TEST_DRAG_APP_ICON_TO_MULTIPLE_WORKSPACES_FAILURE,
-                        "LauncherInstrumentation.sendPointer: ACTION_DOWN");
                 break;
             case MotionEvent.ACTION_UP:
                 if (hasTIS && gestureScope == GestureScope.EXPECT_PILFER) {
                     expectEvent(TestProtocol.SEQUENCE_PILFER, EVENT_PILFER_POINTERS);
                 }
-                Log.d(TEST_DRAG_APP_ICON_TO_MULTIPLE_WORKSPACES_FAILURE,
-                        "LauncherInstrumentation.sendPointer: ACTION_UP");
                 break;
             case MotionEvent.ACTION_POINTER_DOWN:
                 mPointerCount++;
diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
index 9a8d952..8512d73 100644
--- a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
+++ b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
@@ -16,9 +16,10 @@
 
 package com.android.launcher3.tapl;
 
-import static com.android.launcher3.tapl.OverviewTask.OverviewSplitTask.DEFAULT;
-import static com.android.launcher3.tapl.OverviewTask.OverviewSplitTask.SPLIT_BOTTOM_OR_RIGHT;
-import static com.android.launcher3.tapl.OverviewTask.OverviewSplitTask.SPLIT_TOP_OR_LEFT;
+import static com.android.launcher3.tapl.OverviewTask.OverviewTaskContainer.DEFAULT;
+import static com.android.launcher3.tapl.OverviewTask.OverviewTaskContainer.DESKTOP;
+import static com.android.launcher3.tapl.OverviewTask.OverviewTaskContainer.SPLIT_BOTTOM_OR_RIGHT;
+import static com.android.launcher3.tapl.OverviewTask.OverviewTaskContainer.SPLIT_TOP_OR_LEFT;
 
 import android.graphics.Rect;
 
@@ -56,7 +57,7 @@
         mLauncher.assertNotNull("task must not be null", task);
         mTask = task;
         mOverview = overview;
-        mType = getType();
+        mType = getType(task);
         verifyActiveContainer();
     }
 
@@ -69,11 +70,11 @@
      * divider between.
      */
     int getVisibleHeight() {
-        if (isTaskSplit()) {
+        if (isGrouped()) {
             return getCombinedSplitTaskHeight();
         }
 
-        UiObject2 taskSnapshot1 = findObjectInTask(DEFAULT.snapshotRes);
+        UiObject2 taskSnapshot1 = findObjectInTask((isDesktop() ? DESKTOP : DEFAULT).snapshotRes);
         return taskSnapshot1.getVisibleBounds().height();
     }
 
@@ -102,7 +103,7 @@
      * divider between.
      */
     int getVisibleWidth() {
-        if (isTaskSplit()) {
+        if (isGrouped()) {
             return getCombinedSplitTaskWidth();
         }
 
@@ -164,8 +165,11 @@
 
             dismissBySwipingUp();
 
+            long numNonDesktopTasks = mOverview.getCurrentTasksForTablet()
+                    .stream().filter(t -> !t.isDesktop()).count();
+
             try (LauncherInstrumentation.Closable c2 = mLauncher.addContextLayer("dismissed")) {
-                if (taskWasFocused) {
+                if (taskWasFocused && numNonDesktopTasks > 0) {
                     mLauncher.assertNotNull("No task became focused",
                             mOverview.getFocusedTaskForTablet());
                 }
@@ -256,7 +260,7 @@
 
     /** Taps the task menu of the split task. Returns the split task's menu object. */
     @NonNull
-    public OverviewTaskMenu tapMenu(OverviewSplitTask task) {
+    public OverviewTaskMenu tapMenu(OverviewTaskContainer task) {
         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
              LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
                      "want to tap the task menu")) {
@@ -270,10 +274,6 @@
         }
     }
 
-    boolean isTaskSplit() {
-        return findObjectInTask(SPLIT_BOTTOM_OR_RIGHT.snapshotRes) != null;
-    }
-
     private UiObject2 findObjectInTask(String resName) {
         return mTask.findObject(mLauncher.getOverviewObjectSelector(resName));
     }
@@ -285,8 +285,8 @@
      * TODO(b/342627272): remove Nullable support once the bug causing it to be null is fixed.
      */
     public boolean containsContentDescription(@Nullable String expected,
-            OverviewSplitTask overviewSplitTask) {
-        String actual = findObjectInTask(overviewSplitTask.snapshotRes).getContentDescription();
+            OverviewTaskContainer overviewTaskContainer) {
+        String actual = findObjectInTask(overviewTaskContainer.snapshotRes).getContentDescription();
         if (actual == null && expected == null) {
             return true;
         }
@@ -304,8 +304,12 @@
         return containsContentDescription(expected, DEFAULT);
     }
 
-    private TaskViewType getType() {
-        String resourceName = mTask.getResourceName();
+    /**
+     * Returns the TaskView type of the task. It will return whether the task is a single TaskView,
+     * a GroupedTaskView or a DesktopTaskView.
+     */
+    static TaskViewType getType(UiObject2 task) {
+        String resourceName = task.getResourceName();
         if (resourceName.endsWith("task_view_grouped")) {
             return TaskViewType.GROUPED;
         } else if (resourceName.endsWith("task_view_desktop")) {
@@ -315,27 +319,37 @@
         }
     }
 
+    boolean isGrouped() {
+        return mType == TaskViewType.GROUPED;
+    }
+
+    public boolean isDesktop() {
+        return mType == TaskViewType.DESKTOP;
+    }
+
     /**
-     * Enum used to specify  which task is retrieved when it is a split task.
+     * Enum used to specify which resource name should be used depending on the type of the task.
      */
-    public enum OverviewSplitTask {
+    public enum OverviewTaskContainer {
         // The main task when the task is not split.
         DEFAULT("snapshot", "icon"),
         // The first task in split task.
         SPLIT_TOP_OR_LEFT("snapshot", "icon"),
         // The second task in split task.
-        SPLIT_BOTTOM_OR_RIGHT("bottomright_snapshot", "bottomRight_icon");
+        SPLIT_BOTTOM_OR_RIGHT("bottomright_snapshot", "bottomRight_icon"),
+        // The desktop task.
+        DESKTOP("background", "icon");
 
         public final String snapshotRes;
         public final String iconAppRes;
 
-        OverviewSplitTask(String snapshotRes, String iconAppRes) {
+        OverviewTaskContainer(String snapshotRes, String iconAppRes) {
             this.snapshotRes = snapshotRes;
             this.iconAppRes = iconAppRes;
         }
     }
 
-    private enum TaskViewType {
+    enum TaskViewType {
         SINGLE,
         GROUPED,
         DESKTOP
diff --git a/tests/tapl/com/android/launcher3/tapl/Widgets.java b/tests/tapl/com/android/launcher3/tapl/Widgets.java
index 6387b05..ac2748e 100644
--- a/tests/tapl/com/android/launcher3/tapl/Widgets.java
+++ b/tests/tapl/com/android/launcher3/tapl/Widgets.java
@@ -19,6 +19,7 @@
 import static com.android.launcher3.tapl.LauncherInstrumentation.WAIT_TIME_MS;
 import static com.android.launcher3.tapl.LauncherInstrumentation.log;
 
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.graphics.Rect;
 
@@ -31,6 +32,7 @@
 import com.android.launcher3.testing.shared.TestProtocol;
 
 import java.util.Collection;
+import java.util.List;
 
 /**
  * All widgets container.
@@ -116,8 +118,8 @@
     }
 
     /** Get widget with supplied text. */
-    public Widget getWidget(String labelText) {
-        return getWidget(labelText, null);
+    public Widget getWidget(CharSequence labelText) {
+        return getWidget(labelText.toString(), null);
     }
 
     /** Get widget with supplied text and app package */
@@ -128,8 +130,10 @@
             final UiObject2 searchBar = findSearchBar();
             final int searchBarHeight = searchBar.getVisibleBounds().height();
             final UiObject2 fullWidgetsPicker = verifyActiveContainer();
-            mLauncher.assertTrue("Widgets container didn't become scrollable",
-                    fullWidgetsPicker.wait(Until.scrollable(true), WAIT_TIME_MS));
+
+            // Widget picker may not be scrollable if there are few items. Instead of waiting on
+            // picker being scrollable, we wait on widget headers to be available.
+            waitForWidgetListItems(fullWidgetsPicker);
 
             final UiObject2 widgetsContainer =
                     findTestAppWidgetsTableContainer(testAppWidgetPackage);
@@ -176,6 +180,13 @@
         }
     }
 
+    private void waitForWidgetListItems(UiObject2 fullWidgetsPicker) {
+        List<UiObject2> headers = fullWidgetsPicker.wait(Until.findObjects(
+                By.res(mLauncher.getLauncherPackageName(), "widgets_list_header")), WAIT_TIME_MS);
+        mLauncher.assertTrue("Widgets list is not available",
+                headers != null && !headers.isEmpty());
+    }
+
     private UiObject2 findSearchBar() {
         final BySelector searchBarContainerSelector = By.res(mLauncher.getLauncherPackageName(),
                 "search_and_recommendations_container");
@@ -199,19 +210,38 @@
                 "container");
 
         String packageName =  mLauncher.getContext().getPackageName();
+        String packageNameToFind =
+                (testAppWidgetPackage == null || testAppWidgetPackage.isEmpty()) ? packageName
+                        : testAppWidgetPackage;
+
         final BySelector targetAppSelector = By
                 .clazz("android.widget.TextView")
-                .text((testAppWidgetPackage == null || testAppWidgetPackage.isEmpty())
-                                ? packageName
-                                : testAppWidgetPackage);
+                .text(packageNameToFind);
+        final BySelector expandListButtonSelector =
+                By.res(mLauncher.getLauncherPackageName(), "widget_list_expand_button");
         final BySelector widgetsContainerSelector = By.res(mLauncher.getLauncherPackageName(),
                 "widgets_table");
 
         boolean hasHeaderExpanded = false;
+        // List was expanded by clicking "Show all" button.
+        boolean hasListExpanded = false;
+
         int scrollDistance = 0;
         for (int i = 0; i < SCROLL_ATTEMPTS; i++) {
             UiObject2 widgetPicker = mLauncher.waitForLauncherObject(widgetPickerSelector);
             UiObject2 widgetListView = verifyActiveContainer();
+
+            // Press "Show all" button if it exists. Otherwise, keep scrolling to
+            // find the header or show all button.
+            UiObject2 expandListButton =
+                    mLauncher.findObjectInContainer(widgetListView, expandListButtonSelector);
+            if (expandListButton != null) {
+                expandListButton.click();
+                hasListExpanded = true;
+                i = -1;
+                continue;
+            }
+
             UiObject2 header = mLauncher.waitForObjectInContainer(widgetListView,
                     headerSelector);
             // If a header is barely visible in the bottom edge of the screen, its height could be
@@ -222,6 +252,17 @@
             // Look for a header that has the test app name.
             UiObject2 headerTitle = mLauncher.findObjectInContainer(widgetListView,
                     targetAppSelector);
+
+            final UiObject2 searchBar = findSearchBar();
+            // If header's title is under or above search bar, let's not process the header yet,
+            // scroll a bit more to bring the header into visible area.
+            if (headerTitle != null
+                    && headerTitle.getVisibleCenter().y <= searchBar.getVisibleCenter().y) {
+                log("Test app's header is behind the searchbar, scrolling up");
+                mLauncher.scrollUpByDistance(widgetListView, scrollDistance);
+                continue;
+            }
+
             if (headerTitle != null) {
                 // If we find the header and it has not been expanded, let's click it to see the
                 // widgets list. Note that we wait until the header is out of the gesture region at
@@ -258,11 +299,24 @@
                     widgetPicker,
                     widgetsContainerSelector);
 
-            mLauncher.scrollDownByDistance(hasHeaderExpanded && rightPane != null
-                    ? rightPane
-                    : widgetListView, scrollDistance);
+            if (hasListExpanded && packageNameToFind.compareToIgnoreCase(
+                    getFirstHeaderTitle(widgetListView)) < 0) {
+                mLauncher.scrollUpByDistance(hasHeaderExpanded && rightPane != null
+                        ? rightPane
+                        : widgetListView, scrollDistance);
+            } else {
+                mLauncher.scrollDownByDistance(hasHeaderExpanded && rightPane != null
+                        ? rightPane
+                        : widgetListView, scrollDistance);
+            }
         }
 
         return null;
     }
+
+    @NonNull
+    private String getFirstHeaderTitle(UiObject2 widgetListView) {
+        UiObject2 firstHeader = mLauncher.getObjectsInContainer(widgetListView, "app_title").get(0);
+        return firstHeader != null ? firstHeader.getText() : "";
+    }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/Workspace.java b/tests/tapl/com/android/launcher3/tapl/Workspace.java
index 748d576..a29362f 100644
--- a/tests/tapl/com/android/launcher3/tapl/Workspace.java
+++ b/tests/tapl/com/android/launcher3/tapl/Workspace.java
@@ -25,8 +25,6 @@
 import static com.android.launcher3.testing.shared.TestProtocol.ALL_APPS_STATE_ORDINAL;
 import static com.android.launcher3.testing.shared.TestProtocol.NORMAL_STATE_ORDINAL;
 import static com.android.launcher3.testing.shared.TestProtocol.OVERVIEW_STATE_ORDINAL;
-import static com.android.launcher3.testing.shared.TestProtocol.TEST_DRAG_APP_ICON_TO_MULTIPLE_WORKSPACES_FAILURE;
-import static com.android.launcher3.testing.shared.TestProtocol.UIOBJECT_STALE_ELEMENT;
 
 import static junit.framework.TestCase.assertNotNull;
 import static junit.framework.TestCase.assertTrue;
@@ -34,7 +32,6 @@
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.os.SystemClock;
-import android.util.Log;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 
@@ -371,14 +368,9 @@
                 .collect(
                         Collectors.toMap(
                                 /* keyMapper= */ uiObject21 -> {
-                                    Log.d(UIOBJECT_STALE_ELEMENT, "keyText: "
-                                            + uiObject21.getText());
                                     return uiObject21.getText();
                                 },
                                 /* valueMapper= */ uiObject2 -> {
-                                    Log.d(UIOBJECT_STALE_ELEMENT, uiObject2.getText() +
-                                            " dispId" + uiObject2.getDisplayId() +
-                                            " parent" + uiObject2.getParent());
                                     return uiObject2.getVisibleCenter();
                                 },
                                 /* mergeFunction= */ (p1, p2) -> p1.x < p2.x ? p1 : p2));
@@ -646,8 +638,6 @@
         try (LauncherInstrumentation.Closable ignored = launcher.addContextLayer(
                 "want to drag icon to workspace")) {
             final long downTime = SystemClock.uptimeMillis();
-            Log.d(TEST_DRAG_APP_ICON_TO_MULTIPLE_WORKSPACES_FAILURE,
-                    "Workspace.dragIconToWorkspace: starting drag | downtime: " + downTime);
             Point dragStart = launchable.startDrag(
                     downTime,
                     expectLongClickEvents,
diff --git a/tests/tapl/com/android/launcher3/tapl/WorkspaceDragSource.java b/tests/tapl/com/android/launcher3/tapl/WorkspaceDragSource.java
index b42d43b..e5a2a2e 100644
--- a/tests/tapl/com/android/launcher3/tapl/WorkspaceDragSource.java
+++ b/tests/tapl/com/android/launcher3/tapl/WorkspaceDragSource.java
@@ -15,10 +15,7 @@
  */
 package com.android.launcher3.tapl;
 
-import static com.android.launcher3.testing.shared.TestProtocol.TEST_DRAG_APP_ICON_TO_MULTIPLE_WORKSPACES_FAILURE;
-
 import android.graphics.Point;
-import android.util.Log;
 
 import java.util.function.Supplier;
 
@@ -79,9 +76,6 @@
              LauncherInstrumentation.Closable c = launcher.addContextLayer(
                      String.format("want to drag the icon to cell(%d, %d)", cellX, cellY))) {
             final Supplier<Point> dest = () -> Workspace.getCellCenter(launcher, cellX, cellY);
-            Log.d(TEST_DRAG_APP_ICON_TO_MULTIPLE_WORKSPACES_FAILURE,
-                    "WorkspaceDragSource.dragToWorkspace: dragging icon to workspace | dest: "
-                            + dest.get());
             Workspace.dragIconToWorkspace(
                     launcher,
                     launchable,