[automerger skipped] Merge ab/AP4A.240925.013 into aosp-main-future am: 7b31be6dfa -s ours

am skip reason: Merged-In If0497fcc17bb0ec44294c4ea1163104b4d8be459 with SHA-1 7519b7c2e6 is already in history

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

Change-Id: Ib3ca50f17930dae3b394ef6c0632e351238ba1ab
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/Android.bp b/Android.bp
index 4e1c9d3..73d0fce 100644
--- a/Android.bp
+++ b/Android.bp
@@ -64,9 +64,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 +387,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 +471,7 @@
         "SystemUISharedLib",
         "SettingsLibSettingsTheme",
         "dagger2",
+        "protolog-group",
     ],
     manifest: "quickstep/AndroidManifest.xml",
     min_sdk_version: "current",
@@ -503,7 +570,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..4ff976d 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,93 @@
     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: "use_system_radius_for_app_widgets"
+   namespace: "launcher"
+   description: "Use system radius for enforced widget corners instead of a separate 16.dp value"
+   bug: "373351337"
+   metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
+
+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"
+}
+
+flag {
+    name: "enable_pinning_app_with_context_menu"
+    namespace: "launcher"
+    description: "Add options to pin/unpin to taskbar to app context menus."
+    bug: "375648361"
+}
\ 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..8c39585 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"
@@ -151,7 +152,7 @@
             android:showOnLockScreen="true"
             android:launchMode="singleTop"
             android:exported="true"
-            android:permission="android.permission.START_WIDGET_PICKER_ACTIVITY">
+            android:permission="${applicationId}.permission.START_WIDGET_PICKER_ACTIVITY">
             <intent-filter>
                 <action android:name="android.intent.action.PICK" />
                 <category android:name="android.intent.category.DEFAULT" />
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/desktop_mode_ic_taskbar_menu_manage_windows.xml b/quickstep/res/drawable/desktop_mode_ic_taskbar_menu_manage_windows.xml
new file mode 100644
index 0000000..7d912a2
--- /dev/null
+++ b/quickstep/res/drawable/desktop_mode_ic_taskbar_menu_manage_windows.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.
+  -->
+
+<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="@android:color/black" android:pathData="M160,880Q127,880 103.5,856.5Q80,833 80,800L80,440Q80,407 103.5,383.5Q127,360 160,360L240,360L240,160Q240,127 263.5,103.5Q287,80 320,80L800,80Q833,80 856.5,103.5Q880,127 880,160L880,520Q880,553 856.5,576.5Q833,600 800,600L720,600L720,800Q720,833 696.5,856.5Q673,880 640,880L160,880ZM160,800L640,800Q640,800 640,800Q640,800 640,800L640,520L160,520L160,800Q160,800 160,800Q160,800 160,800ZM720,520L800,520Q800,520 800,520Q800,520 800,520L800,240L320,240L320,360L640,360Q673,360 696.5,383.5Q720,407 720,440L720,520Z"/>
+</vector>
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/keyboard_quick_switch_view.xml b/quickstep/res/layout/keyboard_quick_switch_view.xml
index 2420a46..4118500 100644
--- a/quickstep/res/layout/keyboard_quick_switch_view.xml
+++ b/quickstep/res/layout/keyboard_quick_switch_view.xml
@@ -22,6 +22,7 @@
     android:layout_height="wrap_content"
     android:layout_marginTop="@dimen/keyboard_quick_switch_margin_top"
     android:layout_marginHorizontal="@dimen/keyboard_quick_switch_margin_ends"
+    android:layout_gravity="center_horizontal"
     android:background="@drawable/keyboard_quick_switch_view_background"
     android:clipToOutline="true"
     android:alpha="0"
diff --git a/quickstep/res/layout/task_desktop.xml b/quickstep/res/layout/task_desktop.xml
index 1564653..5270284 100644
--- a/quickstep/res/layout/task_desktop.xml
+++ b/quickstep/res/layout/task_desktop.xml
@@ -19,20 +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"
-        android:layout_height="match_parent" />
 
     <ViewStub
         android:id="@+id/icon"
@@ -40,4 +31,16 @@
         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">
+
+        <View
+            android:id="@+id/background"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent" />
+
+    </com.android.quickstep.views.DesktopTaskContentView>
+
 </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 ed90c85..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>
@@ -154,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 0848ddd..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>
@@ -154,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 501654f..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>
@@ -154,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 1dbab02..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>
@@ -154,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 e211463..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>
@@ -154,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 aa16f3c..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>
@@ -154,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 4dcfe62..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>
@@ -154,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 8ceef77..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>
@@ -154,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 14b86de..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>
@@ -154,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 b60436c..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>
@@ -154,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 4447c01..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>
@@ -154,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 711cbfa..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>
@@ -154,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 2a5b34d..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>
@@ -154,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 478a7a3..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>
@@ -154,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 e47b423..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>
@@ -154,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 04b04dd..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>
@@ -154,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 e0787ca..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>
@@ -154,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 04b04dd..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>
@@ -154,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 04b04dd..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>
@@ -154,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-es-rUS/strings.xml b/quickstep/res/values-es-rUS/strings.xml
index dd8de5f..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>
@@ -154,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 d8bbc55..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>
@@ -154,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 114f3a1..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>
@@ -154,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 45fa579..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>
@@ -154,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 d3e3800..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>
@@ -154,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 54a0c23..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>
@@ -154,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 591c7b7..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>
@@ -154,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 6371f30..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>
@@ -154,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 0603284..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>
@@ -154,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 4a8e9f9..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>
@@ -154,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 2cec388..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>
@@ -154,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 ed52e90..441a80c 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>
@@ -51,8 +52,8 @@
     <string name="back_gesture_feedback_swipe_too_far_from_edge" msgid="4175100312909721217">"Prijeđite prstom od krajnjeg desnog ili krajnjeg lijevog ruba"</string>
     <string name="back_gesture_feedback_cancelled" msgid="762621530959111290">"Pazite da prijeđete prstom od desnog ili lijevog ruba do sredine zaslona i podignite prst"</string>
     <string name="back_gesture_feedback_complete_with_overview_follow_up" msgid="9176400654037014471">"Naučili ste kako prijeći prstom zdesna da biste se vratili. Sad saznajte kako promijeniti aplikaciju."</string>
-    <string name="back_gesture_feedback_complete_with_follow_up" msgid="8653374779579748392">"Izvršili ste pokret za povratak. Sad saznajte kako promijeniti aplikaciju."</string>
-    <string name="back_gesture_feedback_complete_without_follow_up" msgid="197189945858268342">"Izvršili ste pokret za povratak"</string>
+    <string name="back_gesture_feedback_complete_with_follow_up" msgid="8653374779579748392">"Napravili ste pokret za povratak. Sad saznajte kako promijeniti aplikaciju."</string>
+    <string name="back_gesture_feedback_complete_without_follow_up" msgid="197189945858268342">"Napravili ste pokret za povratak"</string>
     <string name="back_gesture_feedback_swipe_in_nav_bar" msgid="9157480023651452969">"Pazite da ne prijeđete prstom preblizu dnu zaslona"</string>
     <string name="back_gesture_tutorial_confirm_subtitle" msgid="5181305411668713250">"Osjetljivost pokreta povratka promijenite u postavkama"</string>
     <string name="back_gesture_intro_title" msgid="19551256430224428">"Prijeđite prstom da biste se vratili"</string>
@@ -63,8 +64,8 @@
     <string name="home_gesture_feedback_swipe_too_far_from_edge" msgid="4816365433160895458">"Pazite da prijeđete prstom prema gore od donjeg ruba zaslona"</string>
     <string name="home_gesture_feedback_overview_detected" msgid="5177627157303895077">"Pazite da ne zastanete prije podizanja prsta"</string>
     <string name="home_gesture_feedback_wrong_swipe_direction" msgid="8328465201424027148">"Pazite da prijeđete prstom ravno prema gore"</string>
-    <string name="home_gesture_feedback_complete_with_follow_up" msgid="8766981412895888417">"Izvršili ste pokret za otvaranje početnog zaslona. Sad saznajte kako se vratiti."</string>
-    <string name="home_gesture_feedback_complete_without_follow_up" msgid="2978063221383413443">"Izvršili ste pokret za otvaranje početnog zaslona"</string>
+    <string name="home_gesture_feedback_complete_with_follow_up" msgid="8766981412895888417">"Napravili ste pokret za otvaranje početnog zaslona. Sad saznajte kako se vratiti."</string>
+    <string name="home_gesture_feedback_complete_without_follow_up" msgid="2978063221383413443">"Napravili ste pokret za otvaranje početnog zaslona"</string>
     <string name="home_gesture_intro_title" msgid="836590312858441830">"Prijeđite prstom da biste otvorili početni zaslon"</string>
     <string name="home_gesture_intro_subtitle" msgid="2632238748497975326">"Prijeđite prstom od dna zaslona prema gore. Tim pokretom uvijek će se otvoriti početni zaslon."</string>
     <string name="home_gesture_spoken_intro_subtitle" msgid="1030987707382031750">"Prijeđite s dva prsta od dna zaslona prema gore. Tim pokretom uvijek će se otvoriti početni zaslon."</string>
@@ -75,7 +76,7 @@
     <string name="overview_gesture_feedback_home_detected" msgid="663432226180397138">"Pokušajte zadržati prozor dulje prije podizanja prsta"</string>
     <string name="overview_gesture_feedback_wrong_swipe_direction" msgid="1191055451018584958">"Prijeđite prstom ravno prema gore, a zatim zastanite"</string>
     <string name="overview_gesture_feedback_complete_with_follow_up" msgid="3544611727467765026">"Naučili ste koristiti pokrete. Pokrete možete isključiti u postavkama."</string>
-    <string name="overview_gesture_feedback_complete_without_follow_up" msgid="2903050864432331629">"Izvršili ste pokret za promjenu aplikacije"</string>
+    <string name="overview_gesture_feedback_complete_without_follow_up" msgid="2903050864432331629">"Napravili ste pokret za promjenu aplikacije"</string>
     <string name="overview_gesture_intro_title" msgid="2902054412868489378">"Povlačenje prstom za promjenu aplikacije"</string>
     <string name="overview_gesture_intro_subtitle" msgid="4968091015637850859">"Da biste promijenili aplikaciju, prijeđite prstom od dna zaslona prema gore, zadržite pritisak pa pustite."</string>
     <string name="overview_gesture_spoken_intro_subtitle" msgid="3853371838260201751">"Za promjenu aplikacije prijeđite s dva prsta od dna zaslona prema gore, zadržite pritisak i pustite."</string>
@@ -154,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 27db3e0..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>
@@ -154,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 9a2cb2e..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>
@@ -154,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 5ddfb7e..4039f36 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>
@@ -52,7 +53,7 @@
     <string name="back_gesture_feedback_cancelled" msgid="762621530959111290">"Pastikan Anda menggeser dari tepi kanan atau kiri ke tengah layar, lalu lepaskan"</string>
     <string name="back_gesture_feedback_complete_with_overview_follow_up" msgid="9176400654037014471">"Anda telah belajar cara geser dari kanan untuk kembali. Berikutnya, pelajari cara beralih aplikasi."</string>
     <string name="back_gesture_feedback_complete_with_follow_up" msgid="8653374779579748392">"Anda telah menyelesaikan gestur kembali. Selanjutnya, pelajari cara beralih aplikasi."</string>
-    <string name="back_gesture_feedback_complete_without_follow_up" msgid="197189945858268342">"Anda telah menyelesaikan gestur kembali"</string>
+    <string name="back_gesture_feedback_complete_without_follow_up" msgid="197189945858268342">"Anda telah menyelesaikan gestur untuk kembali"</string>
     <string name="back_gesture_feedback_swipe_in_nav_bar" msgid="9157480023651452969">"Pastikan Anda tidak menggeser terlalu dekat ke bagian bawah layar"</string>
     <string name="back_gesture_tutorial_confirm_subtitle" msgid="5181305411668713250">"Untuk mengubah sensitivitas gestur kembali, buka Setelan"</string>
     <string name="back_gesture_intro_title" msgid="19551256430224428">"Geser untuk kembali"</string>
@@ -70,7 +71,7 @@
     <string name="home_gesture_spoken_intro_subtitle" msgid="1030987707382031750">"Geser ke atas dengan 2 jari dari bawah layar. Gestur ini akan selalu membawa Anda ke Layar utama."</string>
     <string name="home_gesture_tutorial_title" msgid="3126834347496917376">"Buka layar utama"</string>
     <string name="home_gesture_tutorial_subtitle" msgid="7245995490408668778">"Geser ke atas dari bagian bawah layar"</string>
-    <string name="home_gesture_tutorial_success" msgid="1736295017642244751">"Bagus."</string>
+    <string name="home_gesture_tutorial_success" msgid="1736295017642244751">"Bagus!"</string>
     <string name="overview_gesture_feedback_swipe_too_far_from_edge" msgid="6402349235265407385">"Pastikan Anda menggeser ke atas dari tepi bawah layar"</string>
     <string name="overview_gesture_feedback_home_detected" msgid="663432226180397138">"Coba tahan jendela lebih lama sebelum melepaskan"</string>
     <string name="overview_gesture_feedback_wrong_swipe_direction" msgid="1191055451018584958">"Pastikan Anda menggeser lurus ke atas, lalu berhenti sejenak"</string>
@@ -81,12 +82,12 @@
     <string name="overview_gesture_spoken_intro_subtitle" msgid="3853371838260201751">"Untuk beralih antar-aplikasi, geser ke atas dengan 2 jari dari bawah layar, tahan, lalu lepaskan."</string>
     <string name="overview_gesture_tutorial_title" msgid="4125835002668708720">"Beralih aplikasi"</string>
     <string name="overview_gesture_tutorial_subtitle" msgid="5253549754058973071">"Geser ke atas dari bagian bawah layar, tahan, kemudian lepas"</string>
-    <string name="overview_gesture_tutorial_success" msgid="1910267697807973076">"Bagus."</string>
+    <string name="overview_gesture_tutorial_success" msgid="1910267697807973076">"Oke!"</string>
     <string name="gesture_tutorial_confirm_title" msgid="6201516182040074092">"Semua siap"</string>
     <string name="gesture_tutorial_action_button_label" msgid="6249846312991332122">"Selesai"</string>
     <string name="gesture_tutorial_action_button_label_settings" msgid="2923621047916486604">"Setelan"</string>
     <string name="gesture_tutorial_try_again" msgid="65962545858556697">"Coba lagi"</string>
-    <string name="gesture_tutorial_nice" msgid="2936275692616928280">"Bagus!"</string>
+    <string name="gesture_tutorial_nice" msgid="2936275692616928280">"Sip!"</string>
     <string name="gesture_tutorial_step" msgid="1279786122817620968">"Tutorial <xliff:g id="CURRENT">%1$d</xliff:g>/<xliff:g id="TOTAL">%2$d</xliff:g>"</string>
     <string name="allset_title" msgid="5021126669778966707">"Selesai!"</string>
     <string name="allset_hint" msgid="459504134589971527">"Geser ke atas untuk membuka Layar utama"</string>
@@ -154,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 3aec0ce..bddca4d 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>
@@ -51,8 +52,8 @@
     <string name="back_gesture_feedback_swipe_too_far_from_edge" msgid="4175100312909721217">"Passaðu að strjúka frá jaðri hægri eða vinstri brúnar"</string>
     <string name="back_gesture_feedback_cancelled" msgid="762621530959111290">"Passaðu að strjúka frá jaðri hægri eða vinstri brúnar að miðju skjásins og sleppa síðan"</string>
     <string name="back_gesture_feedback_complete_with_overview_follow_up" msgid="9176400654037014471">"Þú lærðir að strjúka frá hægri til að bakka. Næst skaltu læra hvernig þú skiptir á milli forrita."</string>
-    <string name="back_gesture_feedback_complete_with_follow_up" msgid="8653374779579748392">"Þú laukst við að kynna þér bendinguna „til baka“. Næst skaltu læra hvernig þú skiptir á milli forrita."</string>
-    <string name="back_gesture_feedback_complete_without_follow_up" msgid="197189945858268342">"Þú laukst við að kynna þér bendinguna „til baka“"</string>
+    <string name="back_gesture_feedback_complete_with_follow_up" msgid="8653374779579748392">"Þú laukst við að kynna þér bendinguna „Til baka“. Næst skaltu læra hvernig þú skiptir á milli forrita."</string>
+    <string name="back_gesture_feedback_complete_without_follow_up" msgid="197189945858268342">"Þú laukst við að kynna þér bendinguna „Til baka“"</string>
     <string name="back_gesture_feedback_swipe_in_nav_bar" msgid="9157480023651452969">"Passaðu að strjúka ekki of nálægt neðri brún skjásins"</string>
     <string name="back_gesture_tutorial_confirm_subtitle" msgid="5181305411668713250">"Til að breyta næmi til baka-bendingar ferðu í stillingar"</string>
     <string name="back_gesture_intro_title" msgid="19551256430224428">"Strjúktu til að fara til baka"</string>
@@ -154,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 b9e6f62..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>
@@ -154,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 2a016fa..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>
@@ -154,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 28b7746..a25b2bf 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>
@@ -52,7 +53,7 @@
     <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_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>
@@ -64,7 +65,7 @@
     <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_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>
@@ -154,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 d84d53e..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>
@@ -154,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 4cdbfc4..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>
@@ -154,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 5cf1b92..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>
@@ -154,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 63b0006..5fc27d2 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>
@@ -52,7 +53,7 @@
     <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_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>
@@ -102,7 +103,7 @@
     <string name="toast_contextual_split_select_app" msgid="433510957123687090">"ಸ್ಪ್ಲಿಟ್‌ ಸ್ಕ್ರೀನ್ ಬಳಸಲು ಇನ್ನೊಂದು ಆ್ಯಪ್ ಆಯ್ಕೆಮಾಡಿ"</string>
     <string name="toast_split_select_app_cancel" msgid="1939025102486630426">"ರದ್ದುಮಾಡಿ"</string>
     <string name="toast_split_select_cont_desc" msgid="2119685056059607602">"ಸ್ಪ್ಲಿಟ್ ಸ್ಕ್ರೀನ್ ಆಯ್ಕೆಯಿಂದ ನಿರ್ಗಮಿಸಿ"</string>
-    <string name="toast_split_app_unsupported" msgid="2360229567007828914">"\"ಪರದೆ ಬೇರ್ಪಡಿಸಿ\" ಬಳಸಲು ಬೇರೆ ಆ್ಯಪ್ ಅನ್ನು ಆಯ್ಕೆಮಾಡಿ"</string>
+    <string name="toast_split_app_unsupported" msgid="2360229567007828914">"\"ಸ್ಕ್ರೀನ್ ಬೇರ್ಪಡಿಸಿ\" ಬಳಸಲು ಬೇರೆ ಆ್ಯಪ್ ಅನ್ನು ಆಯ್ಕೆಮಾಡಿ"</string>
     <string name="blocked_by_policy" msgid="2071401072261365546">"ಆ್ಯಪ್ ಅಥವಾ ನಿಮ್ಮ ಸಂಸ್ಥೆಯು ಈ ಕ್ರಿಯೆಯನ್ನು ಅನುಮತಿಸುವುದಿಲ್ಲ"</string>
     <string name="split_widgets_not_supported" msgid="1355743038053053866">"ವಿಜೆಟ್‌ಗಳು ಪ್ರಸ್ತುತ ಬೆಂಬಲಿತವಾಗಿಲ್ಲ, ಮತ್ತೊಂದು ಆ್ಯಪ್ ಅನ್ನು ಆಯ್ಕೆಮಾಡಿ"</string>
     <string name="skip_tutorial_dialog_title" msgid="2725643161260038458">"ನ್ಯಾವಿಗೇಶನ್ ಟ್ಯುಟೋರಿಯಲ್ ಸ್ಕಿಪ್ ಮಾಡಬೇಕೇ?"</string>
@@ -154,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 589cc22..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>
@@ -154,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 faf5675..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>
@@ -154,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 622db2d..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>
@@ -154,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 a95249b..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>
@@ -154,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 1892f64..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>
@@ -154,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 ce4e1b0..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>
@@ -154,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 c15a241..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>
@@ -154,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 1ff7502..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>
@@ -154,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 215ae3f..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>
@@ -154,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 a1f19a9..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>
@@ -154,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 eeb774b..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>
@@ -154,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 b62b7fd..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>
@@ -154,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 a2d4d32..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>
@@ -154,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 ee876cf..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>
@@ -154,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 3ee59b5..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>
@@ -154,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 1e8d52e..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>
@@ -154,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 64adddf..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>
@@ -154,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 2167875..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>
@@ -154,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 9309810..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>
@@ -154,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 19075cd..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>
@@ -154,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 76c4e1f..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>
@@ -154,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 0953b38..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>
@@ -154,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 9b682e6..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>
@@ -154,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 94de1e05..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>
@@ -154,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 29214c9..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>
@@ -154,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 d6e5d03..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>
@@ -154,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 bba98c6..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>
@@ -154,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 f8d6a4f..d872287 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>
@@ -51,8 +52,8 @@
     <string name="back_gesture_feedback_swipe_too_far_from_edge" msgid="4175100312909721217">"Hakikisha unatelezesha kidole kutoka ukingo wa kulia au kushoto kabisa"</string>
     <string name="back_gesture_feedback_cancelled" msgid="762621530959111290">"Hakikisha unatelezesha kidole kutoka ukingo wa kulia au kushoto hadi katikati ya skrini na uachilie"</string>
     <string name="back_gesture_feedback_complete_with_overview_follow_up" msgid="9176400654037014471">"Umejifunza jinsi ya kutelezesha kidole kuanzia kulia ili kurudi nyuma. Sasa jifunze jinsi ya kubadilisha programu."</string>
-    <string name="back_gesture_feedback_complete_with_follow_up" msgid="8653374779579748392">"Umekamilisha ishara ya kurudi nyuma. Hatua inayofuata, jifunze jinsi ya kubadilisha programu."</string>
-    <string name="back_gesture_feedback_complete_without_follow_up" msgid="197189945858268342">"Umeweka ishara ya kurudi nyuma"</string>
+    <string name="back_gesture_feedback_complete_with_follow_up" msgid="8653374779579748392">"Umekamilisha mafunzo ya miguso ya kurudi nyuma. Hatua inayofuata, fahamu jinsi ya kubadilisha programu."</string>
+    <string name="back_gesture_feedback_complete_without_follow_up" msgid="197189945858268342">"Umekamilisha mafunzo ya miguso ya kurudi nyuma"</string>
     <string name="back_gesture_feedback_swipe_in_nav_bar" msgid="9157480023651452969">"Hakikisha hutelezeshi kidole karibu sana na sehemu ya chini ya skrini"</string>
     <string name="back_gesture_tutorial_confirm_subtitle" msgid="5181305411668713250">"Kubadilisha hisi ya ishara ya nyuma, nenda kwenye Mipangilio"</string>
     <string name="back_gesture_intro_title" msgid="19551256430224428">"Telezesha kidole ili urudi nyuma"</string>
@@ -154,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 73c6c37..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>
@@ -154,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 91ef846..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>
@@ -154,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 2218e6d0..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>
@@ -154,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 fac6a52..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>
@@ -154,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 d44d710..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>
@@ -154,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 320c2ea..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>
@@ -154,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 c71625a..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>
@@ -154,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 e379453..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>
@@ -154,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 ddefb9e..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>
@@ -154,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 8540cd9..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,7 +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="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>
@@ -154,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 3d16e8d..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>
@@ -154,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 bf03812..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>
@@ -154,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 d0d0bb6..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>
@@ -154,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..b221b22 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -111,12 +111,14 @@
     <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>
 
     <!-- Launcher app transition -->
     <dimen name="closing_window_trans_y">115dp</dimen>
+    <dimen name="closing_freeform_window_trans_y">36dp</dimen>
 
     <dimen name="quick_switch_scaling_scroll_threshold">100dp</dimen>
 
@@ -356,12 +358,10 @@
     <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>
 
     <!-- Transient taskbar -->
     <dimen name="transient_taskbar_padding">12dp</dimen>
@@ -424,6 +424,10 @@
     <!--- 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>
+
+    <!-- Taskbar Multi Instance Menu -->
+    <dimen name="taskbar_multi_instance_menu_min_padding_from_screen_edge">8dp</dimen>
 
     <!--- Floating Ime Inset height-->
     <dimen name="floating_ime_inset_height">60dp</dimen>
@@ -503,6 +507,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/res/values/styles.xml b/quickstep/res/values/styles.xml
index c423d09..6ffcb9b 100644
--- a/quickstep/res/values/styles.xml
+++ b/quickstep/res/values/styles.xml
@@ -211,6 +211,7 @@
         <item name="android:enforceNavigationBarContrast">false</item>
         <item name="android:windowLightStatusBar">true</item>
         <item name="android:windowBackground">@android:color/transparent</item>
+        <item name="android:windowShowWallpaper">false</item>
     </style>
 
     <!--
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..7d193aa 100644
--- a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
+++ b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
@@ -19,6 +19,7 @@
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
+import static android.app.role.RoleManager.ROLE_HOME;
 import static android.provider.Settings.Secure.LAUNCHER_TASKBAR_EDUCATION_SHOWING;
 import static android.view.RemoteAnimationTarget.MODE_CLOSING;
 import static android.view.RemoteAnimationTarget.MODE_OPENING;
@@ -74,6 +75,7 @@
 import android.animation.ValueAnimator;
 import android.app.ActivityOptions;
 import android.app.WindowConfiguration;
+import android.app.role.RoleManager;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.res.Resources;
@@ -108,6 +110,7 @@
 import android.view.animation.AnimationUtils;
 import android.view.animation.Interpolator;
 import android.view.animation.PathInterpolator;
+import android.window.DesktopModeFlags;
 import android.window.RemoteTransition;
 import android.window.TransitionFilter;
 import android.window.WindowAnimationState;
@@ -116,6 +119,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;
@@ -165,11 +169,13 @@
 import com.android.systemui.shared.system.BlurUtils;
 import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
 import com.android.systemui.shared.system.QuickStepContract;
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
 import com.android.wm.shell.startingsurface.IStartingWindowListener;
 
 import java.io.PrintWriter;
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.List;
@@ -213,6 +219,7 @@
 
     public static final int CONTENT_ALPHA_DURATION = 217;
     public static final int TRANSIENT_TASKBAR_TRANSITION_DURATION = 417;
+    public static final int PINNED_TASKBAR_TRANSITION_DURATION = 600;
     public static final int TASKBAR_TO_APP_DURATION = 600;
     // TODO(b/236145847): Tune TASKBAR_TO_HOME_DURATION to 383 after conflict with unlock animation
     // is solved.
@@ -232,6 +239,7 @@
     protected final Handler mHandler;
 
     private final float mClosingWindowTransY;
+    private final float mClosingFreeformWindowTransY;
     private final float mMaxShadowRadius;
 
     private final StartingWindowListener mStartingWindowListener =
@@ -289,6 +297,8 @@
 
         Resources res = mLauncher.getResources();
         mClosingWindowTransY = res.getDimensionPixelSize(R.dimen.closing_window_trans_y);
+        mClosingFreeformWindowTransY =
+                res.getDimensionPixelSize(R.dimen.closing_freeform_window_trans_y);
         mMaxShadowRadius = res.getDimensionPixelSize(R.dimen.max_shadow_radius);
 
         mLauncher.addOnDeviceProfileChangeListener(this);
@@ -574,23 +584,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 +632,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();
             };
@@ -1159,7 +1196,9 @@
                 .registerRemoteTransition(mLauncherOpenTransition, homeCheck);
         if (mBackAnimationController != null) {
             mBackAnimationController.registerComponentCallbacks();
-            mBackAnimationController.registerBackCallbacks(mHandler);
+            if (isHomeRoleHeld()) {
+                mBackAnimationController.registerBackCallbacks(mHandler);
+            }
         }
     }
 
@@ -1172,6 +1211,22 @@
                 .unregisterContentObserver(mAnimationRemovalObserver));
     }
 
+    /**
+     * Called when the overview-target changes. Updates the back callback registration state.
+     */
+    public void onOverviewTargetChange() {
+        if (isHomeRoleHeld()) {
+            mBackAnimationController.registerBackCallbacks(mHandler);
+        } else {
+            mBackAnimationController.unregisterBackCallbacks();
+        }
+    }
+
+    private boolean isHomeRoleHeld() {
+        RoleManager roleManager = mLauncher.getSystemService(RoleManager.class);
+        return roleManager == null || roleManager.isRoleHeld(ROLE_HOME);
+    }
+
     private void unregisterRemoteAnimations() {
         if (SEPARATE_RECENTS_ACTIVITY.get()) {
             return;
@@ -1353,8 +1408,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());
         }
@@ -1447,10 +1507,16 @@
                 ? 0 : getWindowCornerRadius(mLauncher);
         float startShadowRadius = areAllTargetsTranslucent(appTargets) ? 0 : mMaxShadowRadius;
         closingAnimator.setDuration(duration);
+        boolean isFreeform = isFreeformAnimation(appTargets);
+        float translateY = isFreeform ? mClosingFreeformWindowTransY : mClosingWindowTransY;
+        float endScale = isFreeform ? 0.95f : 1f;
+        Interpolator alphaInterpolator = isFreeform
+                ? clampToDuration(LINEAR, 0, 100, duration)
+                : clampToDuration(LINEAR, 25, 125, duration);
         closingAnimator.addUpdateListener(new MultiValueUpdateListener() {
-            FloatProp mDy = new FloatProp(0, mClosingWindowTransY, DECELERATE_1_7);
-            FloatProp mScale = new FloatProp(1f, 1f, DECELERATE_1_7);
-            FloatProp mAlpha = new FloatProp(1f, 0f, clampToDuration(LINEAR, 25, 125, duration));
+            FloatProp mDy = new FloatProp(0, translateY, DECELERATE_1_7);
+            FloatProp mScale = new FloatProp(1f, endScale, DECELERATE_1_7);
+            FloatProp mAlpha = new FloatProp(1f, 0f, alphaInterpolator);
             FloatProp mShadowRadius = new FloatProp(startShadowRadius, 0, DECELERATE_1_7);
 
             @Override
@@ -1499,6 +1565,13 @@
         return closingAnimator;
     }
 
+    private boolean isFreeformAnimation(RemoteAnimationTarget[] appTargets) {
+        return DesktopModeStatus.canEnterDesktopMode(mLauncher.getApplicationContext())
+                && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS.isTrue()
+                && Arrays.stream(appTargets)
+                        .anyMatch(app -> app.taskInfo != null && app.taskInfo.isFreeform());
+    }
+
     private void addCujInstrumentation(Animator anim, int cuj) {
         anim.addListener(getCujAnimationSuccessListener(cuj));
     }
@@ -1693,8 +1766,21 @@
         return new AnimatorBackState(rectFSpringAnim, anim);
     }
 
-    public static int getTaskbarToHomeDuration() {
-        if (enableScalingRevealHomeAnimation()) {
+    /** Get animation duration for taskbar for going to home. */
+    public static int getTaskbarToHomeDuration(boolean isPinnedTaskbar) {
+        return getTaskbarToHomeDuration(false, isPinnedTaskbar);
+    }
+
+    /**
+     * Get animation duration for taskbar for going to home.
+     *
+     * @param shouldOverrideToFastAnimation should overwrite scaling reveal home animation duration
+     */
+    public static int getTaskbarToHomeDuration(boolean shouldOverrideToFastAnimation,
+            boolean isPinnedTaskbar) {
+        if (isPinnedTaskbar) {
+            return PINNED_TASKBAR_TRANSITION_DURATION;
+        } else if (enableScalingRevealHomeAnimation() && !shouldOverrideToFastAnimation) {
             return TASKBAR_TO_HOME_DURATION_SLOW;
         } else {
             return TASKBAR_TO_HOME_DURATION_FAST;
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..fd0243a 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.
@@ -488,6 +488,15 @@
                 }
             });
         }
+
+        public void onEnterDesktopModeTransitionStarted(int transitionDuration) {
+
+        }
+
+        @Override
+        public void onExitDesktopModeTransitionStarted(int transitionDuration) {
+
+        }
     }
 
     /** A listener for Taskbar in Desktop Mode. */
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..de42669 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,89 @@
         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.wasOpenedFromTaskbar() == wasOpenedFromTaskbar) {
+                    return;
+                }
+
+                // Relayout the KQS view instead of recreating a new one if it is the current
+                // trigger surface is different than the previous one.
+                final int currentFocusIndexOverride =
+                        currentFocusedIndex == -1 && !mControllerCallbacks.isFirstTaskRunning()
+                                ? 0 : currentFocusedIndex;
+
+                // Skip the task reload if the list is not changed.
+                if (!mModel.isTaskListValid(mTaskListChangeId) || !taskIdsToExclude.equals(
+                        mExcludedTaskIds)) {
+                    mExcludedTaskIds = taskIdsToExclude;
+                    mTaskListChangeId = mModel.getTasks((tasks) -> {
+                        processLoadedTasks(tasks, taskIdsToExclude);
+                        mQuickSwitchViewController.updateQuickSwitchView(
+                                mTasks,
+                                mNumHiddenTasks,
+                                currentFocusIndexOverride,
+                                mHasDesktopTask,
+                                mWasDesktopTaskFilteredOut);
+                    });
+                }
+
+                mQuickSwitchViewController.updateLayoutForSurface(wasOpenedFromTaskbar,
+                        currentFocusIndexOverride);
                 return;
+            } else {
+                // Allow the KQS to be reopened during the close animation to make it more
+                // responsive.
+                closeQuickSwitchView(false);
             }
-            // Allow the KQS to be reopened during the close animation to make it more responsive
-            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,18 +203,14 @@
                             ? 0 : currentFocusedIndex,
                     onDesktop,
                     mHasDesktopTask,
-                    mWasDesktopTaskFilteredOut);
+                    mWasDesktopTaskFilteredOut,
+                    wasOpenedFromTaskbar);
             return;
         }
 
+        mExcludedTaskIds = taskIdsToExclude;
         mTaskListChangeId = mModel.getTasks((tasks) -> {
-            mHasDesktopTask = false;
-            mWasDesktopTaskFilteredOut = false;
-            if (onDesktop) {
-                processLoadedTasksOnDesktop(tasks);
-            } 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.
             mQuickSwitchViewController.openQuickSwitchView(
@@ -150,15 +221,32 @@
                             ? 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) {
+        mHasDesktopTask = false;
+        mWasDesktopTaskFilteredOut = false;
+        if (mControllers.taskbarDesktopModeController.getAreDesktopTasksVisible()) {
+            processLoadedTasksOnDesktop(tasks, taskIdsToExclude);
+        } else {
+            processLoadedTasksOutsideDesktop(tasks, taskIdsToExclude);
+        }
+    }
+
+    private void processLoadedTasksOutsideDesktop(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 +264,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 {
@@ -220,6 +311,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();
@@ -278,7 +390,19 @@
             });
         }
 
+        void onCloseStarted() {
+            if (mOnClosed != null) {
+                mOnClosed.run();
+                mOnClosed = null;
+            }
+        }
+
         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..1967dfd 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);
@@ -194,13 +201,15 @@
             int currentFocusIndexOverride,
             @NonNull KeyboardQuickSwitchViewController.ViewCallbacks viewCallbacks,
             boolean useDesktopTaskView) {
+        mContent.removeAllViews();
+
         mViewCallbacks = viewCallbacks;
         Resources resources = context.getResources();
         Resources.Theme theme = context.getTheme();
 
         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 +292,10 @@
         return mDesktopTaskIndex;
     }
 
+    void resetViewCallbacks() {
+        mViewCallbacks = null;
+    }
+
     protected Animator getCloseAnimation() {
         AnimatorSet closeAnimation = new AnimatorSet();
 
@@ -322,11 +335,17 @@
         return closeAnimation;
     }
 
-    private void animateOpen(int currentFocusIndexOverride) {
+    protected void animateOpen(int currentFocusIndexOverride) {
         if (mOpenAnimation != null) {
             // Restart animation since currentFocusIndexOverride can change the initial scroll.
             mOpenAnimation.cancel();
         }
+
+        // Reset the alpha for the case where the KQS view is opened before.
+        setAlpha(0);
+        mScrollView.setAlpha(0);
+        mNoRecentItemsPane.setAlpha(0);
+
         mOpenAnimation = new AnimatorSet();
 
         Animator outlineAnimation = mOutlineAnimationProgress.animateToValue(1f);
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
index 40e77e2..e623b21 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,64 @@
                 /* useDesktopTaskView= */ !onDesktop && hasDesktopTask);
     }
 
+    protected void updateQuickSwitchView(
+            @NonNull List<GroupTask> tasks,
+            int numHiddenTasks,
+            int currentFocusIndexOverride,
+            boolean hasDesktopTask,
+            boolean wasDesktopTaskFilteredOut) {
+        mWasDesktopTaskFilteredOut = wasDesktopTaskFilteredOut;
+        mKeyboardQuickSwitchView.applyLoadPlan(
+                mOverlayContext,
+                tasks,
+                numHiddenTasks,
+                /* updateTasks= */ true,
+                currentFocusIndexOverride,
+                mViewCallbacks,
+                /* useDesktopTaskView= */ !mOnDesktop && 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);
+    }
+
+    protected void updateLayoutForSurface(boolean updateLayoutFromTaskbar,
+            int currentFocusIndexOverride) {
+        BaseDragLayer.LayoutParams lp =
+                (BaseDragLayer.LayoutParams) mKeyboardQuickSwitchView.getLayoutParams();
+
+        if (updateLayoutFromTaskbar) {
+            lp.width = BaseDragLayer.LayoutParams.WRAP_CONTENT;
+        } else {
+            lp.width = BaseDragLayer.LayoutParams.MATCH_PARENT;
+        }
+
+        mKeyboardQuickSwitchView.animateOpen(currentFocusIndexOverride);
+    }
+
     boolean isCloseAnimationRunning() {
         return mCloseAnimation != null;
     }
@@ -110,6 +198,7 @@
             // Let currently-running animation finish.
             return;
         }
+        mControllerCallbacks.onCloseStarted();
         if (!animate) {
             InteractionJankMonitorWrapper.begin(
                     mKeyboardQuickSwitchView, Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_CLOSE);
@@ -162,7 +251,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 +265,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 +278,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 +298,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 +320,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 +394,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..c5be13d 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;
@@ -31,8 +33,8 @@
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Flags;
+import com.android.launcher3.Hotseat;
 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 +52,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;
@@ -81,6 +84,7 @@
     private final DeviceProfile.OnDeviceProfileChangeListener mOnDeviceProfileChangeListener =
             dp -> {
                 onStashedInAppChanged(dp);
+                adjustHotseatForBubbleBar();
                 if (mControllers != null && mControllers.taskbarViewController != null) {
                     mControllers.taskbarViewController.onRotationChanged(dp);
                 }
@@ -150,8 +154,9 @@
 
     @Override
     protected boolean isTaskbarTouchable() {
-        return !(mTaskbarLauncherStateController.isAnimatingToLauncher()
-                && mTaskbarLauncherStateController.isTaskbarAlignedWithHotseat());
+        // Touching down during animation to Hotseat will end the transition and allow the touch to
+        // go through to the Hotseat directly.
+        return !isAnimatingToHotseat();
     }
 
     public void setShouldDelayLauncherStateAnim(boolean shouldDelayLauncherStateAnim) {
@@ -204,11 +209,21 @@
                 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) {
+        // fast animation duration since we will not be playing workspace reveal animation.
+        boolean shouldOverrideToFastAnimation =
+                !isHotseatIconOnTopWhenAligned() || mLauncher.getPredictiveBackToHomeInProgress();
+        boolean isPinnedTaskbar = DisplayController.isPinnedTaskbar(mLauncher);
+        if (isVisible || isPinnedTaskbar) {
+            return getTaskbarToHomeDuration(shouldOverrideToFastAnimation, isPinnedTaskbar);
+        } else {
+            return DisplayController.isTransientTaskbar(mLauncher)
+                    ? TRANSIENT_TASKBAR_TRANSITION_DURATION
+                    : TASKBAR_TO_APP_DURATION;
+        }
     }
 
     @Nullable
@@ -255,6 +270,14 @@
         }
     }
 
+    private void adjustHotseatForBubbleBar() {
+        Hotseat hotseat = mLauncher.getHotseat();
+        if (mControllers.bubbleControllers.isEmpty() || hotseat == null) return;
+        boolean hiddenForBubbles =
+                mControllers.bubbleControllers.get().bubbleBarViewController.isHiddenForNoBubbles();
+        hotseat.post(() -> adjustHotseatForBubbleBar(!hiddenForBubbles));
+    }
+
     /**
      * Create Taskbar animation when going from an app to Launcher as part of recents transition.
      * @param toState If known, the state we will end up in when reaching Launcher.
@@ -354,16 +377,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));
+            }
         }
     }
 
@@ -412,6 +441,17 @@
     }
 
     @Override
+    public boolean isAnimatingToHotseat() {
+        return mTaskbarLauncherStateController.isAnimatingToLauncher()
+                && isIconAlignedWithHotseat();
+    }
+
+    @Override
+    public void endAnimationToHotseat() {
+        mTaskbarLauncherStateController.resetIconAlignment();
+    }
+
+    @Override
     protected boolean isInOverviewUi() {
         return mTaskbarLauncherStateController.isInOverviewUi();
     }
@@ -469,6 +509,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/ManageWindowsTaskbarShortcut.kt b/quickstep/src/com/android/launcher3/taskbar/ManageWindowsTaskbarShortcut.kt
new file mode 100644
index 0000000..0a53bd3
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/ManageWindowsTaskbarShortcut.kt
@@ -0,0 +1,239 @@
+/*
+ * 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.content.Context
+import android.graphics.Bitmap
+import android.view.MotionEvent
+import android.view.View
+import com.android.launcher3.AbstractFloatingView
+import com.android.launcher3.R
+import com.android.launcher3.Utilities
+import com.android.launcher3.model.data.ItemInfo
+import com.android.launcher3.popup.SystemShortcut
+import com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_MULTI_INSTANCE_MENU_OPEN
+import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext
+import com.android.launcher3.util.Themes
+import com.android.launcher3.util.TouchController
+import com.android.launcher3.views.ActivityContext
+import com.android.quickstep.RecentsModel
+import com.android.quickstep.SystemUiProxy
+import com.android.quickstep.util.GroupTask
+import com.android.systemui.shared.recents.model.ThumbnailData
+import com.android.wm.shell.shared.multiinstance.ManageWindowsViewContainer
+import java.util.Collections
+import java.util.function.Predicate
+
+/**
+ * A single menu item shortcut to execute displaying open instances of an app. Default interaction
+ * for [onClick] is to open the menu in a floating window. Touching one of the displayed tasks
+ * launches it.
+ */
+class ManageWindowsTaskbarShortcut<T>(
+    private val target: T,
+    private val itemInfo: ItemInfo?,
+    private val originalView: View,
+    private val controllers: TaskbarControllers,
+) :
+    SystemShortcut<T>(
+        R.drawable.desktop_mode_ic_taskbar_menu_manage_windows,
+        R.string.manage_windows_option_taskbar,
+        target,
+        itemInfo,
+        originalView,
+    ) where T : Context?, T : ActivityContext? {
+    private lateinit var taskbarShortcutAllWindowsView: TaskbarShortcutManageWindowsView
+    private val recentsModel = RecentsModel.INSTANCE[controllers.taskbarActivityContext]
+
+    override fun onClick(v: View?) {
+        val filter =
+            Predicate<GroupTask> { task: GroupTask? ->
+                task != null && task.task1.key.packageName == itemInfo?.getTargetPackage()
+            }
+        recentsModel.getTasks(
+            { tasks: List<GroupTask> ->
+                // Since fetching thumbnails is asynchronous, use this set to gate until the tasks
+                // are ready to display
+                val pendingTaskIds =
+                    Collections.synchronizedSet(tasks.map { it.task1.key.id }.toMutableSet())
+                createAndShowTaskShortcutView(tasks, pendingTaskIds)
+            },
+            filter,
+        )
+    }
+
+    /**
+     * Processes a list of tasks to generate thumbnails and create a taskbar shortcut view.
+     *
+     * Iterates through the tasks, retrieves thumbnails, and adds them to a list. When all
+     * thumbnails are processed, it creates a [TaskbarShortcutManageWindowsView] with the collected
+     * thumbnails and positions it appropriately.
+     */
+    private fun createAndShowTaskShortcutView(
+        tasks: List<GroupTask?>,
+        pendingTaskIds: MutableSet<Int>,
+    ) {
+        val taskList = arrayListOf<Pair<Int, Bitmap?>>()
+        tasks.forEach { groupTask ->
+            groupTask?.task1?.let { task ->
+                recentsModel.thumbnailCache.getThumbnailInBackground(task) {
+                    thumbnailData: ThumbnailData ->
+                    pendingTaskIds.remove(task.key.id)
+                    // Add the current pair of task id and ThumbnailData to the list of all tasks
+                    if (thumbnailData.thumbnail != null) {
+                        taskList.add(task.key.id to thumbnailData.thumbnail)
+                    }
+
+                    // If the set is empty, all thumbnails have been fetched
+                    if (pendingTaskIds.isEmpty() && taskList.isNotEmpty()) {
+                        createAndPositionTaskbarShortcut(taskList)
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Creates and positions the [TaskbarShortcutManageWindowsView] with the provided thumbnails.
+     */
+    private fun createAndPositionTaskbarShortcut(taskList: ArrayList<Pair<Int, Bitmap?>>) {
+        val onIconClickListener =
+            ({ taskId: Int? ->
+                taskbarShortcutAllWindowsView.removeFromContainer()
+                if (taskId != null) {
+                    SystemUiProxy.INSTANCE.get(target).showDesktopApp(taskId, null)
+                }
+            })
+
+        val onOutsideClickListener = { taskbarShortcutAllWindowsView.removeFromContainer() }
+
+        taskbarShortcutAllWindowsView =
+            TaskbarShortcutManageWindowsView(
+                originalView,
+                controllers.taskbarOverlayController.requestWindow(),
+                taskList,
+                onIconClickListener,
+                onOutsideClickListener,
+                controllers,
+            )
+    }
+
+    /**
+     * A view container for displaying the window of open instances of an app
+     *
+     * Handles showing the window snapshots, adding the carousel to the overlay, and closing it.
+     * Also acts as a touch controller to intercept touch events outside the carousel to close it.
+     */
+    class TaskbarShortcutManageWindowsView(
+        private val originalView: View,
+        private val taskbarOverlayContext: TaskbarOverlayContext,
+        snapshotList: ArrayList<Pair<Int, Bitmap?>>,
+        onIconClickListener: (Int) -> Unit,
+        onOutsideClickListener: () -> Unit,
+        private val controllers: TaskbarControllers,
+    ) :
+        ManageWindowsViewContainer(
+            originalView.context,
+            Themes.getAttrColor(originalView.context, R.attr.materialColorSurfaceBright),
+        ),
+        TouchController {
+        private val taskbarActivityContext = controllers.taskbarActivityContext
+
+        init {
+            createAndShowMenuView(snapshotList, onIconClickListener, onOutsideClickListener)
+            taskbarOverlayContext.dragLayer.addTouchController(this)
+        }
+
+        /** Adds the carousel menu to the taskbar overlay drag layer */
+        override fun addToContainer(menuView: ManageWindowsView) {
+            positionCarouselMenu()
+
+            controllers.taskbarAutohideSuspendController.updateFlag(
+                FLAG_AUTOHIDE_SUSPEND_MULTI_INSTANCE_MENU_OPEN,
+                true,
+            )
+            AbstractFloatingView.closeAllOpenViewsExcept(
+                taskbarActivityContext,
+                AbstractFloatingView.TYPE_TASKBAR_OVERLAY_PROXY,
+            )
+            menuView.rootView.minimumHeight = menuView.menuHeight
+            menuView.rootView.minimumWidth = menuView.menuWidth
+
+            taskbarOverlayContext.dragLayer?.addView(menuView.rootView)
+            menuView.rootView.requestFocus()
+        }
+
+        /**
+         * Positions the carousel menu relative to the taskbar and the calling app's icon.
+         *
+         * Calculates the Y position to place the carousel above the taskbar, and the X position to
+         * align with the calling app while ensuring it doesn't go beyond the screen edge.
+         */
+        private fun positionCarouselMenu() {
+            val deviceProfile = taskbarActivityContext.deviceProfile
+            val margin =
+                context.resources.getDimension(
+                    R.dimen.taskbar_multi_instance_menu_min_padding_from_screen_edge
+                )
+
+            // Calculate the Y position to place the carousel above the taskbar
+            menuView.rootView.y =
+                deviceProfile.availableHeightPx -
+                    menuView.menuHeight -
+                    controllers.taskbarStashController.touchableHeight -
+                    margin
+
+            // Calculate the X position to align with the calling app,
+            // but avoid clashing with the screen edge
+            menuView.rootView.translationX =
+                if (Utilities.isRtl(context.resources)) {
+                    -(deviceProfile.availableWidthPx - menuView.menuWidth) / 2f
+                } else {
+                    val maxX = deviceProfile.availableWidthPx - menuView.menuWidth - margin
+                    minOf(originalView.x, maxX)
+                }
+        }
+
+        /** Closes the carousel menu and removes it from the taskbar overlay drag layer */
+        override fun removeFromContainer() {
+            controllers.taskbarAutohideSuspendController.updateFlag(
+                FLAG_AUTOHIDE_SUSPEND_MULTI_INSTANCE_MENU_OPEN,
+                false,
+            )
+            controllers.taskbarStashController.updateAndAnimateTransientTaskbar(true)
+            taskbarOverlayContext.dragLayer?.removeView(menuView.rootView)
+            taskbarOverlayContext.dragLayer.removeTouchController(this)
+        }
+
+        /** TouchController implementations for closing the carousel when touched outside */
+        override fun onControllerTouchEvent(ev: MotionEvent?): Boolean {
+            return false
+        }
+
+        override fun onControllerInterceptTouchEvent(ev: MotionEvent?): Boolean {
+            ev?.let {
+                if (
+                    it.action == MotionEvent.ACTION_DOWN &&
+                        !taskbarOverlayContext.dragLayer.isEventOverView(menuView.rootView, it)
+                ) {
+                    removeFromContainer()
+                }
+            }
+            return false
+        }
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
index 2ac5793..f346e19 100644
--- a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
@@ -15,6 +15,7 @@
  */
 package com.android.launcher3.taskbar;
 
+import static android.view.KeyEvent.ACTION_UP;
 import static android.view.View.AccessibilityDelegate;
 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
 import static android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION;
@@ -23,6 +24,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 +50,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;
@@ -61,6 +66,7 @@
 import android.graphics.Color;
 import android.graphics.Point;
 import android.graphics.Rect;
+import android.graphics.RectF;
 import android.graphics.Region;
 import android.graphics.Region.Op;
 import android.graphics.drawable.Drawable;
@@ -70,6 +76,8 @@
 import android.os.Handler;
 import android.util.Property;
 import android.view.Gravity;
+import android.view.HapticFeedbackConstants;
+import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.View.OnAttachStateChangeListener;
@@ -170,11 +178,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 +196,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 +254,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 +414,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,
@@ -430,14 +450,16 @@
         mPropertyHolders.add(new StatePropertyHolder(mBackButton,
                 flags -> (flags & FLAG_IME_VISIBLE) != 0,
                 ROTATION_DRAWABLE_PERCENT, 1f, 0f));
-        // Translate back button to be at end/start of other buttons for keyguard
+        // Translate back button to be at end/start of other buttons for keyguard (only after SUW
+        // since it is laid to align with SUW actions while in that state)
         int navButtonSize = mContext.getResources().getDimensionPixelSize(
                 R.dimen.taskbar_nav_buttons_size);
         boolean isRtl = Utilities.isRtl(mContext.getResources());
         if (!mContext.isPhoneMode()) {
             mPropertyHolders.add(new StatePropertyHolder(
-                    mBackButton, flags -> (flags & FLAG_ONLY_BACK_FOR_BOUNCER_VISIBLE) != 0
-                            || (flags & FLAG_KEYGUARD_VISIBLE) != 0,
+                    mBackButton, flags -> mContext.isUserSetupComplete()
+                            && ((flags & FLAG_ONLY_BACK_FOR_BOUNCER_VISIBLE) != 0
+                                    || (flags & FLAG_KEYGUARD_VISIBLE) != 0),
                     VIEW_TRANSLATE_X, navButtonSize * (isRtl ? -2 : 2), 0));
         }
 
@@ -750,40 +772,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 +850,44 @@
         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) {
+        final RectF rect = new RectF();
+        buttonView.setOnTouchListener((v, event) -> {
+            if (event.getAction() == MotionEvent.ACTION_DOWN) {
+                rect.set(0, 0, v.getWidth(), v.getHeight());
+            }
+            boolean isCancelled = event.getAction() == MotionEvent.ACTION_CANCEL
+                    || !rect.contains(event.getX(), event.getY());
+            if (event.getAction() == MotionEvent.ACTION_MOVE && !isCancelled) return false;
+            int motionEventAction = event.getAction();
+            int keyEventAction = motionEventAction == MotionEvent.ACTION_DOWN
+                    ? KeyEvent.ACTION_DOWN : ACTION_UP;
+            navButtonController.sendBackKeyEvent(keyEventAction, isCancelled);
+            if (motionEventAction == MotionEvent.ACTION_UP && !isCancelled) {
+                buttonView.performClick();
+                buttonView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
+            }
+            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,21 +1256,52 @@
     /** 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) {
         boolean isNavbarOnRight = location.isOnLeft(mNavButtonsView.isLayoutRtl());
         DeviceProfile dp = mContext.getDeviceProfile();
         float navBarTargetStartX;
-        if (mContext.shouldStartAlignTaskbar()) {
+        if (!mContext.isUserSetupComplete()) {
+            // Skip additional translations on the nav bar container while in SUW layout
+            return 0;
+        } else if (mContext.shouldStartAlignTaskbar()) {
             int navBarSpacing = dp.inlineNavButtonsEndSpacingPx;
             // If the taskbar is start aligned the navigation bar is aligned to the start or end of
             // the container, depending on the bubble bar location
@@ -1218,12 +1331,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..8149f81 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) {
@@ -564,7 +582,9 @@
         int windowFlags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                 | WindowManager.LayoutParams.FLAG_SLIPPERY
                 | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH;
-        if (DisplayController.isTransientTaskbar(this) && !isRunningInTestHarness()) {
+        boolean watchOutside = DisplayController.isTransientTaskbar(this)
+                || isThreeButtonNav();
+        if (watchOutside && !isRunningInTestHarness()) {
             windowFlags |= WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
                     | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
         }
@@ -846,6 +866,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 +986,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 +1033,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 +1250,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 +1274,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 +1293,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 +1369,8 @@
             GroupTask task,
             @Nullable RemoteTransition remoteTransition,
             boolean onDesktop) {
-        handleGroupTaskLaunch(task, remoteTransition, onDesktop, null, null);
+        handleGroupTaskLaunch(task, remoteTransition, onDesktop,
+                /* onStartCallback= */ null, /* onFinishCallback= */ null);
     }
 
     /**
@@ -1332,17 +1394,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 +1419,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 +1536,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 +1618,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 +1760,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/TaskbarAutohideSuspendController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendController.java
index 8ab2ffa..bdc7f92 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendController.java
@@ -47,6 +47,8 @@
     public static final int FLAG_AUTOHIDE_SUSPEND_TRANSIENT_TASKBAR = 1 << 5;
     // User has hovered the taskbar.
     public static final int FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS = 1 << 6;
+    // User has multi instance window open.
+    public static final int FLAG_AUTOHIDE_SUSPEND_MULTI_INSTANCE_MENU_OPEN = 1 << 7;
 
     @IntDef(flag = true, value = {
             FLAG_AUTOHIDE_SUSPEND_FULLSCREEN,
@@ -56,6 +58,7 @@
             FLAG_AUTOHIDE_SUSPEND_IN_LAUNCHER,
             FLAG_AUTOHIDE_SUSPEND_TRANSIENT_TASKBAR,
             FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS,
+            FLAG_AUTOHIDE_SUSPEND_MULTI_INSTANCE_MENU_OPEN,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface AutohideSuspendFlag {}
@@ -133,6 +136,8 @@
                 "FLAG_AUTOHIDE_SUSPEND_IN_LAUNCHER");
         appendFlag(str, flags, FLAG_AUTOHIDE_SUSPEND_TRANSIENT_TASKBAR,
                 "FLAG_AUTOHIDE_SUSPEND_TRANSIENT_TASKBAR");
+        appendFlag(str, flags, FLAG_AUTOHIDE_SUSPEND_MULTI_INSTANCE_MENU_OPEN,
+                "FLAG_AUTOHIDE_SUSPEND_MULTI_INSTANCE_MENU_OPEN");
         return str.toString();
     }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt
index c0e921e..ea6d82b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt
@@ -23,7 +23,6 @@
 import android.graphics.Path
 import android.graphics.RectF
 import com.android.app.animation.Interpolators
-import com.android.internal.policy.ScreenDecorationsUtils
 import com.android.launcher3.R
 import com.android.launcher3.Utilities
 import com.android.launcher3.Utilities.mapRange
@@ -98,12 +97,9 @@
             shadowAlpha = LIGHT_THEME_SHADOW_ALPHA
         }
 
-        if (context.areDesktopTasksVisible()) {
-            fullCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context)
-            cornerRadius = fullCornerRadius
-        } else {
-            fullCornerRadius = context.cornerRadius.toFloat()
-            cornerRadius = fullCornerRadius
+        fullCornerRadius = context.cornerRadius.toFloat()
+        cornerRadius = fullCornerRadius
+        if (!context.areDesktopTasksVisible()) {
             setCornerRoundness(MAX_ROUNDNESS)
         }
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
index 56fd2bb..db70724 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,15 @@
         uiController = newUiController;
         uiController.init(this);
         uiController.updateStateForSysuiFlags(mSharedState.sysuiStateFlags);
-
+        // if bubble controllers are present configure the UI controller
+        bubbleControllers.ifPresentOrElse(bubbleControllers -> {
+            BubbleBarLocation location =
+                    bubbleControllers.bubbleBarViewController.getBubbleBarLocation();
+            boolean hiddenForBubbles =
+                    bubbleControllers.bubbleBarViewController.isHiddenForNoBubbles();
+            uiController.adjustHotseatForBubbleBar(!hiddenForBubbles);
+            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..8b52112 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();
     }
 
@@ -262,6 +262,7 @@
     @Override
     public boolean dispatchTouchEvent(MotionEvent ev) {
         TestLogging.recordMotionEvent(TestProtocol.SEQUENCE_MAIN, "Touch event", ev);
+        mControllerCallbacks.onDispatchTouchEvent(ev);
         return super.dispatchTouchEvent(ev);
     }
 
@@ -269,7 +270,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/TaskbarDragLayerController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java
index 2845cee..925e10b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java
@@ -23,6 +23,7 @@
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.os.SystemProperties;
+import android.view.MotionEvent;
 import android.view.ViewTreeObserver;
 
 import com.android.launcher3.DeviceProfile;
@@ -325,5 +326,15 @@
             }
             mControllers.taskbarInsetsController.drawDebugTouchableRegionBounds(canvas);
         }
+
+        /** Handles any touch event before it is dispatched to the rest of TaskbarDragLayer. */
+        public void onDispatchTouchEvent(MotionEvent ev) {
+            if (mActivity.isThreeButtonNav() && ev.getAction() == MotionEvent.ACTION_OUTSIDE
+                    && mControllers.uiController.isAnimatingToHotseat()) {
+                // When touching during animation to home, jump to the end so Hotseat can handle
+                // the touch. (Gesture Navigation handles this in AbsSwipeUpHandler.)
+                mControllers.uiController.endAnimationToHotseat();
+            }
+        }
     }
 }
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 39ddb40..f33666a 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
@@ -16,22 +16,25 @@
 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.app.animation.Interpolators.INSTANT;
+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;
@@ -39,12 +42,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;
@@ -53,16 +59,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;
@@ -152,6 +161,7 @@
     private AnimatedFloat mTaskbarAlpha;
     private AnimatedFloat mTaskbarCornerRoundness;
     private MultiProperty mTaskbarAlphaForHome;
+    private @Nullable Animator mHotseatTranslationXAnimation;
     private QuickstepLauncher mLauncher;
 
     private boolean mIsDestroyed = false;
@@ -174,6 +184,8 @@
 
     private boolean mShouldDelayLauncherStateAnim;
 
+    private @Nullable BubbleBarLocation mBubbleBarLocation;
+
     // We skip any view synchronizations during init/destroy.
     private boolean mCanSyncViews;
 
@@ -191,6 +203,8 @@
                     mIsQsbInline = dp.isQsbInline;
                     TaskbarLauncherStateController.this.updateIconAlphaForHome(
                             mTaskbarAlphaForHome.getValue(), ALPHA_CHANNEL_TASKBAR_ALIGNMENT);
+                    TaskbarLauncherStateController.this.onBubbleBarLocationChanged(
+                            mBubbleBarLocation, /* animate = */ false);
                 }
             };
 
@@ -209,7 +223,9 @@
                     updateStateForFlag(FLAG_LAUNCHER_IN_STATE_TRANSITION, true);
                     if (!mShouldDelayLauncherStateAnim) {
                         if (toState == LauncherState.NORMAL) {
-                            applyState(QuickstepTransitionManager.getTaskbarToHomeDuration());
+                            applyState(QuickstepTransitionManager.getTaskbarToHomeDuration(
+                                    DisplayController.isPinnedTaskbar(
+                                            mControllers.taskbarActivityContext)));
                         } else {
                             applyState();
                         }
@@ -363,16 +379,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();
@@ -455,9 +462,12 @@
 
     private Animator onStateChangeApplied(int changedFlags, long duration, boolean start) {
         final boolean isInLauncher = isInLauncher();
+        final boolean isInOverview = mControllers.uiController.isInOverviewUi();
         final boolean isIconAlignedWithHotseat = isIconAlignedWithHotseat();
         final float toAlignment = isIconAlignedWithHotseat ? 1 : 0;
         boolean handleOpenFloatingViews = false;
+        boolean isPinnedTaskbar = DisplayController.isPinnedTaskbar(
+                mControllers.taskbarActivityContext);
         if (DEBUG) {
             Log.d(TAG, "onStateChangeApplied - isInLauncher: " + isInLauncher
                     + ", mLauncherState: " + mLauncherState
@@ -465,11 +475,15 @@
         }
         mControllers.bubbleControllers.ifPresent(controllers -> {
             // Show the bubble bar when on launcher home (hotseat icons visible) or in overview
-            boolean onOverview = mLauncherState == LauncherState.OVERVIEW;
+            boolean onOverview = isInLauncher && 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;
@@ -565,10 +579,17 @@
         }
 
         float backgroundAlpha = isInLauncher && isTaskbarAlignedWithHotseat() ? 0 : 1;
+        AnimatedFloat taskbarBgOffset =
+                mControllers.taskbarDragLayerController.getTaskbarBackgroundOffset();
+        boolean showTaskbar = !isInLauncher || isInOverview;
+        float taskbarBgOffsetEnd = showTaskbar ? 0f : 1f;
+        float taskbarBgOffsetStart = showTaskbar ? 1f : 0f;
 
         // Don't animate if background has reached desired value.
         if (mTaskbarBackgroundAlpha.isAnimating()
-                || mTaskbarBackgroundAlpha.value != backgroundAlpha) {
+                || mTaskbarBackgroundAlpha.value != backgroundAlpha
+                || taskbarBgOffset.isAnimatingToValue(taskbarBgOffsetStart)
+                || taskbarBgOffset.value != taskbarBgOffsetEnd) {
             mTaskbarBackgroundAlpha.cancelAnimation();
             if (DEBUG) {
                 Log.d(TAG, "onStateChangeApplied - taskbarBackgroundAlpha - "
@@ -579,25 +600,35 @@
             boolean isInLauncherIconNotAligned = isInLauncher && !isIconAlignedWithHotseat;
             boolean notInLauncherIconNotAligned = !isInLauncher && !isIconAlignedWithHotseat;
             boolean isInLauncherIconIsAligned = isInLauncher && isIconAlignedWithHotseat;
+            // When Hotseat icons are not on top don't change duration or add start delay.
+            // This will keep the duration in sync for icon alignment and background fade in/out.
+            // For example, launching app from launcher all apps.
+            boolean isHotseatIconOnTopWhenAligned =
+                    mControllers.uiController.isHotseatIconOnTopWhenAligned();
 
             float startDelay = 0;
             // We want to delay the background from fading in so that the icons have time to move
             // into the bounds of the background before it appears.
             if (isInLauncherIconNotAligned) {
                 startDelay = duration * TASKBAR_BG_ALPHA_LAUNCHER_NOT_ALIGNED_DELAY_MULT;
-            } else if (notInLauncherIconNotAligned) {
+            } else if (notInLauncherIconNotAligned && isHotseatIconOnTopWhenAligned) {
                 startDelay = duration * TASKBAR_BG_ALPHA_NOT_LAUNCHER_NOT_ALIGNED_DELAY_MULT;
             }
             float newDuration = duration - startDelay;
-            if (isInLauncherIconIsAligned) {
+            if (isInLauncherIconIsAligned && isHotseatIconOnTopWhenAligned) {
                 // Make the background fade out faster so that it is gone by the time the
                 // icons move outside of the bounds of the background.
                 newDuration = duration * TASKBAR_BG_ALPHA_LAUNCHER_IS_ALIGNED_DURATION_MULT;
             }
-            Animator taskbarBackgroundAlpha = mTaskbarBackgroundAlpha
-                    .animateToValue(backgroundAlpha)
-                    .setDuration((long) newDuration);
-            taskbarBackgroundAlpha.setStartDelay((long) startDelay);
+            Animator taskbarBackgroundAlpha = mTaskbarBackgroundAlpha.animateToValue(
+                    backgroundAlpha);
+            if (isPinnedTaskbar) {
+                setupPinnedTaskbarAnimation(animatorSet, showTaskbar, taskbarBgOffset,
+                        taskbarBgOffsetStart, taskbarBgOffsetEnd, duration, taskbarBackgroundAlpha);
+            } else {
+                taskbarBackgroundAlpha.setDuration((long) newDuration);
+                taskbarBackgroundAlpha.setStartDelay((long) startDelay);
+            }
             animatorSet.play(taskbarBackgroundAlpha);
         }
 
@@ -663,10 +694,19 @@
                         + mIconAlignment.value
                         + " -> " + toAlignment + ": " + duration);
             }
-            animatorSet.play(iconAlignAnim);
+            if (!isPinnedTaskbar) {
+                if (hasAnyFlag(FLAG_TASKBAR_HIDDEN)) {
+                    iconAlignAnim.setInterpolator(FINAL_FRAME);
+                } else {
+                    animatorSet.play(iconAlignAnim);
+                }
+            }
         }
 
-        animatorSet.setInterpolator(EMPHASIZED);
+        Interpolator interpolator = enableScalingRevealHomeAnimation() && !isPinnedTaskbar
+                ? ScalingWorkspaceRevealAnim.SCALE_INTERPOLATOR : EMPHASIZED;
+
+        animatorSet.setInterpolator(interpolator);
 
         if (start) {
             animatorSet.start();
@@ -674,6 +714,49 @@
         return animatorSet;
     }
 
+    private void setupPinnedTaskbarAnimation(AnimatorSet animatorSet, boolean showTaskbar,
+            AnimatedFloat taskbarBgOffset, float taskbarBgOffsetStart, float taskbarBgOffsetEnd,
+            long duration, Animator taskbarBackgroundAlpha) {
+        float targetAlpha = !showTaskbar ? 1 : 0;
+        mLauncher.getHotseat().setIconsAlpha(targetAlpha, ALPHA_CHANNEL_TASKBAR_ALIGNMENT);
+        if (mIsQsbInline) {
+            mLauncher.getHotseat().setQsbAlpha(targetAlpha,
+                    ALPHA_CHANNEL_TASKBAR_ALIGNMENT);
+        }
+
+        if ((taskbarBgOffset.value != taskbarBgOffsetEnd && !taskbarBgOffset.isAnimating())
+                || taskbarBgOffset.isAnimatingToValue(taskbarBgOffsetStart)) {
+            taskbarBgOffset.cancelAnimation();
+            Animator taskbarIconAlpha = mTaskbarAlphaForHome.animateToValue(
+                    showTaskbar ? 1f : 0f);
+            AnimatedFloat taskbarIconTranslationYForHome =
+                    mControllers.taskbarViewController.mTaskbarIconTranslationYForHome;
+            ObjectAnimator taskbarBackgroundOffset = taskbarBgOffset.animateToValue(
+                    taskbarBgOffsetStart,
+                    taskbarBgOffsetEnd);
+            ObjectAnimator taskbarIconsYTranslation = null;
+            float taskbarHeight =
+                    mControllers.taskbarActivityContext.getDeviceProfile().taskbarHeight;
+            if (showTaskbar) {
+                taskbarIconsYTranslation = taskbarIconTranslationYForHome.animateToValue(
+                        taskbarHeight, 0);
+            } else {
+                taskbarIconsYTranslation = taskbarIconTranslationYForHome.animateToValue(0,
+                        taskbarHeight);
+            }
+
+            taskbarIconAlpha.setDuration(duration);
+            taskbarIconsYTranslation.setDuration(duration);
+            taskbarBackgroundOffset.setDuration(duration);
+
+            animatorSet.play(taskbarIconAlpha);
+            animatorSet.play(taskbarIconsYTranslation);
+            animatorSet.play(taskbarBackgroundOffset);
+        }
+        taskbarBackgroundAlpha.setInterpolator(showTaskbar ? INSTANT : FINAL_FRAME);
+        taskbarBackgroundAlpha.setDuration(duration);
+    }
+
     /**
      * Whether the taskbar is aligned with the hotseat in the current/target launcher state.
      *
@@ -765,6 +848,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() {
@@ -849,6 +935,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;
@@ -875,8 +1019,9 @@
          *
          * @param finishedToApp {@code true} if the recents animation finished to showing an app and
          *                      not workspace or overview
-         * @param canceled {@code true} if the recents animation was canceled instead of finishing
-         *                 to completion
+         * @param canceled      {@code true} if the recents animation was canceled instead of
+         *                      finishing
+         *                      to completion
          */
         private void endGestureStateOverride(boolean finishedToApp, boolean canceled) {
             mCallbacks.removeListener(this);
@@ -893,6 +1038,7 @@
 
     /**
      * Updates the visible state immediately to ensure a seamless handoff.
+     *
      * @param finishedToApp True iff user is in an app.
      */
     private void updateStateForUserFinishedToApp(boolean finishedToApp) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
index 78e7b47..4498fea 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;
 
@@ -47,6 +46,7 @@
 import android.os.Trace;
 import android.provider.Settings;
 import android.util.Log;
+import android.util.SparseArray;
 import android.view.Display;
 import android.view.MotionEvent;
 import android.view.WindowManager;
@@ -72,7 +72,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;
@@ -114,14 +116,12 @@
     private final Context mContext;
     private final @Nullable Context mNavigationBarPanelContext;
     private WindowManager mWindowManager;
-    private FrameLayout mTaskbarRootLayout;
     private boolean mAddedWindow;
-    private boolean mIsSuspended;
-    private final TaskbarNavButtonController mNavButtonController;
-    private final ComponentCallbacks mComponentCallbacks;
+    private final TaskbarNavButtonController mDefaultNavButtonController;
+    private final ComponentCallbacks mDefaultComponentCallbacks;
 
     private final SimpleBroadcastReceiver mShutdownReceiver =
-            new SimpleBroadcastReceiver(UI_HELPER_EXECUTOR, i -> destroyExistingTaskbar());
+            new SimpleBroadcastReceiver(UI_HELPER_EXECUTOR, i -> destroyAllTaskbars());
 
     // The source for this provider is set when Launcher is available
     // We use 'non-destroyable' version here so the original provider won't be destroyed
@@ -129,9 +129,13 @@
     // It's destruction/creation will be managed by the activity.
     private final ScopedUnfoldTransitionProgressProvider mUnfoldProgressProvider =
             new NonDestroyableScopedUnfoldTransitionProgressProvider();
-
-    private TaskbarActivityContext mTaskbarActivityContext;
+    /** DisplayId - {@link TaskbarActivityContext} map for Connected Display. */
+    private final SparseArray<TaskbarActivityContext> mTaskbars = new SparseArray<>();
+    /** DisplayId - {@link FrameLayout} map for Connected Display. */
+    private final SparseArray<FrameLayout> mRootLayouts = new SparseArray<>();
     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.
@@ -165,7 +169,9 @@
     private final Runnable mActivityOnDestroyCallback = new Runnable() {
         @Override
         public void run() {
+            int displayId = getDefaultDisplayId();
             if (mActivity != null) {
+                displayId = mActivity.getDisplayId();
                 mActivity.removeOnDeviceProfileChangeListener(
                         mDebugActivityDeviceProfileChanged);
                 Log.d(TASKBAR_NOT_DESTROYED_TAG,
@@ -173,10 +179,14 @@
                                 + "onActivityDestroyed.");
                 mActivity.removeEventCallback(EVENT_DESTROYED, this);
             }
+            if (mActivity == mRecentsViewContainer) {
+                mRecentsViewContainer = null;
+            }
             mActivity = null;
             debugWhyTaskbarNotDestroyed("clearActivity");
-            if (mTaskbarActivityContext != null) {
-                mTaskbarActivityContext.setUIController(TaskbarUIController.DEFAULT);
+            TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId);
+            if (taskbar != null) {
+                taskbar.setUIController(TaskbarUIController.DEFAULT);
             }
             mUnfoldProgressProvider.setSourceProvider(null);
         }
@@ -220,7 +230,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);
@@ -231,27 +241,28 @@
         mDesktopVisibilityController = desktopVisibilityController;
         if (enableTaskbarNoRecreate()) {
             mWindowManager = mContext.getSystemService(WindowManager.class);
-            mTaskbarRootLayout = new FrameLayout(mContext) {
+            FrameLayout taskbarRootLayout = new FrameLayout(mContext) {
                 @Override
                 public boolean dispatchTouchEvent(MotionEvent ev) {
                     // The motion events can be outside the view bounds of task bar, and hence
                     // manually dispatching them to the drag layer here.
-                    if (mTaskbarActivityContext != null
-                            && mTaskbarActivityContext.getDragLayer().isAttachedToWindow()) {
-                        return mTaskbarActivityContext.getDragLayer().dispatchTouchEvent(ev);
+                    TaskbarActivityContext taskbar = getTaskbarForDisplay(getDefaultDisplayId());
+                    if (taskbar != null && taskbar.getDragLayer().isAttachedToWindow()) {
+                        return taskbar.getDragLayer().dispatchTouchEvent(ev);
                     }
                     return super.dispatchTouchEvent(ev);
                 }
             };
+            addTaskbarRootLayoutToMap(getDefaultDisplayId(), taskbarRootLayout);
         }
-        mNavButtonController = new TaskbarNavButtonController(
+        mDefaultNavButtonController = new TaskbarNavButtonController(
                 context,
                 navCallbacks,
                 SystemUiProxy.INSTANCE.get(mContext),
                 ContextualEduStatsManager.INSTANCE.get(mContext),
                 new Handler(),
-                AssistUtils.newInstance(mContext));
-        mComponentCallbacks = new ComponentCallbacks() {
+                ContextualSearchInvoker.newInstance(mContext));
+        mDefaultComponentCallbacks = new ComponentCallbacks() {
             private Configuration mOldConfig = mContext.getResources().getConfiguration();
 
             @Override
@@ -260,6 +271,7 @@
                         "onConfigurationChanged: " + newConfig);
                 debugWhyTaskbarNotDestroyed(
                         "TaskbarManager#mComponentCallbacks.onConfigurationChanged: " + newConfig);
+                // TODO: adapt this logic to be specific to different displays.
                 DeviceProfile dp = mUserUnlocked
                         ? LauncherAppState.getIDP(mContext).getDeviceProfile(mContext)
                         : null;
@@ -276,12 +288,12 @@
 
                 debugWhyTaskbarNotDestroyed("ComponentCallbacks#onConfigurationChanged() "
                         + "configDiff=" + Configuration.configurationDiffToString(configDiff));
-                if (configDiff != 0 || mTaskbarActivityContext == null) {
+                if (configDiff != 0 || getCurrentActivityContext() == null) {
                     recreateTaskbar();
                 } else {
                     // Config change might be handled without re-creating the taskbar
                     if (dp != null && !isTaskbarEnabled(dp)) {
-                        destroyExistingTaskbar();
+                        destroyDefaultTaskbar();
                     } else {
                         if (dp != null && isTaskbarEnabled(dp)) {
                             if (ENABLE_TASKBAR_NAVBAR_UNIFICATION) {
@@ -290,10 +302,10 @@
                                 // block above?
                                 recreateTaskbar();
                             } else {
-                                mTaskbarActivityContext.updateDeviceProfile(dp);
+                                getCurrentActivityContext().updateDeviceProfile(dp);
                             }
                         }
-                        mTaskbarActivityContext.onConfigurationChanged(configDiff);
+                        getCurrentActivityContext().onConfigurationChanged(configDiff);
                     }
                 }
                 mOldConfig = new Configuration(newConfig);
@@ -310,7 +322,7 @@
         SettingsCache.INSTANCE.get(mContext)
                 .register(NAV_BAR_KIDS_MODE, mOnSettingsChangeListener);
         Log.d(TASKBAR_NOT_DESTROYED_TAG, "registering component callbacks from constructor.");
-        mContext.registerComponentCallbacks(mComponentCallbacks);
+        mContext.registerComponentCallbacks(mDefaultComponentCallbacks);
         mShutdownReceiver.register(mContext, Intent.ACTION_SHUTDOWN);
         UI_HELPER_EXECUTOR.execute(() -> {
             mSharedState.taskbarSystemActionPendingIntent = PendingIntent.getBroadcast(
@@ -326,18 +338,31 @@
         recreateTaskbar();
     }
 
-    private void destroyExistingTaskbar() {
-        debugWhyTaskbarNotDestroyed("destroyExistingTaskbar: " + mTaskbarActivityContext);
-        if (mTaskbarActivityContext != null) {
-            mTaskbarActivityContext.onDestroy();
-            if (!ENABLE_TASKBAR_NAVBAR_UNIFICATION || enableTaskbarNoRecreate()) {
-                mTaskbarActivityContext = null;
-            }
+    private void destroyAllTaskbars() {
+        for (int i = 0; i < mTaskbars.size(); i++) {
+            int displayId = mTaskbars.keyAt(i);
+            destroyTaskbarForDisplay(displayId);
+            removeTaskbarRootViewFromWindow(displayId);
+        }
+    }
+
+    private void destroyDefaultTaskbar() {
+        destroyTaskbarForDisplay(getDefaultDisplayId());
+    }
+
+    private void destroyTaskbarForDisplay(int displayId) {
+        TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId);
+        debugWhyTaskbarNotDestroyed(
+                "destroyTaskbarForDisplay: " + taskbar + " displayId=" + displayId);
+        if (taskbar != null) {
+            taskbar.onDestroy();
+            // remove all defaults that we store
+            removeTaskbarFromMap(displayId);
         }
         DeviceProfile dp = mUserUnlocked ?
                 LauncherAppState.getIDP(mContext).getDeviceProfile(mContext) : null;
         if (dp == null || !isTaskbarEnabled(dp)) {
-            removeTaskbarRootViewFromWindow();
+            removeTaskbarRootViewFromWindow(displayId);
         }
     }
 
@@ -345,8 +370,10 @@
      * Show Taskbar upon receiving broadcast
      */
     private void showTaskbarFromBroadcast(Intent intent) {
-        if (ACTION_SHOW_TASKBAR.equals(intent.getAction()) && mTaskbarActivityContext != null) {
-            mTaskbarActivityContext.showTaskbarFromBroadcast();
+        // TODO: make this code displayId specific
+        TaskbarActivityContext taskbar = getTaskbarForDisplay(getDefaultDisplayId());
+        if (ACTION_SHOW_TASKBAR.equals(intent.getAction()) && taskbar != null) {
+            taskbar.showTaskbarFromBroadcast();
         }
     }
 
@@ -354,12 +381,13 @@
      * Toggles All Apps for Taskbar or Launcher depending on the current state.
      */
     public void toggleAllApps() {
-        if (mTaskbarActivityContext == null || mTaskbarActivityContext.canToggleHomeAllApps()) {
+        TaskbarActivityContext taskbar = getTaskbarForDisplay(getDefaultDisplayId());
+        if (taskbar == null || taskbar.canToggleHomeAllApps()) {
             // Home All Apps should be toggled from this class, because the controllers are not
             // initialized when Taskbar is disabled (i.e. TaskbarActivityContext is null).
             if (mActivity instanceof Launcher l) l.toggleAllAppsSearch();
         } else {
-            mTaskbarActivityContext.toggleAllAppsSearch();
+            taskbar.toggleAllAppsSearch();
         }
     }
 
@@ -370,8 +398,8 @@
      * progress.
      */
     public AnimatorPlaybackController createLauncherStartFromSuwAnim(int duration) {
-        return mTaskbarActivityContext == null
-                ? null : mTaskbarActivityContext.createLauncherStartFromSuwAnim(duration);
+        TaskbarActivityContext taskbar = getTaskbarForDisplay(getDefaultDisplayId());
+        return taskbar == null ? null : taskbar.createLauncherStartFromSuwAnim(duration);
     }
 
     /**
@@ -381,7 +409,7 @@
         mUserUnlocked = true;
         DisplayController.INSTANCE.get(mContext).addChangeListener(mRecreationListener);
         recreateTaskbar();
-        addTaskbarRootViewToWindow();
+        addTaskbarRootViewToWindow(getDefaultDisplayId());
     }
 
     /**
@@ -405,9 +433,29 @@
         }
         mUnfoldProgressProvider.setSourceProvider(unfoldTransitionProgressProvider);
 
-        if (mTaskbarActivityContext != null) {
-            mTaskbarActivityContext.setUIController(
-                    createTaskbarUIControllerForActivity(mActivity));
+        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;
+        TaskbarActivityContext taskbar = getCurrentActivityContext();
+        if (taskbar != null) {
+            taskbar.setUIController(
+                    createTaskbarUIControllerForRecentsViewContainer(mRecentsViewContainer));
         }
     }
 
@@ -430,25 +478,39 @@
     /**
      * 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;
     }
 
     /**
      * This method is called multiple times (ex. initial init, then when user unlocks) in which case
-     * we fully want to destroy an existing taskbar and create a new one.
+     * we fully want to destroy the existing default display's taskbar and create a new one.
      * In other case (folding/unfolding) we don't need to remove and add window.
      */
     @VisibleForTesting
     public synchronized void recreateTaskbar() {
-        if (mIsSuspended) return;
+        // TODO: make this recreate all taskbars in map.
+        recreateTaskbarForDisplay(getDefaultDisplayId());
+    }
 
+    /**
+     * This method is called multiple times (ex. initial init, then when user unlocks) in which case
+     * we fully want to destroy an existing taskbar for a specified display and create a new one.
+     * In other case (folding/unfolding) we don't need to remove and add window.
+     */
+    private void recreateTaskbarForDisplay(int displayId) {
         Trace.beginSection("recreateTaskbar");
         try {
             DeviceProfile dp = mUserUnlocked ?
@@ -458,7 +520,7 @@
             final boolean isLargeScreenTaskbar = dp != null && dp.isTaskbarPresent;
             mAllAppsActionManager.setTaskbarPresent(isLargeScreenTaskbar);
 
-            destroyExistingTaskbar();
+            destroyTaskbarForDisplay(displayId);
 
             boolean isTaskbarEnabled = dp != null && isTaskbarEnabled(dp);
             debugWhyTaskbarNotDestroyed("recreateTaskbar: isTaskbarEnabled=" + isTaskbarEnabled
@@ -473,29 +535,32 @@
                 }
             }
 
-            if (enableTaskbarNoRecreate() || mTaskbarActivityContext == null) {
-                mTaskbarActivityContext = new TaskbarActivityContext(mContext,
-                        mNavigationBarPanelContext, dp, mNavButtonController,
+            TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId);
+            if (enableTaskbarNoRecreate() || taskbar == null) {
+                taskbar = new TaskbarActivityContext(mContext,
+                        mNavigationBarPanelContext, dp, mDefaultNavButtonController,
                         mUnfoldProgressProvider, mDesktopVisibilityController);
             } else {
-                mTaskbarActivityContext.updateDeviceProfile(dp);
+                taskbar.updateDeviceProfile(dp);
             }
             mSharedState.startTaskbarVariantIsTransient =
-                    DisplayController.isTransientTaskbar(mTaskbarActivityContext);
+                    DisplayController.isTransientTaskbar(taskbar);
             mSharedState.allAppsVisible = mSharedState.allAppsVisible && isLargeScreenTaskbar;
-            mTaskbarActivityContext.init(mSharedState);
+            taskbar.init(mSharedState);
 
-            if (mActivity != null) {
-                mTaskbarActivityContext.setUIController(
-                    createTaskbarUIControllerForActivity(mActivity));
+            if (mRecentsViewContainer != null) {
+                taskbar.setUIController(
+                        createTaskbarUIControllerForRecentsViewContainer(mRecentsViewContainer));
             }
 
             if (enableTaskbarNoRecreate()) {
-                addTaskbarRootViewToWindow();
-                mTaskbarRootLayout.removeAllViews();
-                mTaskbarRootLayout.addView(mTaskbarActivityContext.getDragLayer());
-                mTaskbarActivityContext.notifyUpdateLayoutParams();
+                addTaskbarRootViewToWindow(displayId);
+                FrameLayout taskbarRootLayout = getTaskbarRootLayoutForDisplay(displayId);
+                taskbarRootLayout.removeAllViews();
+                taskbarRootLayout.addView(taskbar.getDragLayer());
+                taskbar.notifyUpdateLayoutParams();
             }
+            addTaskbarToMap(displayId, taskbar);
         } finally {
             Trace.endSection();
         }
@@ -507,14 +572,15 @@
                     mSharedState.sysuiStateFlags, QuickStepContract::getSystemUiStateString));
         }
         mSharedState.sysuiStateFlags = systemUiStateFlags;
-        if (mTaskbarActivityContext != null) {
-            mTaskbarActivityContext.updateSysuiStateFlags(systemUiStateFlags, false /* fromInit */);
+        TaskbarActivityContext taskbar = getTaskbarForDisplay(getDefaultDisplayId());
+        if (taskbar != null) {
+            taskbar.updateSysuiStateFlags(systemUiStateFlags, false /* fromInit */);
         }
     }
 
     public void onLongPressHomeEnabled(boolean assistantLongPressEnabled) {
-        if (mNavButtonController != null) {
-            mNavButtonController.setAssistantLongPressEnabled(assistantLongPressEnabled);
+        if (mDefaultNavButtonController != null) {
+            mDefaultNavButtonController.setAssistantLongPressEnabled(assistantLongPressEnabled);
         }
     }
 
@@ -523,46 +589,53 @@
      */
     public void setSetupUIVisible(boolean isVisible) {
         mSharedState.setupUIVisible = isVisible;
-        if (mTaskbarActivityContext != null) {
-            mTaskbarActivityContext.setSetupUIVisible(isVisible);
+        TaskbarActivityContext taskbar = getTaskbarForDisplay(getDefaultDisplayId());
+        if (taskbar != null) {
+            taskbar.setSetupUIVisible(isVisible);
         }
     }
 
     public void setWallpaperVisible(boolean isVisible) {
         mSharedState.wallpaperVisible = isVisible;
-        if (mTaskbarActivityContext != null) {
-            mTaskbarActivityContext.setWallpaperVisible(isVisible);
+        TaskbarActivityContext taskbar = getTaskbarForDisplay(getDefaultDisplayId());
+        if (taskbar != null) {
+            taskbar.setWallpaperVisible(isVisible);
         }
     }
 
-    public void checkNavBarModes() {
-        if (mTaskbarActivityContext != null) {
-            mTaskbarActivityContext.checkNavBarModes();
+    public void checkNavBarModes(int displayId) {
+        TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId);
+        if (taskbar != null) {
+            taskbar.checkNavBarModes();
         }
     }
 
-    public void finishBarAnimations() {
-        if (mTaskbarActivityContext != null) {
-            mTaskbarActivityContext.finishBarAnimations();
+    public void finishBarAnimations(int displayId) {
+        TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId);
+        if (taskbar != null) {
+            taskbar.finishBarAnimations();
         }
     }
 
-    public void touchAutoDim(boolean reset) {
-        if (mTaskbarActivityContext != null) {
-            mTaskbarActivityContext.touchAutoDim(reset);
+    public void touchAutoDim(int displayId, boolean reset) {
+        TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId);
+        if (taskbar != null) {
+            taskbar.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);
+        TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId);
+        if (taskbar != null) {
+            taskbar.transitionTo(barMode, animate);
         }
     }
 
     public void appTransitionPending(boolean pending) {
-        if (mTaskbarActivityContext != null) {
-            mTaskbarActivityContext.appTransitionPending(pending);
+        TaskbarActivityContext taskbar = getTaskbarForDisplay(getDefaultDisplayId());
+        if (taskbar != null) {
+            taskbar.appTransitionPending(pending);
         }
     }
 
@@ -571,8 +644,9 @@
     }
 
     public void onRotationProposal(int rotation, boolean isValid) {
-        if (mTaskbarActivityContext != null) {
-            mTaskbarActivityContext.onRotationProposal(rotation, isValid);
+        TaskbarActivityContext taskbar = getTaskbarForDisplay(getDefaultDisplayId());
+        if (taskbar != null) {
+            taskbar.onRotationProposal(rotation, isValid);
         }
     }
 
@@ -580,38 +654,43 @@
         mSharedState.disableNavBarDisplayId = displayId;
         mSharedState.disableNavBarState1 = state1;
         mSharedState.disableNavBarState2 = state2;
-        if (mTaskbarActivityContext != null) {
-            mTaskbarActivityContext.disableNavBarElements(displayId, state1, state2, animate);
+        TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId);
+        if (taskbar != null) {
+            taskbar.disableNavBarElements(displayId, state1, state2, animate);
         }
     }
 
     public void onSystemBarAttributesChanged(int displayId, int behavior) {
         mSharedState.systemBarAttrsDisplayId = displayId;
         mSharedState.systemBarAttrsBehavior = behavior;
-        if (mTaskbarActivityContext != null) {
-            mTaskbarActivityContext.onSystemBarAttributesChanged(displayId, behavior);
+        TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId);
+        if (taskbar != null) {
+            taskbar.onSystemBarAttributesChanged(displayId, behavior);
         }
     }
 
     public void onTransitionModeUpdated(int barMode, boolean checkBarModes) {
         mSharedState.barMode = barMode;
-        if (mTaskbarActivityContext != null) {
-            mTaskbarActivityContext.onTransitionModeUpdated(barMode, checkBarModes);
+        TaskbarActivityContext taskbar = getTaskbarForDisplay(getDefaultDisplayId());
+        if (taskbar != null) {
+            taskbar.onTransitionModeUpdated(barMode, checkBarModes);
         }
     }
 
     public void onNavButtonsDarkIntensityChanged(float darkIntensity) {
         mSharedState.navButtonsDarkIntensity = darkIntensity;
-        if (mTaskbarActivityContext != null) {
-            mTaskbarActivityContext.onNavButtonsDarkIntensityChanged(darkIntensity);
+        TaskbarActivityContext taskbar = getTaskbarForDisplay(getDefaultDisplayId());
+        if (taskbar != null) {
+            taskbar.onNavButtonsDarkIntensityChanged(darkIntensity);
         }
     }
 
     public void onNavigationBarLumaSamplingEnabled(int displayId, boolean enable) {
         mSharedState.mLumaSamplingDisplayId = displayId;
         mSharedState.mIsLumaSamplingEnabled = enable;
-        if (mTaskbarActivityContext != null) {
-            mTaskbarActivityContext.onNavigationBarLumaSamplingEnabled(displayId, enable);
+        TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId);
+        if (taskbar != null) {
+            taskbar.onNavigationBarLumaSamplingEnabled(displayId, enable);
         }
     }
 
@@ -634,11 +713,11 @@
      * Called when the manager is no longer needed
      */
     public void destroy() {
+        mRecentsViewContainer = null;
         debugWhyTaskbarNotDestroyed("TaskbarManager#destroy()");
         removeActivityCallbacksAndListeners();
         mTaskbarBroadcastReceiver.unregisterReceiverSafely(mContext);
-        destroyExistingTaskbar();
-        removeTaskbarRootViewFromWindow();
+        destroyAllTaskbars();
         if (mUserUnlocked) {
             DisplayController.INSTANCE.get(mContext).removeChangeListener(mRecreationListener);
         }
@@ -647,53 +726,112 @@
         SettingsCache.INSTANCE.get(mContext)
                 .unregister(NAV_BAR_KIDS_MODE, mOnSettingsChangeListener);
         Log.d(TASKBAR_NOT_DESTROYED_TAG, "unregistering component callbacks from destroy().");
-        mContext.unregisterComponentCallbacks(mComponentCallbacks);
+        mContext.unregisterComponentCallbacks(mDefaultComponentCallbacks);
         mShutdownReceiver.unregisterReceiverSafely(mContext);
     }
 
     public @Nullable TaskbarActivityContext getCurrentActivityContext() {
-        return mTaskbarActivityContext;
+        return getTaskbarForDisplay(mContext.getDisplayId());
     }
 
     public void dumpLogs(String prefix, PrintWriter pw) {
         pw.println(prefix + "TaskbarManager:");
-        if (mTaskbarActivityContext == null) {
+        TaskbarActivityContext taskbar = getTaskbarForDisplay(getDefaultDisplayId());
+        if (taskbar == null) {
             pw.println(prefix + "\tTaskbarActivityContext: null");
         } else {
-            mTaskbarActivityContext.dumpLogs(prefix + "\t", pw);
+            taskbar.dumpLogs(prefix + "\t", pw);
         }
     }
 
-    /**
-     * 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,
-                    mTaskbarActivityContext.getWindowLayoutParams());
+    private void addTaskbarRootViewToWindow(int displayId) {
+        TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId);
+        if (enableTaskbarNoRecreate() && !mAddedWindow && taskbar != null) {
+            mWindowManager.addView(getTaskbarRootLayoutForDisplay(displayId),
+                    taskbar.getWindowLayoutParams());
             mAddedWindow = true;
         }
     }
 
-    private void removeTaskbarRootViewFromWindow() {
-        if (enableTaskbarNoRecreate() && mAddedWindow) {
-            mWindowManager.removeViewImmediate(mTaskbarRootLayout);
+    private void removeTaskbarRootViewFromWindow(int displayId) {
+        FrameLayout rootLayout = getTaskbarRootLayoutForDisplay(displayId);
+        if (enableTaskbarNoRecreate() && mAddedWindow && rootLayout != null) {
+            mWindowManager.removeViewImmediate(rootLayout);
             mAddedWindow = false;
+            removeTaskbarRootLayoutFromMap(displayId);
         }
     }
 
+    /**
+     * Returns the {@link TaskbarActivityContext} associated with the given display ID.
+     *
+     * @param displayId The ID of the display to retrieve the taskbar for.
+     * @return The {@link TaskbarActivityContext} for the specified display, or
+     *         {@code null} if no taskbar is associated with that display.
+     */
+    private TaskbarActivityContext getTaskbarForDisplay(int displayId) {
+        return mTaskbars.get(displayId);
+    }
+
+    /**
+     * Adds the {@link TaskbarActivityContext} associated with the given display ID to taskbar
+     * map if there is not already a taskbar mapped to that displayId.
+     *
+     * @param displayId The ID of the display to retrieve the taskbar for.
+     * @param newTaskbar The new {@link TaskbarActivityContext} to add to the map.
+     */
+    private void addTaskbarToMap(int displayId, TaskbarActivityContext newTaskbar) {
+        if (!mTaskbars.contains(displayId)) {
+            mTaskbars.put(displayId, newTaskbar);
+        }
+    }
+
+    /**
+     * Removes the taskbar associated with the given display ID from the taskbar map.
+     *
+     * @param displayId The ID of the display for which to remove the taskbar.
+     */
+    private void removeTaskbarFromMap(int displayId) {
+        mTaskbars.delete(displayId);
+    }
+
+    /**
+     * Retrieves the root layout of the taskbar for the specified display.
+     *
+     * @param displayId The ID of the display for which to retrieve the taskbar root layout.
+     * @return The taskbar root layout {@link FrameLayout} for a given display or {@code null}.
+     */
+    private FrameLayout getTaskbarRootLayoutForDisplay(int displayId) {
+        return mRootLayouts.get(displayId);
+    }
+
+    /**
+     * Adds the taskbar root layout {@link FrameLayout} to taskbar map, mapped to display ID.
+     *
+     * @param displayId The ID of the display to associate with the taskbar root layout.
+     * @param rootLayout The taskbar root layout {@link FrameLayout} to add to the map.
+     */
+    private void addTaskbarRootLayoutToMap(int displayId, FrameLayout rootLayout) {
+        if (!mRootLayouts.contains(displayId)) {
+            mRootLayouts.put(displayId, rootLayout);
+        }
+    }
+
+    /**
+     * Removes taskbar root layout {@link FrameLayout} for given display ID from the taskbar map.
+     *
+     * @param displayId The ID of the display for which to remove the taskbar root layout.
+     */
+    private void removeTaskbarRootLayoutFromMap(int displayId) {
+        if (mRootLayouts.contains(displayId)) {
+            mRootLayouts.delete(displayId);
+        }
+    }
+
+    private int getDefaultDisplayId() {
+        return mContext.getDisplayId();
+    }
+
     /** Temp logs for b/254119092. */
     public void debugWhyTaskbarNotDestroyed(String debugReason) {
         StringJoiner log = new StringJoiner("\n");
@@ -735,4 +873,9 @@
 
     private final DeviceProfile.OnDeviceProfileChangeListener mDebugActivityDeviceProfileChanged =
             dp -> debugWhyTaskbarNotDestroyed("mActivity onDeviceProfileChanged");
+
+    @VisibleForTesting
+    public Context getWindowContext() {
+        return mContext;
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java
index bdefea6..f905c5f 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) {
-        mContainer.updateHotseatItems(hotseatItemInfos, recentTasks);
-        mControllers.taskbarViewController.updateIconViewsRunningStates(
-                runningTaskIds, minimizedTaskIds);
+    private void commitHotseatItemUpdates(
+            ItemInfo[] hotseatItemInfos, List<GroupTask> recentTasks) {
+        mContainer.updateItems(hotseatItemInfos, recentTasks);
+        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..4881836 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java
@@ -16,6 +16,9 @@
 
 package com.android.launcher3.taskbar;
 
+import static android.view.KeyEvent.ACTION_DOWN;
+import static android.view.KeyEvent.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 +34,15 @@
 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.os.SystemClock;
 import android.util.Log;
 import android.view.HapticFeedbackConstants;
+import android.view.KeyEvent;
 import android.view.View;
 import android.view.inputmethod.Flags;
 
@@ -51,7 +57,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;
 
@@ -74,6 +80,7 @@
     private long mLastScreenPinLongPress;
     private boolean mScreenPinned;
     private boolean mAssistantLongPressEnabled;
+    private int mLastSentBackAction = ACTION_UP;
 
     @Override
     public void dumpLogs(String prefix, PrintWriter pw) {
@@ -81,6 +88,8 @@
 
         pw.println(prefix + "\tmLastScreenPinLongPress=" + mLastScreenPinLongPress);
         pw.println(prefix + "\tmScreenPinned=" + mScreenPinned);
+        pw.println(prefix + "\tmLastSentBackAction="
+                + KeyEvent.actionToString(mLastSentBackAction));
     }
 
     @Retention(RetentionPolicy.SOURCE)
@@ -113,7 +122,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,27 +133,29 @@
             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) {
         if (buttonType == BUTTON_SPACE) {
             return;
         }
+        if (predictiveBackThreeButtonNav() && mLastSentBackAction == ACTION_DOWN) {
+            Log.i(TAG, "Button click ignored while back button is pressed");
+            // prevent interactions with other buttons while back button is pressed
+            return;
+        }
         // Provide the same haptic feedback that the system offers for virtual keys.
         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);
@@ -179,10 +190,19 @@
         if (buttonType == BUTTON_SPACE) {
             return false;
         }
+        if (predictiveBackThreeButtonNav() && mLastSentBackAction == ACTION_DOWN
+                && buttonType != BUTTON_BACK && buttonType != BUTTON_RECENTS) {
+            // prevent interactions with other buttons while back button is pressed (except back
+            // and recents button for screen-unpin action).
+            Log.i(TAG, "Button long click ignored while back button is pressed");
+            return false;
+        }
 
         // 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) {
@@ -278,6 +298,10 @@
     }
 
     private void resetScreenUnpin() {
+        // if only back button was long pressed, navigate back like a single click back behavior.
+        if (mLongPressedButtons == BUTTON_BACK) {
+            executeBack(null);
+        }
         mLongPressedButtons = 0;
         mLastScreenPinLongPress = 0;
     }
@@ -320,8 +344,27 @@
         mCallbacks.onToggleOverview();
     }
 
-    private void executeBack() {
-        mSystemUiProxy.onBackPressed();
+    void sendBackKeyEvent(int action, boolean cancelled) {
+        if (action == mLastSentBackAction) {
+            // There must always be an alternating sequence of ACTION_DOWN and ACTION_UP events
+            return;
+        }
+        long time = SystemClock.uptimeMillis();
+        KeyEvent keyEvent = new KeyEvent(time, time, action, KeyEvent.KEYCODE_BACK, 0);
+        if (cancelled) {
+            keyEvent.cancel();
+        }
+        executeBack(keyEvent);
+    }
+
+    private 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);
+        mLastSentBackAction = keyEvent != null ? keyEvent.getAction() : ACTION_UP;
     }
 
     private void onImeSwitcherPress() {
@@ -344,8 +387,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..8775766
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarOverflowView.java
@@ -0,0 +1,490 @@
+/*
+ * 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.icons.IconNormalizer;
+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 float LEAVE_BEHIND_SIZE_SCALE_DOWN_MULTIPLIER = 0.83f;
+    private static final int MAX_ITEMS_IN_PREVIEW = 4;
+
+    // The height divided by the width of the horizontal box containing two overlapping app icons.
+    // According to the spec, this ratio is constant for different sizes of taskbar app icons.
+    // Assuming the width of this box = taskbar app icon size - 2 paddings - 2 stroke widths, and
+    // the height = width * 0.61, which is also equal to the height of a single item in the preview.
+    private static final float TWO_ITEM_ICONS_BOX_ASPECT_RATIO = 0.61f;
+
+    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 taskbarIconRadius =
+                iconSize * IconNormalizer.ICON_VISIBLE_AREA_FACTOR / 2f - padding;
+
+        icon.mLeaveBehindSizeDefault = taskbarIconRadius;  // 1/2 of taskbar app icon size
+        icon.mLeaveBehindSizeScaledDown =
+                icon.mLeaveBehindSizeDefault * LEAVE_BEHIND_SIZE_SCALE_DOWN_MULTIPLIER;
+        icon.mLeaveBehindSize = icon.mLeaveBehindSizeScaledDown;
+
+        icon.mItemIconStrokeWidthDefault = taskbarIconRadius / 5f;  // 1/10 of taskbar app icon size
+        icon.mItemIconStrokeWidth = icon.mItemIconStrokeWidthDefault;
+
+        icon.mItemIconSizeDefault = 2 * (taskbarIconRadius - icon.mItemIconStrokeWidthDefault)
+                * TWO_ITEM_ICONS_BOX_ASPECT_RATIO;
+        icon.mItemIconSizeScaledDown = icon.mLeaveBehindSizeScaledDown;
+        icon.mItemIconSize = icon.mItemIconSizeDefault;
+
+        icon.mItemIconCenterOffsetDefault = taskbarIconRadius - icon.mItemIconSizeDefault / 2f
+                - icon.mItemIconStrokeWidthDefault;
+        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);
+
+        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/TaskbarPopupController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java
index 70d4bb1..2e0bae5 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java
@@ -201,8 +201,10 @@
         if (com.android.wm.shell.Flags.enableBubbleAnything()) {
             shortcuts.add(BUBBLE);
         }
+
         if (Flags.enableMultiInstanceMenuTaskbar()
-                && DesktopModeStatus.canEnterDesktopMode(mContext)) {
+                && DesktopModeStatus.canEnterDesktopMode(mContext)
+                && !mControllers.taskbarStashController.isInOverview()) {
             shortcuts.addAll(getMultiInstanceMenuOptions().toList());
         }
         return shortcuts.stream();
@@ -295,9 +297,9 @@
      * Returns a stream of Multi Instance menu options if an app supports it.
      */
     Stream<SystemShortcut.Factory<BaseTaskbarContext>> getMultiInstanceMenuOptions() {
-        SystemShortcut.Factory<BaseTaskbarContext> factory = createNewWindowShortcutFactory();
-        return factory != null ? Stream.of(factory) : Stream.empty();
-
+        SystemShortcut.Factory<BaseTaskbarContext> f1 = createNewWindowShortcutFactory();
+        SystemShortcut.Factory<BaseTaskbarContext> f2 = createManageWindowsShortcutFactory();
+        return f1 != null ? Stream.of(f1, f2) : Stream.empty();
     }
 
     /**
@@ -317,6 +319,23 @@
     }
 
     /**
+     * Creates a factory function representing a "Manage Windows" menu item only if the calling app
+     * supports multi-instance. This menu item shows the open instances of the calling app.
+     * @return A factory function to be used in populating the long-press menu.
+     */
+    public SystemShortcut.Factory<BaseTaskbarContext> createManageWindowsShortcutFactory() {
+        return (context, itemInfo, originalView) -> {
+            ComponentKey key = itemInfo.getComponentKey();
+            AppInfo app = getApp(key);
+            if (app != null && app.supportsMultiInstance()) {
+                return new ManageWindowsTaskbarShortcut<>(context, itemInfo, originalView,
+                        mControllers);
+            }
+            return null;
+        };
+    }
+
+    /**
      * A single menu item ("Split left," "Split right," or "Split top") that executes a split
      * from the taskbar, as if the user performed a drag and drop split.
      * Includes an onClick method that initiates the actual split.
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 07e34aa..67be8da 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
@@ -23,6 +23,7 @@
 import static com.android.app.animation.Interpolators.LINEAR;
 import static com.android.internal.jank.InteractionJankMonitor.Configuration;
 import static com.android.launcher3.Flags.enableScalingRevealHomeAnimation;
+import static com.android.launcher3.QuickstepTransitionManager.PINNED_TASKBAR_TRANSITION_DURATION;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_TASKBAR_NAVBAR_UNIFICATION;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TRANSIENT_TASKBAR_HIDE;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TRANSIENT_TASKBAR_SHOW;
@@ -30,6 +31,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 +63,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 +84,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 +106,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 +136,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 +172,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.
@@ -209,6 +218,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 +268,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
@@ -383,6 +399,9 @@
      * Returns how long the stash/unstash animation should play.
      */
     public long getStashDuration() {
+        if (DisplayController.isPinnedTaskbar(mActivity)) {
+            return PINNED_TASKBAR_TRANSITION_DURATION;
+        }
         return DisplayController.isTransientTaskbar(mActivity)
                 ? TRANSIENT_TASKBAR_STASH_DURATION
                 : TASKBAR_STASH_DURATION;
@@ -395,12 +414,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 +471,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 +831,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 +983,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 +995,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 +1109,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 +1144,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 +1172,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 +1192,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 +1332,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 +1360,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..8b636dd 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();
@@ -191,6 +195,16 @@
         return true;
     }
 
+    public boolean isAnimatingToHotseat() {
+        return false;
+    }
+
+    /**
+     * Skips to the end of the animation to Hotseat - should only be used if
+     * {@link #isAnimatingToHotseat()} returns true.
+     */
+    public void endAnimationToHotseat() {}
+
     /** Returns {@code true} if Taskbar is currently within overview. */
     protected boolean isInOverviewUi() {
         return false;
@@ -433,6 +447,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..f2355b8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
@@ -20,11 +20,14 @@
 import static com.android.launcher3.BubbleTextView.DISPLAY_TASKBAR;
 import static com.android.launcher3.Flags.enableCursorHoverStates;
 import static com.android.launcher3.Flags.enableRecentsInTaskbar;
+import static com.android.launcher3.Flags.taskbarRecentsLayoutTransition;
 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR;
 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_FOLDER;
 import static com.android.launcher3.config.FeatureFlags.enableTaskbarPinning;
 import static com.android.launcher3.icons.IconNormalizer.ICON_VISIBLE_AREA_FACTOR;
 
+import static java.util.function.Predicate.not;
+
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Canvas;
@@ -64,13 +67,15 @@
 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.Arrays;
 import java.util.List;
+import java.util.Objects;
 import java.util.function.Predicate;
 
 /**
@@ -100,13 +105,15 @@
     @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;
+
+    private int mNextViewIndex;
 
     /**
      * Whether the divider is between Hotseat icons and Recents,
@@ -120,6 +127,13 @@
 
     private boolean mShouldTryStartAlign;
 
+    private int mMaxNumIcons = 0;
+    private int mIdealNumIcons = 0;
+
+    private final int mAllAppsButtonTranslationOffset;
+
+    private final int mNumStaticViews;
+
     public TaskbarView(@NonNull Context context) {
         this(context, null);
     }
@@ -139,8 +153,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,20 +183,100 @@
         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);
+
+        mNumStaticViews = taskbarRecentsLayoutTransition() && !mActivityContext.isPhoneMode()
+                ? addStaticViews() : 0;
+    }
+
+    /**
+     * @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);
+    }
+
+    /**
+     * Pre-adds views that are always children of this view for LayoutTransition support.
+     * <p>
+     * Normally these views are removed and re-added when updating hotseat and recents. This
+     * approach does not behave well with LayoutTransition, so we instead need to add them
+     * initially and avoid removing them during updates.
+     */
+    private int addStaticViews() {
+        int numStaticViews = 1;
+        addView(mAllAppsButtonContainer);
+        if (mActivityContext.getDeviceProfile().isQsbInline) {
+            addView(mQsb, mIsRtl ? 1 : 0);
+            mQsb.setVisibility(View.INVISIBLE);
+            numStaticViews++;
+        }
+        return numStaticViews;
     }
 
     @Override
@@ -269,10 +361,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 +372,14 @@
             mTaskbarOverflowView.setOnLongClickListener(
                     mControllerCallbacks.getOverflowOnLongClickListener());
         }
+        if (Flags.showTaskbarPinningPopupFromAnywhere()
+                && mActivityContext.getTaskbarFeatureEvaluator().getSupportsPinningPopup()) {
+            setOnTouchListener(mControllerCallbacks.getTaskbarTouchListener());
+        }
+
+        if (Flags.taskbarOverflow()) {
+            mMaxNumIcons = calculateMaxNumIcons();
+        }
     }
 
     private void removeAndRecycle(View view) {
@@ -293,32 +392,137 @@
         view.setTag(null);
     }
 
-    /**
-     * Inflates/binds the Hotseat views to show in the Taskbar given their ItemInfos.
-     */
-    protected void updateHotseatItems(ItemInfo[] hotseatItemInfos, List<GroupTask> recentTasks) {
-        int nextViewIndex = 0;
-        int numViewsAnimated = 0;
+    /** Inflates/binds the hotseat items and recent tasks to the view. */
+    protected void updateItems(ItemInfo[] hotseatItemInfos, List<GroupTask> recentTasks) {
+        // Filter out unsupported items.
+        hotseatItemInfos = Arrays.stream(hotseatItemInfos)
+                .filter(Objects::nonNull)
+                .toArray(ItemInfo[]::new);
+        // TODO(b/343289567 and b/316004172): support app pairs and desktop mode.
+        recentTasks = recentTasks.stream().filter(not(GroupTask::supportsMultipleTasks)).toList();
+
+        if (taskbarRecentsLayoutTransition()) {
+            updateItemsWithLayoutTransition(hotseatItemInfos, recentTasks);
+        } else {
+            updateItemsWithoutLayoutTransition(hotseatItemInfos, recentTasks);
+        }
+    }
+
+    private void updateItemsWithoutLayoutTransition(
+            ItemInfo[] hotseatItemInfos, List<GroupTask> recentTasks) {
+
+        mNextViewIndex = 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);
         }
         removeView(mQsb);
 
-        // Add Hotseat icons.
-        for (ItemInfo hotseatItemInfo : hotseatItemInfos) {
-            if (hotseatItemInfo == null) {
-                continue;
-            }
+        updateHotseatItems(hotseatItemInfos);
 
+        if (mTaskbarDividerContainer != null && !recentTasks.isEmpty()) {
+            addView(mTaskbarDividerContainer, mNextViewIndex++);
+            mAddedDividerForRecents = true;
+        }
+
+        updateRecents(recentTasks);
+
+        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 (mActivityContext.getDeviceProfile().isQsbInline) {
+            addView(mQsb, mIsRtl ? getChildCount() : 0);
+            // Always set QSB to invisible after re-adding.
+            mQsb.setVisibility(View.INVISIBLE);
+        }
+    }
+
+    private void updateItemsWithLayoutTransition(
+            ItemInfo[] hotseatItemInfos, List<GroupTask> recentTasks) {
+
+        // Skip static views and potential All Apps divider, if they are on the left.
+        mNextViewIndex = mIsRtl ? 0 : mNumStaticViews;
+        if (getChildAt(mNextViewIndex) == mTaskbarDividerContainer) {
+            mNextViewIndex++;
+        }
+
+        // Update left section.
+        if (mIsRtl) {
+            updateRecents(recentTasks.reversed());
+        } else {
+            updateHotseatItems(hotseatItemInfos);
+        }
+
+        // Now at theoretical position for recent apps divider.
+        updateRecentsDivider(!recentTasks.isEmpty());
+        if (getChildAt(mNextViewIndex) == mTaskbarDividerContainer) {
+            mNextViewIndex++;
+        }
+
+        // Update right section.
+        if (mIsRtl) {
+            updateHotseatItems(hotseatItemInfos);
+        } else {
+            updateRecents(recentTasks);
+        }
+
+        // Recents divider takes priority.
+        if (!mAddedDividerForRecents) {
+            updateAllAppsDivider();
+        }
+    }
+
+    private void updateRecentsDivider(boolean hasRecents) {
+        if (hasRecents && !mAddedDividerForRecents) {
+            mAddedDividerForRecents = true;
+
+            // Remove possible All Apps divider.
+            if (getChildAt(mNumStaticViews) == mTaskbarDividerContainer) {
+                mNextViewIndex--; // All Apps divider on the left. Need to account for removing it.
+            }
+            removeView(mTaskbarDividerContainer);
+
+            addView(mTaskbarDividerContainer, mNextViewIndex);
+        } else if (!hasRecents && mAddedDividerForRecents) {
+            mAddedDividerForRecents = false;
+            removeViewAt(mNextViewIndex);
+        }
+    }
+
+    private void updateAllAppsDivider() {
+        // Index where All Apps divider would be if it is already in Taskbar.
+        final int expectedAllAppsDividerIndex =
+                mIsRtl ? getChildCount() - mNumStaticViews - 1 : mNumStaticViews;
+        if (getChildAt(expectedAllAppsDividerIndex) == mTaskbarDividerContainer
+                && getChildCount() == mNumStaticViews + 1) {
+            // Only static views with divider so remove divider.
+            removeView(mTaskbarDividerContainer);
+        } else if (getChildAt(expectedAllAppsDividerIndex) != mTaskbarDividerContainer
+                && getChildCount() >= mNumStaticViews + 1) {
+            // Static views with at least one app icon so add divider. For RTL, add it after the
+            // icon that is at the expected index.
+            addView(
+                    mTaskbarDividerContainer,
+                    mIsRtl ? expectedAllAppsDividerIndex + 1 : expectedAllAppsDividerIndex);
+        }
+    }
+
+    private void updateHotseatItems(ItemInfo[] hotseatItemInfos) {
+        int numViewsAnimated = 0;
+
+        for (ItemInfo hotseatItemInfo : hotseatItemInfos) {
             // Replace any Hotseat views with the appropriate type if it's not already that type.
             final int expectedLayoutResId;
             boolean isCollection = false;
@@ -334,8 +538,8 @@
             }
 
             View hotseatView = null;
-            while (nextViewIndex < getChildCount()) {
-                hotseatView = getChildAt(nextViewIndex);
+            while (isNextViewInSection(ItemInfo.class)) {
+                hotseatView = getChildAt(mNextViewIndex);
 
                 // see if the view can be reused
                 if ((hotseatView.getSourceLayoutResId() != expectedLayoutResId)
@@ -376,7 +580,7 @@
                 }
                 LayoutParams lp = new LayoutParams(mIconTouchSize, mIconTouchSize);
                 hotseatView.setPadding(mItemPadding, mItemPadding, mItemPadding, mItemPadding);
-                addView(hotseatView, nextViewIndex, lp);
+                addView(hotseatView, mNextViewIndex, lp);
             }
 
             // Apply the Hotseat ItemInfos, or hide the view if there is none for a given index.
@@ -392,24 +596,58 @@
             if (enableCursorHoverStates()) {
                 setHoverListenerForIcon(hotseatView);
             }
-            nextViewIndex++;
+            mNextViewIndex++;
         }
 
-        if (mTaskbarDividerContainer != null && !recentTasks.isEmpty()) {
-            addView(mTaskbarDividerContainer, nextViewIndex++);
-            mAddedDividerForRecents = true;
-            if (mTaskbarOverflowView != null) {
-                addView(mTaskbarOverflowView, nextViewIndex++);
+        while (isNextViewInSection(ItemInfo.class)) {
+            removeAndRecycle(getChildAt(mNextViewIndex));
+        }
+    }
+
+    private void updateRecents(List<GroupTask> recentTasks) {
+        // 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) {
+            mIdealNumIcons = mNextViewIndex + recentTasks.size() + nonTaskIconsToBeAdded;
+            overflowSize = mIdealNumIcons - mMaxNumIcons;
+
+            if (overflowSize > 0 && mTaskbarOverflowView != null) {
+                addView(mTaskbarOverflowView, mNextViewIndex++);
+            } 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) ? Math.min(overflowSize + 1, recentTasks.size()) : 0;
+        if (overflowSize > 0) {
+            overflownTasks = new ArrayList<>(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.
+                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 {
@@ -422,8 +660,8 @@
             }
 
             View recentIcon = null;
-            while (nextViewIndex < getChildCount()) {
-                recentIcon = getChildAt(nextViewIndex);
+            while (isNextViewInSection(GroupTask.class)) {
+                recentIcon = getChildAt(mNextViewIndex);
 
                 // see if the view can be reused
                 if ((recentIcon.getSourceLayoutResId() != expectedLayoutResId)
@@ -437,15 +675,11 @@
             }
 
             if (recentIcon == null) {
-                if (isCollection) {
-                    // TODO(b/343289567 and b/316004172): support app pairs and desktop mode.
-                    continue;
-                }
-
+                // TODO(b/343289567 and b/316004172): support app pairs and desktop mode.
                 recentIcon = inflate(expectedLayoutResId);
                 LayoutParams lp = new LayoutParams(mIconTouchSize, mIconTouchSize);
                 recentIcon.setPadding(mItemPadding, mItemPadding, mItemPadding, mItemPadding);
-                addView(recentIcon, nextViewIndex, lp);
+                addView(recentIcon, mNextViewIndex, lp);
             }
 
             if (recentIcon instanceof BubbleTextView btv) {
@@ -455,29 +689,17 @@
             if (enableCursorHoverStates()) {
                 setHoverListenerForIcon(recentIcon);
             }
-            nextViewIndex++;
+            mNextViewIndex++;
         }
 
-        // Remove remaining views
-        while (nextViewIndex < getChildCount()) {
-            removeAndRecycle(getChildAt(nextViewIndex));
+        while (isNextViewInSection(GroupTask.class)) {
+            removeAndRecycle(getChildAt(mNextViewIndex));
         }
+    }
 
-        if (mAllAppsButtonContainer != null) {
-            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 (mActivityContext.getDeviceProfile().isQsbInline) {
-            addView(mQsb, mIsRtl ? getChildCount() : 0);
-            // Always set QSB to invisible after re-adding.
-            mQsb.setVisibility(View.INVISIBLE);
-        }
+    private boolean isNextViewInSection(Class<?> tagClass) {
+        return mNextViewIndex < getChildCount()
+                && tagClass.isInstance(getChildAt(mNextViewIndex).getTag());
     }
 
     /** Binds the GroupTask to the BubbleTextView to be ready to present to the user. */
@@ -604,6 +826,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 +866,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 +917,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,25 +932,42 @@
             // 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;
     }
 
     /**
+     * The max number of icon views the taskbar can have when taskbar overflow is enabled.
+     */
+    int getMaxNumIconViews() {
+        return mMaxNumIcons;
+    }
+
+    /**
      * Returns the all apps button in the taskbar.
      */
-    @Nullable
     public TaskbarAllAppsButtonContainer getAllAppsButtonContainer() {
         return mAllAppsButtonContainer;
     }
@@ -726,7 +984,7 @@
      * Returns the taskbar overflow view in the taskbar.
      */
     @Nullable
-    public IconButtonView getTaskbarOverflowView() {
+    public TaskbarOverflowView getTaskbarOverflowView() {
         return mTaskbarOverflowView;
     }
 
@@ -784,12 +1042,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 a69f453..bc5f9a3 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;
 
@@ -117,7 +120,7 @@
     private final TaskbarView mTaskbarView;
     private final MultiValueAlpha mTaskbarIconAlpha;
     private final AnimatedFloat mTaskbarIconScaleForStash = new AnimatedFloat(this::updateScale);
-    private final AnimatedFloat mTaskbarIconTranslationYForHome = new AnimatedFloat(
+    public final AnimatedFloat mTaskbarIconTranslationYForHome = new AnimatedFloat(
             this::updateTranslationY);
     private final AnimatedFloat mTaskbarIconTranslationYForStash = new AnimatedFloat(
             this::updateTranslationY);
@@ -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
@@ -346,6 +351,11 @@
         OneShotPreDrawListener.add(mTaskbarView, listener);
     }
 
+    @VisibleForTesting
+    int getMaxNumIconViews() {
+        return mTaskbarView.getMaxNumIconViews();
+    }
+
     public Rect getIconLayoutVisualBounds() {
         return mTaskbarView.getIconLayoutVisualBounds();
     }
@@ -354,15 +364,10 @@
         return mTaskbarView.getIconLayoutBounds();
     }
 
-    public int getIconLayoutWidth() {
-        return mTaskbarView.getIconLayoutWidth();
-    }
-
     public View[] getIconViews() {
         return mTaskbarView.getIconViews();
     }
 
-    @Nullable
     public View getAllAppsButtonView() {
         return mTaskbarView.getAllAppsButtonContainer();
     }
@@ -438,6 +443,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;
@@ -445,6 +463,7 @@
 
         if (mIsRtl) {
             allAppIconTranslateRange *= -1;
+            sizeDiffTranslationRange *= -1;
         }
 
         if (mActivity.isThreeButtonNav()) {
@@ -453,25 +472,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(
@@ -484,18 +496,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);
@@ -620,36 +628,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;
     }
@@ -771,9 +784,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();
+        }
     }
 
     /**
@@ -781,6 +801,8 @@
      */
     private AnimatorPlaybackController createIconAlignmentController(DeviceProfile launcherDp) {
         PendingAnimation setter = new PendingAnimation(100);
+        // icon alignment not needed for pinned taskbar.
+        if (DisplayController.isPinnedTaskbar(mActivity)) return setter.createPlaybackController();
         mOnControllerPreCreateCallback.run();
         DeviceProfile taskbarDp = mActivity.getDeviceProfile();
         Rect hotseatPadding = launcherDp.getHotseatLayoutPadding(mActivity);
@@ -813,6 +835,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();
@@ -848,16 +877,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)
@@ -866,8 +899,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);
@@ -902,10 +935,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;
@@ -914,6 +949,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();
@@ -1054,6 +1090,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..987937e 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,7 +114,7 @@
 
     private BubbleBarItem mSelectedBubble;
 
-    private ImeVisibilityChecker mImeVisibilityChecker;
+    private TaskbarSharedState mSharedState;
     private BubbleBarViewController mBubbleBarViewController;
     private BubbleStashController mBubbleStashController;
     private Optional<BubbleStashedHandleViewController> mBubbleStashedHandleViewController;
@@ -123,6 +125,8 @@
     // Cache last sent top coordinate to avoid sending duplicate updates to shell
     private int mLastSentBubbleBarTop;
 
+    private boolean mIsImeVisible = false;
+
     /**
      * Similar to {@link BubbleBarUpdate} but rather than {@link BubbleInfo}s it uses
      * {@link BubbleBarBubble}s so that it can be used to update the views.
@@ -173,13 +177,24 @@
 
     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) {
-        mImeVisibilityChecker = imeVisibilityChecker;
+            TaskbarSharedState sharedState) {
+        mSharedState = sharedState;
         mBubbleBarViewController = bubbleControllers.bubbleBarViewController;
         mBubbleStashController = bubbleControllers.bubbleStashController;
         mBubbleStashedHandleViewController = bubbleControllers.bubbleStashedHandleViewController;
@@ -188,6 +203,7 @@
         mBubbleBarLocationListener = bubbleBarLocationListener;
 
         bubbleControllers.runAfterInit(() -> {
+            restoreSavedState(sharedState);
             mBubbleBarViewController.setHiddenForBubbles(
                     !sBubbleBarEnabled || mBubbles.isEmpty());
             mBubbleStashedHandleViewController.ifPresent(
@@ -196,7 +212,8 @@
             mBubbleBarViewController.setUpdateSelectedBubbleAfterCollapse(
                     key -> setSelectedBubbleInternal(mBubbles.get(key)));
             mBubbleBarViewController.setBoundsChangeListener(this::onBubbleBarBoundsChanged);
-
+            mBubbleBarLocationListener.onBubbleBarLocationUpdated(
+                    mBubbleBarViewController.getBubbleBarLocation());
             if (sBubbleBarEnabled) {
                 mSystemUiProxy.setBubblesListener(this);
             }
@@ -216,6 +233,10 @@
 
         boolean sysuiLocked = (flags & MASK_SYSUI_LOCKED) != 0;
         mBubbleStashController.setSysuiLocked(sysuiLocked);
+        mIsImeVisible = (flags & SYSUI_STATE_IME_SHOWING) != 0;
+        if (mIsImeVisible) {
+            mBubbleBarViewController.onImeVisible();
+        }
     }
 
     //
@@ -265,6 +286,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());
@@ -273,8 +312,13 @@
         // enabling gesture nav. also suppress animation if the bubble bar is hidden for sysui e.g.
         // the shade is open, or we're locked.
         final boolean suppressAnimation =
-                update.initialState || mBubbleBarViewController.isHiddenForSysui()
-                        || mImeVisibilityChecker.isImeVisible();
+                update.initialState || mBubbleBarViewController.isHiddenForSysui() || mIsImeVisible;
+
+        if (update.initialState && mSharedState.hasSavedBubbles()) {
+            // clear restored state
+            mBubbleBarViewController.removeAllBubbles();
+            mBubbles.clear();
+        }
 
         BubbleBarBubble bubbleToSelect = null;
 
@@ -346,8 +390,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 +419,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 +467,7 @@
             }
         }
         if (update.bubbleBarLocation != null) {
+            mSharedState.bubbleBarLocation = update.bubbleBarLocation;
             if (update.bubbleBarLocation != mBubbleBarViewController.getBubbleBarLocation()) {
                 updateBubbleBarLocationInternal(update.bubbleBarLocation);
             }
@@ -483,9 +530,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,10 +568,10 @@
         }
     }
 
-    /** Interface for checking whether the IME is visible. */
-    public interface ImeVisibilityChecker {
-        /** Whether the IME is visible. */
-        boolean isImeVisible();
+    private void addBubbleInternally(BubbleBarBubble bubble, boolean isExpanding,
+            boolean suppressAnimation) {
+        mBubbles.put(bubble.getKey(), bubble);
+        mBubbleBarViewController.addBubble(bubble, isExpanding, suppressAnimation);
     }
 
     /** Listener of {@link BubbleBarLocation} updates. */
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..833be61 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;
@@ -487,7 +456,7 @@
             return;
         }
         mDragging = dragging;
-        setElevation(dragging ? mDragElevation : mBubbleElevation);
+        mController.setIsDragging(dragging);
         if (!mDragging) {
             // Relayout after dragging to ensure that the dragged bubble is positioned correctly
             requestLayout();
@@ -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,10 @@
         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);
+
+        /** Notifies the controller that bubble bar is being dragged */
+        void setIsDragging(boolean dragging);
     }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index 025c038..fd08078 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,11 +75,17 @@
     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;
     private int mIconSize;
     private int mBubbleBarPadding;
+    private final int mDragElevation;
 
     // Initialized in init.
     private BubbleStashController mBubbleStashController;
@@ -96,6 +114,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 +128,50 @@
     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);
+        mDragElevation = activity.getResources().getDimensionPixelSize(
+                R.dimen.bubblebar_drag_elevation);
+        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 +181,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 +192,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 +204,9 @@
 
             @Override
             public void onBubbleBarTouched() {
-                BubbleBarViewController.this.onBubbleBarTouched();
+                if (isAnimatingNewBubble()) {
+                    interruptAnimationForTouch();
+                }
             }
 
             @Override
@@ -175,8 +220,14 @@
             }
 
             @Override
-            public void updateBubbleBarLocation(BubbleBarLocation location) {
-                mBubbleBarController.updateBubbleBarLocation(location);
+            public void updateBubbleBarLocation(BubbleBarLocation location,
+                    @BubbleBarLocation.UpdateSource int source) {
+                mBubbleBarController.updateBubbleBarLocation(location, source);
+            }
+
+            @Override
+            public void setIsDragging(boolean dragging) {
+                mBubbleBarContainer.setElevation(dragging ? mDragElevation : 0);
             }
         });
 
@@ -200,13 +251,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 +354,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() {
@@ -261,6 +391,13 @@
         }
     }
 
+    /** Notifies that the IME became visible. */
+    public void onImeVisible() {
+        if (isAnimatingNewBubble()) {
+            mBubbleBarViewAnimator.interruptForIme();
+        }
+    }
+
     //
     // The below animators are exposed to BubbleStashController so it can manage the stashing
     // animation.
@@ -351,6 +488,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 +525,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 +617,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 +656,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 +675,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 +719,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 +884,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 +901,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 +953,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 +968,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();
         }
     }
 
@@ -793,10 +1012,9 @@
         }
     }
 
-    /** Marks as should show education and shows the bubble bar in a collapsed state */
+    /** Marks as should show education. */
     public void prepareToShowEducation() {
         mShouldShowEducation = true;
-        mBubbleStashController.showBubbleBar(false /* expand the bubbles */);
     }
 
     /**
@@ -876,6 +1094,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..cb592e6 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,16 @@
      * 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);
+                taskbarSharedState);
         bubbleStashedHandleViewController.ifPresent(
                 controller -> controller.init(/* bubbleControllers = */ this));
         bubbleStashController.init(
@@ -111,6 +116,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..92fd5e8 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,53 @@
     }
 
     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 drawing the dot due to an update for this bubble. */
+    public void suppressDotForBubbleUpdate() {
+        mDotSuppressedForBubbleUpdate = true;
+        setDotScale(0);
+    }
+
+    /**
+     * Unsuppresses the dot after the bubble update finished animating.
+     *
+     * @param animate whether or not to animate the dot back in
+     */
+    public void unsuppressDotForBubbleUpdate(boolean animate) {
+        mDotSuppressedForBubbleUpdate = false;
+        showDotIfNeeded(animate);
+    }
+
     boolean hasUnseenContent() {
         return mBubble != null
                 && mBubble instanceof BubbleBarBubble
@@ -353,8 +403,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 +444,7 @@
 
         clearAnimation();
         animate()
-                .setDuration(200)
+                .setDuration(SCALE_ANIMATION_DURATION_MS)
                 .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
                 .setUpdateListener((valueAnimator) -> {
                     float fraction = valueAnimator.getAnimatedFraction();
@@ -406,6 +456,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 +491,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..447dad1 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()
             }
         }
 
@@ -421,16 +473,45 @@
         ObjectAnimator.ofFloat(bubbleBarView, View.TRANSLATION_Y, ty - bubbleBarBounceDistanceInPx)
             .withDuration(BUBBLE_BAR_BOUNCE_ANIMATION_DURATION_MS)
             .withEndAction {
-                if (animatingBubble?.expand == true) expandBubbleBar()
                 springBackAnimation.start()
+                if (animatingBubble?.expand == true) expandBubbleBar()
             }
             .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() },
+                onEnd = {
+                    moveToState(AnimatingBubble.State.IN)
+                    bubbleStashController.updateTaskbarTouchRegion()
+                },
+            )
+        } else {
+            moveToState(AnimatingBubble.State.IN)
+        }
+    }
+
+    private fun cancelFlyout() {
+        animatingBubble?.bubbleView?.unsuppressDotForBubbleUpdate(/* animate= */ true)
+        bubbleBarFlyoutController.cancelFlyout { bubbleStashController.updateTaskbarTouchRegion() }
+    }
+
+    private fun onFlyoutRemoved() {
+        animatingBubble?.bubbleView?.unsuppressDotForBubbleUpdate(/* animate= */ 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 +520,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 +528,22 @@
         bubbleBarView.relativePivotY = 1f
         bubbleStashController.onNewBubbleAnimationInterrupted(
             /* isStashed= */ bubbleBarView.alpha == 0f,
-            bubbleBarView.translationY
+            bubbleBarView.translationY,
+        )
+    }
+
+    /** Interrupts the animation due to the IME becoming visible. */
+    fun interruptForIme() {
+        cancelFlyout()
+        val hideAnimation = animatingBubble?.hideAnimation ?: return
+        scheduler.cancel(hideAnimation)
+        animatingBubble = null
+        bubbleStashController.getStashedHandlePhysicsAnimator().cancelIfRunning()
+        bubbleBarView.relativePivotY = 1f
+        // stash the bubble bar since the IME is now visible
+        bubbleStashController.onNewBubbleAnimationInterrupted(
+            /* isStashed= */ true,
+            bubbleBarView.translationY,
         )
     }
 
@@ -455,11 +552,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()
+            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.unsuppressDotForBubbleUpdate(/* animate= */ 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()
+            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.unsuppressDotForBubbleUpdate(/* animate= */ 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()
+                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%
rename from tests/multivalentTests/src/com/android/launcher3/model/ModelTestRule.kt
rename 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..22d504f 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,36 @@
         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()) {
-                // if there are no bubbles, there's nothing to show, so just return.
+    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 no need to update the bubble bar, just keep the
+                // isStashed state up to date so that we can process state changes when bubbles are
+                // created.
+                isStashed = launcherState == BubbleLauncherState.IN_APP
                 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 +123,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 +175,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 +200,7 @@
         bubbleBarBackgroundScaleX.updateValue(1f)
         bubbleBarBackgroundScaleY.updateValue(1f)
         isStashed = false
+        bubbleBarViewController.setHiddenForStashed(false)
         onIsStashedChanged()
     }
 
@@ -209,6 +215,7 @@
         bubbleBarBackgroundScaleX.updateValue(getStashScaleX())
         bubbleBarBackgroundScaleY.updateValue(getStashScaleY())
         isStashed = true
+        bubbleBarViewController.setHiddenForStashed(true)
         onIsStashedChanged()
     }
 
@@ -305,7 +312,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 +325,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
                 }
@@ -481,14 +489,14 @@
         val isStashed = stash && !isBubblesShowingOnHome && !isBubblesShowingOnOverview
         if (this.isStashed != isStashed) {
             this.isStashed = isStashed
+
             // notify the view controller that the stash state is about to change so that it can
             // cancel an ongoing animation if there is one.
-            // note that this has to be called before updating mIsStashed with the new value,
-            // otherwise interrupting an ongoing animation may update it again with the wrong state
             bubbleBarViewController.onStashStateChanging()
             animator?.cancel()
             animator =
                 createStashAnimator(isStashed, BAR_STASH_DURATION).apply {
+                    updateBarVisibility(isStashed)
                     updateTouchRegionOnAnimationEnd()
                     start()
                 }
@@ -503,6 +511,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..e552b24 100644
--- a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarAllAppsButtonContainer.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarAllAppsButtonContainer.kt
@@ -21,13 +21,11 @@
 import android.content.res.ColorStateList
 import android.graphics.Color.TRANSPARENT
 import android.util.AttributeSet
-import android.view.LayoutInflater
 import android.view.MotionEvent
 import android.view.View
 import android.view.ViewConfiguration
 import androidx.annotation.DimenRes
 import androidx.annotation.DrawableRes
-import androidx.core.view.setPadding
 import com.android.launcher3.R
 import com.android.launcher3.Utilities.dpToPx
 import com.android.launcher3.config.FeatureFlags.enableTaskbarPinning
@@ -37,7 +35,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
@@ -57,7 +55,7 @@
         }
 
     init {
-        LayoutInflater.from(context).inflate(R.layout.taskbar_all_apps_button, null, false)
+        contentDescription = context.getString(R.string.all_apps_button_label)
         setUpIcon()
     }
 
@@ -69,7 +67,6 @@
             )
         backgroundTintList = ColorStateList.valueOf(TRANSPARENT)
         setIconDrawable(drawable)
-        setPadding(dpToPx(activityContext.taskbarSpecsEvaluator.taskbarIconPadding.toFloat()))
         setForegroundTint(activityContext.getColor(R.color.all_apps_button_color))
     }
 
@@ -79,17 +76,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/TaskbarDividerContainer.kt b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarDividerContainer.kt
index 1fb835a..df61d8a 100644
--- a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarDividerContainer.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarDividerContainer.kt
@@ -21,8 +21,6 @@
 import android.content.res.ColorStateList
 import android.graphics.Color.TRANSPARENT
 import android.util.AttributeSet
-import android.view.LayoutInflater
-import androidx.core.view.setPadding
 import com.android.launcher3.R
 import com.android.launcher3.Utilities.dpToPx
 import com.android.launcher3.taskbar.TaskbarActivityContext
@@ -33,11 +31,8 @@
 /** Taskbar divider view container for customizable taskbar. */
 class TaskbarDividerContainer
 @JvmOverloads
-constructor(
-    context: Context,
-    attrs: AttributeSet? = null,
-    defStyleAttr: Int = 0,
-) : IconButtonView(context, attrs), TaskbarContainer {
+constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
+    IconButtonView(context, attrs), TaskbarContainer {
     private val activityContext: TaskbarActivityContext = ActivityContext.lookupContext(context)
 
     override val spaceNeeded: Int
@@ -46,7 +41,7 @@
         }
 
     init {
-        LayoutInflater.from(context).inflate(R.layout.taskbar_divider, null, false)
+        contentDescription = context.getString(R.string.taskbar_divider_a11y_title)
         setUpIcon()
     }
 
@@ -55,7 +50,6 @@
         backgroundTintList = ColorStateList.valueOf(TRANSPARENT)
         val drawable = resources.getDrawable(R.drawable.taskbar_divider_button)
         setIconDrawable(drawable)
-        setPadding(dpToPx(activityContext.taskbarSpecsEvaluator.taskbarIconPadding.toFloat()))
     }
 
     @SuppressLint("ClickableViewAccessibility")
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/PredictedAppIcon.java b/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
index 4590efe..535ae1c 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
@@ -418,7 +418,9 @@
         mIconRingPaint.setColor(RING_SHADOW_COLOR);
         mIconRingPaint.setMaskFilter(mShadowFilter);
         int count = canvas.save();
-        canvas.scale(mRingScale, mRingScale, canvas.getWidth() / 2f, canvas.getHeight() / 2f);
+        if (Float.compare(1, mRingScale) != 0) {
+            canvas.scale(mRingScale, mRingScale, canvas.getWidth() / 2f, canvas.getHeight() / 2f);
+        }
         canvas.drawPath(mRingPath, mIconRingPaint);
         mIconRingPaint.setColor(mPlateColor);
         mIconRingPaint.setMaskFilter(null);
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index e80e838..cc51adc 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
@@ -259,6 +264,8 @@
 
     private boolean mIsOverlayVisible;
 
+    private final Runnable mOverviewTargetChangeRunnable = this::onOverviewTargetChanged;
+
     public static QuickstepLauncher getLauncher(Context context) {
         return fromContext(context);
     }
@@ -271,7 +278,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());
@@ -299,7 +306,7 @@
 
         mTISBindHelper = new TISBindHelper(this, this::onTISConnected);
         mDepthController = new DepthController(this);
-        if (DesktopModeStatus.canEnterDesktopMode(this)) {
+        if (DesktopModeStatus.canEnterDesktopModeOrShowAppHandle(this)) {
             mSplitSelectStateController.initSplitFromDesktopController(this,
                     overviewComponentObserver);
         }
@@ -463,7 +470,7 @@
         if (Flags.enablePrivateSpace()) {
             shortcuts.add(UNINSTALL_APP);
         }
-        if (com.android.wm.shell.Flags.enableBubbleAnything()) {
+        if (enableBubbleAnything()) {
             shortcuts.add(BUBBLE_SHORTCUT);
         }
         return shortcuts.stream();
@@ -545,6 +552,10 @@
             mUnfoldTransitionProgressProvider.destroy();
         }
 
+        TISBinder binder = mTISBindHelper.getBinder();
+        if (binder != null) {
+            binder.unregisterOverviewTargetChangeListener(mOverviewTargetChangeRunnable);
+        }
         mTISBindHelper.onDestroy();
 
         if (mLauncherUnfoldAnimationController != null) {
@@ -595,11 +606,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 +618,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 +685,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 +907,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 +922,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 +938,7 @@
                     }
 
                     @Override
-                    public void onBackProgressed(@NonNull BackEvent backEvent) {
+                    public void onBackProgressedCompat(@NonNull BackEvent backEvent) {
                         if (!FeatureFlags.IS_STUDIO_BUILD
                                 && mActiveOnBackAnimationCallback == null) {
                             return;
@@ -947,7 +947,7 @@
                     }
 
                     @Override
-                    public void onBackCancelled() {
+                    public void onBackCancelledCompat() {
                         if (!FeatureFlags.IS_STUDIO_BUILD
                                 && mActiveOnBackAnimationCallback == null) {
                             return;
@@ -1031,12 +1031,20 @@
         }
     }
 
+    private void onOverviewTargetChanged() {
+        QuickstepTransitionManager transitionManager = getAppTransitionManager();
+        if (transitionManager != null) {
+            transitionManager.onOverviewTargetChange();
+        }
+    }
+
     private void onTISConnected(TISBinder binder) {
         TaskbarManager taskbarManager = mTISBindHelper.getTaskbarManager();
         if (taskbarManager != null) {
             taskbarManager.setActivity(this);
         }
         mTISBindHelper.setPredictiveBackToHomeInProgress(mIsPredictiveBackToHomeInProgress);
+        binder.registerOverviewTargetChangeListener(mOverviewTargetChangeRunnable);
     }
 
     @Override
@@ -1097,14 +1105,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 +1328,10 @@
         mTISBindHelper.setPredictiveBackToHomeInProgress(isInProgress);
     }
 
+    public boolean getPredictiveBackToHomeInProgress() {
+        return mIsPredictiveBackToHomeInProgress;
+    }
+
     @Override
     public boolean areDesktopTasksVisible() {
         DesktopVisibilityController desktopVisibilityController = getDesktopVisibilityController();
@@ -1345,7 +1377,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 +1417,7 @@
     }
 
     @NonNull
+    @Override
     public TISBindHelper getTISBindHelper() {
         return mTISBindHelper;
     }
@@ -1410,6 +1443,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..374db6a 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/SystemApiWrapper.kt
+++ b/quickstep/src/com/android/launcher3/uioverrides/SystemApiWrapper.kt
@@ -23,6 +23,7 @@
 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
@@ -40,15 +41,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
 
@@ -76,7 +82,7 @@
                             UserManager.USER_TYPE_PROFILE_PRIVATE -> UserIconInfo.TYPE_PRIVATE
                             else -> UserIconInfo.TYPE_MAIN
                         },
-                        userSerialNumber.toLong()
+                        userSerialNumber.toLong(),
                     )
             }
         }
@@ -110,7 +116,7 @@
                             )
                             .toBundle()
                     requireActivityResult = false
-                }
+                },
             )
         else super.getAppMarketActivityIntent(packageName, user)
 
@@ -131,7 +137,7 @@
                             )
                             .toBundle()
                     requireActivityResult = false
-                }
+                },
             )
         else null
 
@@ -160,7 +166,7 @@
                             allowlistToken: IBinder?,
                             finishedReceiver: IIntentReceiver?,
                             requiredPermission: String?,
-                            options: Bundle?
+                            options: Bundle?,
                         ) {
                             if (code != -1) {
                                 Executors.MAIN_EXECUTOR.execute {
@@ -168,9 +174,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 +189,7 @@
             context.startActivity(ProxyActivityStarter.getLaunchIntent(context, params))
         }
     }
+
+    override fun getApplicationInfoHash(appInfo: ApplicationInfo): String =
+        (appInfo.sourceDir?.hashCode() ?: 0).toString() + " " + appInfo.longVersionCode
 }
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/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java b/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java
index 3a39cf2..8ad00bf 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java
@@ -95,6 +95,7 @@
     public void prepareForAtomicAnimation(LauncherState fromState, LauncherState toState,
             StateAnimationConfig config) {
         RecentsView overview = mContainer.getOverviewPanel();
+        boolean isPinnedTaskbar = DisplayController.isPinnedTaskbar(mContainer);
         if ((fromState == OVERVIEW || fromState == OVERVIEW_SPLIT_SELECT) && toState == NORMAL) {
             overview.switchToScreenshot(() ->
                     overview.finishRecentsAnimation(true /* toRecents */, null));
@@ -109,7 +110,8 @@
             // We sync the scrim fade with the taskbar animation duration to avoid any flickers for
             // taskbar icons disappearing before hotseat icons show up.
             float scrimUpperBoundFromSplit =
-                    QuickstepTransitionManager.getTaskbarToHomeDuration() / (float) config.duration;
+                    QuickstepTransitionManager.getTaskbarToHomeDuration(isPinnedTaskbar)
+                            / (float) config.duration;
             scrimUpperBoundFromSplit = Math.min(scrimUpperBoundFromSplit, 1f);
             config.setInterpolator(ANIM_OVERVIEW_ACTIONS_FADE, clampToProgress(LINEAR, 0, 0.25f));
             config.setInterpolator(ANIM_SCRIM_FADE,
@@ -139,7 +141,8 @@
                 // Sync scroll so that it ends before or at the same time as the taskbar animation.
                 if (mContainer.getDeviceProfile().isTaskbarPresent) {
                     config.duration = Math.min(
-                            config.duration, QuickstepTransitionManager.getTaskbarToHomeDuration());
+                            config.duration,
+                            QuickstepTransitionManager.getTaskbarToHomeDuration(isPinnedTaskbar));
                 }
                 overview.snapToPage(DEFAULT_PAGE, Math.toIntExact(config.duration));
             } else {
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index a6d651c..bf8cff6 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,13 +56,11 @@
 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.TaskViewUtils.extractTargetsAndStates;
 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;
+import static com.android.wm.shell.shared.ShellSharedConstants.KEY_EXTRA_SHELL_CAN_HAND_OFF_ANIMATION;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -80,6 +79,9 @@
 import android.os.IBinder;
 import android.os.SystemClock;
 import android.util.Log;
+import android.util.Pair;
+import android.util.TimeUtils;
+import android.view.Choreographer;
 import android.view.MotionEvent;
 import android.view.RemoteAnimationTarget;
 import android.view.SurfaceControl;
@@ -91,8 +93,9 @@
 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 android.window.WindowAnimationState;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -103,6 +106,7 @@
 import com.android.internal.util.LatencyTracker;
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.QuickstepTransitionManager;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimationSuccessListener;
@@ -112,6 +116,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 +125,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;
@@ -143,6 +149,7 @@
 import com.android.quickstep.views.RecentsViewContainer;
 import com.android.quickstep.views.TaskContainer;
 import com.android.quickstep.views.TaskView;
+import com.android.systemui.animation.TransitionAnimator;
 import com.android.systemui.contextualeducation.GestureType;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.ThumbnailData;
@@ -152,6 +159,7 @@
 import com.android.systemui.shared.system.SysUiStatsLog;
 import com.android.systemui.shared.system.TaskStackChangeListener;
 import com.android.systemui.shared.system.TaskStackChangeListeners;
+import com.android.wm.shell.Flags;
 import com.android.wm.shell.shared.TransactionPool;
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
 import com.android.wm.shell.shared.startingsurface.SplashScreenExitAnimationUtils;
@@ -171,11 +179,10 @@
  * 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 +192,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 +208,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 +245,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 +301,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
@@ -346,6 +355,9 @@
     // Indicates whether the divider is shown, only used when split screen is activated.
     private boolean mIsDividerShown = true;
     private boolean mStartMovingTasks;
+    // Whether the animation to home should be handed off to another handler once the gesture is
+    // committed.
+    protected boolean mHandOffAnimationToHome = false;
 
     @Nullable
     private RemoteAnimationTargets.ReleaseCheck mSwipePipToHomeReleaseCheck = null;
@@ -353,10 +365,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 +381,7 @@
                     endLauncherTransitionController();
                 }, new InputProxyHandlerFactory(mContainerInterface, mGestureState));
         mTaskAnimationManager = taskAnimationManager;
+        mRecentsWindowManager = recentsWindowManager;
         mTouchTimeMs = touchTimeMs;
         mContinuingLastGesture = continuingLastGesture;
 
@@ -450,7 +463,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 +490,7 @@
                 this::resetStateForAnimationCancel);
     }
 
-    protected boolean onActivityInit(Boolean alreadyOnHome) {
+    protected boolean onActivityInit(Boolean isHomeStarted) {
         if (mStateCallback.hasStates(STATE_HANDLER_INVALIDATED)) {
             return false;
         }
@@ -504,11 +518,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 +532,7 @@
         mRecentsView.setOnPageTransitionEndCallback(null);
 
         mStateCallback.setState(STATE_LAUNCHER_PRESENT);
-        if (alreadyOnHome) {
+        if (isHomeStarted) {
             onLauncherStart();
         } else {
             container.addEventCallback(EVENT_STARTED, mLauncherOnStartCallback);
@@ -665,7 +679,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();
         }
@@ -772,7 +786,7 @@
                 && recentsAttachedToAppWindow) {
             // Only move running task if RecentsView has never been attached before, to avoid
             // TaskView jumping to new position as we move the tasks.
-            mRecentsView.moveRunningTaskToFront();
+            mRecentsView.moveRunningTaskToExpectedPosition();
         }
         mAnimationFactory.setRecentsAttachedToAppWindow(
                 recentsAttachedToAppWindow, animate, updateRunningTaskAlpha);
@@ -819,7 +833,7 @@
             return;
         }
         initTransitionEndpoints(mContainer.getDeviceProfile());
-        mAnimationFactory.createActivityInterface(mTransitionDragLength);
+        mAnimationFactory.createContainerInterface(mTransitionDragLength);
     }
 
     /**
@@ -863,10 +877,13 @@
         }
     }
 
+    public Intent getHomeIntent() {
+        return mGestureState.getHomeIntent();
+    }
+
     public Intent getLaunchIntent() {
         return mGestureState.getOverviewIntent();
     }
-
     /**
      * Called when the value of {@link #mCurrentShift} changes
      */
@@ -939,6 +956,10 @@
         mSwipePipToHomeReleaseCheck = new RemoteAnimationTargets.ReleaseCheck();
         mSwipePipToHomeReleaseCheck.setCanRelease(true);
         mRecentsAnimationTargets.addReleaseCheck(mSwipePipToHomeReleaseCheck);
+        if (TransitionAnimator.Companion.longLivedReturnAnimationsEnabled()) {
+            mHandOffAnimationToHome =
+                    targets.extras.getBoolean(KEY_EXTRA_SHELL_CAN_HAND_OFF_ANIMATION, false);
+        }
 
         // Only initialize the device profile, if it has not been initialized before, as in some
         // configurations targets.homeContentInsets may not be correct.
@@ -975,10 +996,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 +1164,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 +1206,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 +1222,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 +1238,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.
@@ -1378,8 +1389,9 @@
             mInputConsumerProxy.enable();
         }
         if (endTarget == HOME) {
+            boolean isPinnedTaskbar = DisplayController.isPinnedTaskbar(mContext);
             duration = mContainer != null && mContainer.getDeviceProfile().isTaskbarPresent
-                    ? StaggeredWorkspaceAnim.DURATION_TASKBAR_MS
+                    ? QuickstepTransitionManager.getTaskbarToHomeDuration(isPinnedTaskbar)
                     : StaggeredWorkspaceAnim.DURATION_MS;
             ContextualEduStatsManager.INSTANCE.get(mContext).updateEduStats(
                     mGestureState.isTrackpadGesture(), GestureType.HOME);
@@ -1443,8 +1455,21 @@
                 onPageTransitionEnd.run();
             }
         }
+        long finalDuration = duration;
+        runOnRecentsAnimationAndLauncherBound(() -> animateGestureEnd(
+                startShift, endShift, finalDuration, interpolator, endTarget, velocityPxPerMs));
+    }
 
-        animateToProgress(startShift, endShift, duration, interpolator, endTarget, velocityPxPerMs);
+    @UiThread
+    protected void animateGestureEnd(
+            float startShift,
+            float endShift,
+            long duration,
+            @NonNull Interpolator interpolator,
+            @NonNull GestureEndTarget endTarget,
+            @NonNull PointF velocityPxPerMs) {
+        animateToProgressInternal(
+                startShift, endShift, duration, interpolator, endTarget, velocityPxPerMs);
     }
 
     private void doLogGesture(GestureEndTarget endTarget, @Nullable TaskView targetTask) {
@@ -1487,14 +1512,6 @@
         logger.log(event);
     }
 
-    /** Animates to the given progress, where 0 is the current app and 1 is overview. */
-    @UiThread
-    private void animateToProgress(float start, float end, long duration, Interpolator interpolator,
-            GestureEndTarget target, PointF velocityPxPerMs) {
-        runOnRecentsAnimationAndLauncherBound(() -> animateToProgressInternal(start, end, duration,
-                interpolator, target, velocityPxPerMs));
-    }
-
     protected abstract HomeAnimationFactory createHomeAnimationFactory(
             List<IBinder> launchCookies,
             long duration,
@@ -1548,9 +1565,12 @@
                     @Override
                     public void onAnimationEnd(Animator animation) {
                         mParallelRunningAnim = null;
+                        mStateCallback.setStateOnUiThread(STATE_PARALLEL_ANIM_FINISHED);
                     }
                 });
                 mParallelRunningAnim.start();
+            } else {
+                mStateCallback.setStateOnUiThread(STATE_PARALLEL_ANIM_FINISHED);
             }
         }
 
@@ -1629,6 +1649,10 @@
                 }
                 windowAnim = createWindowAnimationToHome(start, homeAnimFactory);
 
+                if (mHandOffAnimationToHome) {
+                    handOffAnimation(velocityPxPerMs);
+                }
+
                 windowAnim[0].addAnimatorListener(new AnimationSuccessListener() {
                     @Override
                     public void onAnimationSuccess(Animator animator) {
@@ -1711,6 +1735,36 @@
         }
     }
 
+    private void handOffAnimation(PointF velocityPxPerMs) {
+        if (!TransitionAnimator.Companion.longLivedReturnAnimationsEnabled()) {
+            return;
+        }
+
+        // This function is not guaranteed to be called inside a frame. We try to access the frame
+        // time immediately, but if we're not inside a frame we must post a callback to be run at
+        // the beginning of the next frame.
+        try  {
+            handOffAnimationInternal(Choreographer.getInstance().getFrameTime(), velocityPxPerMs);
+        } catch (IllegalStateException e) {
+            Choreographer.getInstance().postFrameCallback(
+                    frameTimeNanos -> handOffAnimationInternal(
+                            frameTimeNanos / TimeUtils.NANOS_PER_MS, velocityPxPerMs));
+        }
+    }
+
+    private void handOffAnimationInternal(long timestamp, PointF velocityPxPerMs) {
+        if (mRecentsAnimationController == null) {
+            return;
+        }
+
+        Pair<RemoteAnimationTarget[], WindowAnimationState[]> targetsAndStates =
+                extractTargetsAndStates(
+                        mRemoteTargetHandles, timestamp, velocityPxPerMs);
+        mRecentsAnimationController.handOffAnimation(
+                targetsAndStates.first, targetsAndStates.second);
+        ActiveGestureProtoLogProxy.logHandOffAnimation();
+    }
+
     private int calculateWindowRotation(RemoteAnimationTarget runningTaskTarget,
             RecentsOrientedState orientationState) {
         if (runningTaskTarget.rotationChange != 0) {
@@ -1981,15 +2035,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 +2059,7 @@
             mGestureEndCallback.run();
         }
 
-        mActivityInitListener.unregister("AbsSwipeUpHandler.invalidateHandler");
+        mContextInitListener.unregister("AbsSwipeUpHandler.invalidateHandler");
         TaskStackChangeListeners.getInstance().unregisterTaskStackListener(
                 mActivityRestartListener);
         mTaskSnapshotCache.clear();
@@ -2178,8 +2230,9 @@
                     mSwipePipToHomeAnimator.getFinishTransaction(),
                     mSwipePipToHomeAnimator.getContentOverlay());
             mIsSwipingPipToHome = false;
-        } else if (mIsSwipeForSplit) {
+        } else if (mIsSwipeForSplit && !Flags.enablePip2()) {
             // Transaction to hide the task to avoid flicker for entering PiP from split-screen.
+            // Note: PiP2 handles entering differently, so skip if enable_pip2=true
             PictureInPictureSurfaceTransaction tx =
                     new PictureInPictureSurfaceTransaction.Builder()
                             .setAlpha(0f)
@@ -2284,18 +2337,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 +2354,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 +2432,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 +2461,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 +2471,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 +2534,7 @@
         if (mRecentsAnimationController != null) {
             mRecentsAnimationController.finish(false /* toRecents */, onFinishComplete);
         }
-        ActiveGestureLog.INSTANCE.addLog("finishRecentsAnimationOnTasksAppeared");
+        ActiveGestureProtoLogProxy.logFinishRecentsAnimationOnTasksAppeared();
     }
 
     /**
@@ -2520,7 +2569,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/DesktopFullscreenDrawParams.kt b/quickstep/src/com/android/quickstep/DesktopFullscreenDrawParams.kt
new file mode 100644
index 0000000..bafb0b2
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/DesktopFullscreenDrawParams.kt
@@ -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.quickstep
+
+import android.content.Context
+import com.android.systemui.shared.system.QuickStepContract
+
+// DesktopTaskView thumbnail's corner radius is independent of fullscreenProgress.
+open class DesktopFullscreenDrawParams
+@JvmOverloads
+constructor(context: Context, cornerRadiusProvider: (Context) -> Float = ::computeCornerRadius) :
+    FullscreenDrawParams(context, cornerRadiusProvider, cornerRadiusProvider) {
+    companion object {
+        // computeCornerRadius is used as cornerRadiusProvider, so
+        // QuickStepContract::getWindowCornerRadius can be mocked properly.
+        private fun computeCornerRadius(context: Context): Float =
+            QuickStepContract.getWindowCornerRadius(context)
+    }
+}
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..9b56fd4 100644
--- a/quickstep/src/com/android/quickstep/FallbackSwipeHandler.java
+++ b/quickstep/src/com/android/quickstep/FallbackSwipeHandler.java
@@ -64,7 +64,6 @@
 import com.android.launcher3.util.DisplayController;
 import com.android.quickstep.fallback.FallbackRecentsView;
 import com.android.quickstep.fallback.RecentsState;
-import com.android.quickstep.util.ActiveGestureLog;
 import com.android.quickstep.util.RectFSpringAnim;
 import com.android.quickstep.util.SurfaceTransaction.SurfaceProperties;
 import com.android.quickstep.util.TransformParams;
@@ -82,7 +81,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 +104,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();
@@ -171,16 +170,14 @@
     }
 
     @Override
-    protected boolean handleTaskAppeared(@NonNull RemoteAnimationTarget[] appearedTaskTarget,
-            @NonNull ActiveGestureLog.CompoundString failureReason) {
-        if (mActiveAnimationFactory != null
-                && mActiveAnimationFactory.handleHomeTaskAppeared(appearedTaskTarget)) {
+    public void onTasksAppeared(@NonNull RemoteAnimationTarget[] appearedTaskTargets) {
+        if (mActiveAnimationFactory != null && mActiveAnimationFactory.handleHomeTaskAppeared(
+                appearedTaskTargets)) {
             mActiveAnimationFactory = null;
-            failureReason.append("(FallbackSwipeHandler) should be handled as home task appeared");
-            return false;
+            return;
         }
 
-        return super.handleTaskAppeared(appearedTaskTarget, failureReason);
+        super.onTasksAppeared(appearedTaskTargets);
     }
 
     @Override
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/FullscreenDrawParams.kt b/quickstep/src/com/android/quickstep/FullscreenDrawParams.kt
new file mode 100644
index 0000000..a5ba52a
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/FullscreenDrawParams.kt
@@ -0,0 +1,81 @@
+/*
+ * 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.Context
+import com.android.launcher3.R
+import com.android.launcher3.Utilities
+import com.android.launcher3.util.DisplayController
+import com.android.launcher3.util.SafeCloseable
+import com.android.launcher3.views.ActivityContext
+import com.android.quickstep.util.TaskCornerRadius
+import com.android.systemui.shared.system.QuickStepContract
+
+/**
+ * Class for computing corner radius by interpolating between overview and fullscreen corner radius
+ * with fullscreenProgress set in [setProgress].
+ */
+open class FullscreenDrawParams
+@JvmOverloads
+constructor(
+    context: Context,
+    private val taskCornerRadiusProvider: (Context) -> Float = ::computeTaskCornerRadius,
+    private val windowCornerRadiusProvider: (Context) -> Float = ::computeWindowCornerRadius,
+) : SafeCloseable {
+    private var taskCornerRadius = 0f
+    private var windowCornerRadius = 0f
+    var currentCornerRadius = 0f
+
+    init {
+        updateCornerRadius(context)
+    }
+
+    /** Recomputes the start and end corner radius for the given Context. */
+    fun updateCornerRadius(context: Context) {
+        taskCornerRadius = taskCornerRadiusProvider(context)
+        windowCornerRadius = windowCornerRadiusProvider(context)
+    }
+
+    /** Sets the progress in range [0, 1] */
+    fun setProgress(fullscreenProgress: Float, parentScale: Float, taskViewScale: Float) {
+        currentCornerRadius =
+            Utilities.mapRange(fullscreenProgress, taskCornerRadius, windowCornerRadius) /
+                parentScale /
+                taskViewScale
+    }
+
+    override fun close() {}
+
+    companion object {
+        private fun computeTaskCornerRadius(context: Context): Float = TaskCornerRadius.get(context)
+
+        private fun computeWindowCornerRadius(context: Context): Float {
+            val activityContext: ActivityContext? = ActivityContext.lookupContextNoThrow(context)
+            return if (
+                activityContext?.deviceProfile?.isTaskbarPresent == true &&
+                    DisplayController.isTransientTaskbar(context)
+            ) {
+                context.resources
+                    .getDimensionPixelSize(R.dimen.persistent_taskbar_corner_radius)
+                    .toFloat()
+            } else {
+                // The corner radius is fixed to match when Taskbar is persistent mode
+                QuickStepContract.getWindowCornerRadius(context)
+            }
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/GestureState.java b/quickstep/src/com/android/quickstep/GestureState.java
index 9cc463a..cfbcf0a 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,7 +273,7 @@
     /**
      * @return the interface to the activity handing the UI updates for this gesture.
      */
-    public <S extends BaseState<S>, T extends RecentsViewContainer>
+    public <S extends BaseState<S>, T extends RecentsViewContainer & StatefulContainer<S>>
             BaseContainerInterface<S, T> 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/InputConsumerUtils.kt b/quickstep/src/com/android/quickstep/InputConsumerUtils.kt
new file mode 100644
index 0000000..bea3150
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/InputConsumerUtils.kt
@@ -0,0 +1,746 @@
+/*
+ * 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.Context
+import android.view.MotionEvent
+import androidx.annotation.VisibleForTesting
+import com.android.launcher3.anim.AnimatedFloat
+import com.android.launcher3.statemanager.BaseState
+import com.android.launcher3.statemanager.StatefulContainer
+import com.android.launcher3.taskbar.TaskbarManager
+import com.android.launcher3.util.LockedUserState.Companion.get
+import com.android.quickstep.inputconsumers.AccessibilityInputConsumer
+import com.android.quickstep.inputconsumers.AssistantInputConsumer
+import com.android.quickstep.inputconsumers.BubbleBarInputConsumer
+import com.android.quickstep.inputconsumers.DeviceLockedInputConsumer
+import com.android.quickstep.inputconsumers.NavHandleLongPressInputConsumer
+import com.android.quickstep.inputconsumers.OneHandedModeInputConsumer
+import com.android.quickstep.inputconsumers.OtherActivityInputConsumer
+import com.android.quickstep.inputconsumers.OverviewInputConsumer
+import com.android.quickstep.inputconsumers.OverviewWithoutFocusInputConsumer
+import com.android.quickstep.inputconsumers.ProgressDelegateInputConsumer
+import com.android.quickstep.inputconsumers.ResetGestureInputConsumer
+import com.android.quickstep.inputconsumers.ScreenPinnedInputConsumer
+import com.android.quickstep.inputconsumers.SysUiOverlayInputConsumer
+import com.android.quickstep.inputconsumers.TaskbarUnstashInputConsumer
+import com.android.quickstep.inputconsumers.TrackpadStatusBarInputConsumer
+import com.android.quickstep.util.ActiveGestureErrorDetector
+import com.android.quickstep.util.ActiveGestureLog
+import com.android.quickstep.util.ActiveGestureLog.CompoundString
+import com.android.quickstep.util.ActiveGestureProtoLogProxy
+import com.android.quickstep.views.RecentsViewContainer
+import com.android.systemui.shared.system.InputChannelCompat
+import com.android.systemui.shared.system.InputMonitorCompat
+import com.android.wm.shell.Flags
+import java.util.function.Consumer
+import java.util.function.Function
+
+/** Utility class for creating input consumers. */
+object InputConsumerUtils {
+    private const val SUBSTRING_PREFIX = "; "
+    private const val NEWLINE_PREFIX = "\n\t\t\t-> "
+
+    @JvmStatic
+    fun <S : BaseState<S>, T> newConsumer(
+        baseContext: Context,
+        tisContext: Context,
+        resetGestureInputConsumer: ResetGestureInputConsumer?,
+        overviewComponentObserver: OverviewComponentObserver,
+        deviceState: RecentsAnimationDeviceState,
+        previousGestureState: GestureState,
+        gestureState: GestureState,
+        taskAnimationManager: TaskAnimationManager,
+        inputMonitorCompat: InputMonitorCompat,
+        swipeUpHandlerFactory: AbsSwipeUpHandler.Factory,
+        onCompleteCallback: Consumer<OtherActivityInputConsumer>,
+        inputEventReceiver: InputChannelCompat.InputEventReceiver,
+        taskbarManager: TaskbarManager,
+        swipeUpProxyProvider: Function<GestureState?, AnimatedFloat?>,
+        overviewCommandHelper: OverviewCommandHelper,
+        event: MotionEvent,
+    ): InputConsumer where T : RecentsViewContainer, T : StatefulContainer<S> {
+        val tac = taskbarManager.currentActivityContext
+        val bubbleControllers = tac?.bubbleControllers
+        if (bubbleControllers != null && BubbleBarInputConsumer.isEventOnBubbles(tac, event)) {
+            val consumer: InputConsumer =
+                BubbleBarInputConsumer(tisContext, bubbleControllers, inputMonitorCompat)
+            logInputConsumerSelectionReason(
+                consumer,
+                newCompoundString("event is on bubbles, creating new input consumer"),
+            )
+            return consumer
+        }
+        val progressProxy = swipeUpProxyProvider.apply(gestureState)
+        if (progressProxy != null) {
+            val consumer: InputConsumer =
+                ProgressDelegateInputConsumer(
+                    tisContext,
+                    taskAnimationManager,
+                    gestureState,
+                    inputMonitorCompat,
+                    progressProxy,
+                )
+
+            logInputConsumerSelectionReason(
+                consumer,
+                newCompoundString(
+                    "mSwipeUpProxyProvider has been set, using ProgressDelegateInputConsumer"
+                ),
+            )
+
+            return consumer
+        }
+
+        val canStartSystemGesture =
+            if (gestureState.isTrackpadGesture) deviceState.canStartTrackpadGesture()
+            else deviceState.canStartSystemGesture()
+
+        if (!get(tisContext).isUserUnlocked) {
+            val reasonString = newCompoundString("device locked")
+            val consumer =
+                if (canStartSystemGesture) {
+                    // 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).
+                    createDeviceLockedInputConsumer(
+                        tisContext,
+                        resetGestureInputConsumer,
+                        deviceState,
+                        gestureState,
+                        taskAnimationManager,
+                        inputMonitorCompat,
+                        reasonString.append("%scan start system gesture", SUBSTRING_PREFIX),
+                    )
+                } else {
+                    getDefaultInputConsumer(
+                        resetGestureInputConsumer,
+                        reasonString.append("%scannot start system gesture", SUBSTRING_PREFIX),
+                    )
+                }
+            logInputConsumerSelectionReason(consumer, reasonString)
+            return consumer
+        }
+
+        var reasonString: CompoundString
+        var base: InputConsumer
+        // When there is an existing recents animation running, bypass systemState check as this is
+        // a followup gesture and the first gesture started in a valid system state.
+        if (canStartSystemGesture || previousGestureState.isRecentsAnimationRunning) {
+            reasonString =
+                newCompoundString(
+                    if (canStartSystemGesture)
+                        "can start system gesture, trying to use base consumer"
+                    else "recents animation was running, trying to use base consumer"
+                )
+            base =
+                newBaseConsumer<S, T>(
+                    tisContext,
+                    resetGestureInputConsumer,
+                    overviewComponentObserver,
+                    deviceState,
+                    previousGestureState,
+                    gestureState,
+                    taskAnimationManager,
+                    inputMonitorCompat,
+                    swipeUpHandlerFactory,
+                    onCompleteCallback,
+                    inputEventReceiver,
+                    event,
+                    reasonString,
+                )
+        } else {
+            reasonString =
+                newCompoundString(
+                    "cannot start system gesture and recents " +
+                        "animation was not running, trying to use default input consumer"
+                )
+            base = getDefaultInputConsumer(resetGestureInputConsumer, reasonString)
+        }
+        if (deviceState.isGesturalNavMode || gestureState.isTrackpadGesture) {
+            handleOrientationSetup(base)
+        }
+        if (deviceState.isFullyGesturalNavMode || gestureState.isTrackpadGesture) {
+            val reasonPrefix =
+                "device is in gesture navigation mode or 3-button mode with a trackpad gesture"
+            if (deviceState.canTriggerAssistantAction(event)) {
+                reasonString.append(
+                    "%s%s%sgesture can trigger the assistant, " +
+                        "trying to use assistant input consumer",
+                    NEWLINE_PREFIX,
+                    reasonPrefix,
+                    SUBSTRING_PREFIX,
+                )
+                base =
+                    tryCreateAssistantInputConsumer(
+                        tisContext,
+                        deviceState,
+                        inputMonitorCompat,
+                        base,
+                        gestureState,
+                        event,
+                        reasonString,
+                    )
+            }
+
+            // If Taskbar is present, we listen for swipe or cursor hover events to unstash it.
+            if (tac != null && base !is AssistantInputConsumer) {
+                // Present always on large screen or on small screen w/ flag
+                val useTaskbarConsumer =
+                    (tac.deviceProfile.isTaskbarPresent &&
+                        !tac.isPhoneMode &&
+                        !tac.isInStashedLauncherState)
+                if (canStartSystemGesture && useTaskbarConsumer) {
+                    reasonString.append(
+                        "%s%s%sTaskbarActivityContext != null, " +
+                            "using TaskbarUnstashInputConsumer",
+                        NEWLINE_PREFIX,
+                        reasonPrefix,
+                        SUBSTRING_PREFIX,
+                    )
+                    base =
+                        TaskbarUnstashInputConsumer(
+                            tisContext,
+                            base,
+                            inputMonitorCompat,
+                            tac,
+                            overviewCommandHelper,
+                            gestureState,
+                        )
+                }
+            }
+            if (Flags.enableBubblesLongPressNavHandle()) {
+                // Create bubbles input consumer before NavHandleLongPressInputConsumer.
+                // This allows for nav handle to fall back to bubbles.
+                if (deviceState.isBubblesExpanded) {
+                    reasonString =
+                        newCompoundString(reasonPrefix)
+                            .append(
+                                "%sbubbles expanded, trying to use default input consumer",
+                                SUBSTRING_PREFIX,
+                            )
+                    // Bubbles can handle home gesture itself.
+                    base = getDefaultInputConsumer(resetGestureInputConsumer, reasonString)
+                }
+            }
+
+            val navHandle = tac?.navHandle ?: SystemUiProxy.INSTANCE[tisContext]
+            if (
+                canStartSystemGesture &&
+                    !previousGestureState.isRecentsAnimationRunning &&
+                    navHandle.canNavHandleBeLongPressed() &&
+                    !ignoreThreeFingerTrackpadForNavHandleLongPress(gestureState)
+            ) {
+                reasonString.append(
+                    "%s%s%sNot running recents animation, ",
+                    NEWLINE_PREFIX,
+                    reasonPrefix,
+                    SUBSTRING_PREFIX,
+                )
+                if (tac != null && tac.navHandle.canNavHandleBeLongPressed()) {
+                    reasonString.append("stashed handle is long-pressable, ")
+                }
+                reasonString.append("using NavHandleLongPressInputConsumer")
+                base =
+                    NavHandleLongPressInputConsumer(
+                        tisContext,
+                        base,
+                        inputMonitorCompat,
+                        deviceState,
+                        navHandle,
+                        gestureState,
+                    )
+            }
+
+            if (!Flags.enableBubblesLongPressNavHandle()) {
+                // Continue overriding nav handle input consumer with bubbles
+                if (deviceState.isBubblesExpanded) {
+                    reasonString =
+                        newCompoundString(reasonPrefix)
+                            .append(
+                                "%sbubbles expanded, trying to use default input consumer",
+                                SUBSTRING_PREFIX,
+                            )
+                    // Bubbles can handle home gesture itself.
+                    base = getDefaultInputConsumer(resetGestureInputConsumer, reasonString)
+                }
+            }
+
+            if (deviceState.isSystemUiDialogShowing) {
+                reasonString =
+                    newCompoundString(reasonPrefix)
+                        .append(
+                            "%ssystem dialog is showing, using SysUiOverlayInputConsumer",
+                            SUBSTRING_PREFIX,
+                        )
+                base = SysUiOverlayInputConsumer(baseContext, deviceState, inputMonitorCompat)
+            }
+
+            if (
+                gestureState.isTrackpadGesture &&
+                    canStartSystemGesture &&
+                    !previousGestureState.isRecentsAnimationRunning
+            ) {
+                reasonString =
+                    newCompoundString(reasonPrefix)
+                        .append(
+                            "%sTrackpad 3-finger gesture, using TrackpadStatusBarInputConsumer",
+                            SUBSTRING_PREFIX,
+                        )
+                base = TrackpadStatusBarInputConsumer(baseContext, base, inputMonitorCompat)
+            }
+
+            if (deviceState.isScreenPinningActive) {
+                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 = ScreenPinnedInputConsumer(tisContext, gestureState)
+            }
+
+            if (deviceState.canTriggerOneHandedAction(event)) {
+                reasonString.append(
+                    "%s%s%sgesture can trigger one handed mode, " +
+                        "using OneHandedModeInputConsumer",
+                    NEWLINE_PREFIX,
+                    reasonPrefix,
+                    SUBSTRING_PREFIX,
+                )
+                base = OneHandedModeInputConsumer(tisContext, deviceState, base, inputMonitorCompat)
+            }
+
+            if (deviceState.isAccessibilityMenuAvailable) {
+                reasonString.append(
+                    "%s%s%saccessibility menu is available, using AccessibilityInputConsumer",
+                    NEWLINE_PREFIX,
+                    reasonPrefix,
+                    SUBSTRING_PREFIX,
+                )
+                base =
+                    AccessibilityInputConsumer(
+                        tisContext,
+                        deviceState,
+                        gestureState,
+                        base,
+                        inputMonitorCompat,
+                    )
+            }
+        } else {
+            val reasonPrefix = "device is not in gesture navigation mode"
+            if (deviceState.isScreenPinningActive) {
+                reasonString =
+                    newCompoundString(reasonPrefix)
+                        .append(
+                            "%sscreen pinning is active, trying to use default input consumer",
+                            SUBSTRING_PREFIX,
+                        )
+                base = getDefaultInputConsumer(resetGestureInputConsumer, reasonString)
+            }
+
+            if (deviceState.canTriggerOneHandedAction(event)) {
+                reasonString.append(
+                    "%s%s%sgesture can trigger one handed mode, " +
+                        "using OneHandedModeInputConsumer",
+                    NEWLINE_PREFIX,
+                    reasonPrefix,
+                    SUBSTRING_PREFIX,
+                )
+                base = OneHandedModeInputConsumer(tisContext, deviceState, base, inputMonitorCompat)
+            }
+        }
+        logInputConsumerSelectionReason(base, reasonString)
+        return base
+    }
+
+    @JvmStatic
+    fun tryCreateAssistantInputConsumer(
+        context: Context,
+        deviceState: RecentsAnimationDeviceState,
+        inputMonitorCompat: InputMonitorCompat,
+        gestureState: GestureState,
+        motionEvent: MotionEvent,
+    ): InputConsumer {
+        return tryCreateAssistantInputConsumer(
+            context,
+            deviceState,
+            inputMonitorCompat,
+            InputConsumer.NO_OP,
+            gestureState,
+            motionEvent,
+            CompoundString.NO_OP,
+        )
+    }
+
+    private fun tryCreateAssistantInputConsumer(
+        context: Context,
+        deviceState: RecentsAnimationDeviceState,
+        inputMonitorCompat: InputMonitorCompat,
+        base: InputConsumer,
+        gestureState: GestureState,
+        motionEvent: MotionEvent,
+        reasonString: CompoundString,
+    ): InputConsumer {
+        return if (deviceState.isGestureBlockedTask(gestureState.runningTask)) {
+            reasonString.append(
+                "%sis gesture-blocked task, using base input consumer",
+                SUBSTRING_PREFIX,
+            )
+            base
+        } else {
+            reasonString.append("%susing AssistantInputConsumer", SUBSTRING_PREFIX)
+            AssistantInputConsumer(
+                context,
+                gestureState,
+                base,
+                inputMonitorCompat,
+                deviceState,
+                motionEvent,
+            )
+        }
+    }
+
+    @VisibleForTesting
+    @JvmStatic
+    fun <S : BaseState<S>, T> newBaseConsumer(
+        context: Context,
+        resetGestureInputConsumer: ResetGestureInputConsumer?,
+        overviewComponentObserver: OverviewComponentObserver,
+        deviceState: RecentsAnimationDeviceState,
+        previousGestureState: GestureState,
+        gestureState: GestureState,
+        taskAnimationManager: TaskAnimationManager,
+        inputMonitorCompat: InputMonitorCompat,
+        swipeUpHandlerFactory: AbsSwipeUpHandler.Factory,
+        onCompleteCallback: Consumer<OtherActivityInputConsumer>,
+        inputEventReceiver: InputChannelCompat.InputEventReceiver,
+        event: MotionEvent,
+        reasonString: CompoundString,
+    ): InputConsumer where T : RecentsViewContainer, T : StatefulContainer<S> {
+        if (deviceState.isKeyguardShowingOccluded) {
+            // This handles apps showing over the lockscreen (e.g. camera)
+            return createDeviceLockedInputConsumer(
+                context,
+                resetGestureInputConsumer,
+                deviceState,
+                gestureState,
+                taskAnimationManager,
+                inputMonitorCompat,
+                reasonString.append(
+                    "%skeyguard is showing occluded, " +
+                        "trying to use device locked input consumer",
+                    SUBSTRING_PREFIX,
+                ),
+            )
+        }
+
+        reasonString.append("%skeyguard is not showing occluded", SUBSTRING_PREFIX)
+
+        val runningTask = gestureState.runningTask
+        // Use overview input consumer for sharesheets on top of home.
+        val forceOverviewInputConsumer =
+            gestureState.getContainerInterface<S, T>().isStarted() &&
+                runningTask != null &&
+                runningTask.isRootChooseActivity
+
+        if (!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.
+            val otherVisibleTask = runningTask?.visibleNonExcludedTask
+            if (otherVisibleTask != null) {
+                ActiveGestureProtoLogProxy.logUpdateGestureStateRunningTask(
+                    otherVisibleTask.packageName ?: "MISSING",
+                    runningTask.packageName ?: "MISSING",
+                )
+                gestureState.updateRunningTask(otherVisibleTask)
+            }
+        }
+
+        val previousGestureAnimatedToLauncher =
+            (previousGestureState.isRunningAnimationToLauncher ||
+                deviceState.isPredictiveBackToHomeInProgress)
+        // with shell-transitions, home is resumed during recents animation, so
+        // explicitly check against recents animation too.
+        val launcherResumedThroughShellTransition =
+            (gestureState.getContainerInterface<S, T>().isResumed() &&
+                !previousGestureState.isRecentsAnimationRunning)
+        // If a task fragment within Launcher is resumed
+        val launcherChildActivityResumed =
+            (com.android.launcher3.Flags.useActivityOverlay() &&
+                runningTask != null &&
+                runningTask.isHomeTask &&
+                overviewComponentObserver.isHomeAndOverviewSame &&
+                !launcherResumedThroughShellTransition &&
+                !previousGestureState.isRecentsAnimationRunning)
+
+        return if (gestureState.getContainerInterface<S, T>().isInLiveTileMode()) {
+            createOverviewInputConsumer<S, T>(
+                resetGestureInputConsumer,
+                deviceState,
+                inputMonitorCompat,
+                previousGestureState,
+                gestureState,
+                event,
+                reasonString.append(
+                    "%sis in live tile mode, trying to use overview input consumer",
+                    SUBSTRING_PREFIX,
+                ),
+            )
+        } else if (runningTask == null) {
+            getDefaultInputConsumer(
+                resetGestureInputConsumer,
+                reasonString.append("%srunning task == null", SUBSTRING_PREFIX),
+            )
+        } else if (
+            previousGestureAnimatedToLauncher ||
+                launcherResumedThroughShellTransition ||
+                forceOverviewInputConsumer
+        ) {
+            createOverviewInputConsumer<S, T>(
+                resetGestureInputConsumer,
+                deviceState,
+                inputMonitorCompat,
+                previousGestureState,
+                gestureState,
+                event,
+                reasonString.append(
+                    if (previousGestureAnimatedToLauncher)
+                        ("%sprevious gesture animated to launcher, " +
+                            "trying to use overview input consumer")
+                    else
+                        (if (launcherResumedThroughShellTransition)
+                            ("%slauncher resumed through a shell transition, " +
+                                "trying to use overview input consumer")
+                        else
+                            ("%sforceOverviewInputConsumer == true, " +
+                                "trying to use overview input consumer")),
+                    SUBSTRING_PREFIX,
+                ),
+            )
+        } else if (deviceState.isGestureBlockedTask(runningTask) || launcherChildActivityResumed) {
+            getDefaultInputConsumer(
+                resetGestureInputConsumer,
+                reasonString.append(
+                    if (launcherChildActivityResumed)
+                        "%sis launcher child-task, trying to use default input consumer"
+                    else "%sis gesture-blocked task, trying to use default input consumer",
+                    SUBSTRING_PREFIX,
+                ),
+            )
+        } else {
+            reasonString.append("%susing OtherActivityInputConsumer", SUBSTRING_PREFIX)
+            createOtherActivityInputConsumer<S, T>(
+                context,
+                swipeUpHandlerFactory,
+                overviewComponentObserver,
+                deviceState,
+                taskAnimationManager,
+                inputMonitorCompat,
+                onCompleteCallback,
+                inputEventReceiver,
+                gestureState,
+                event,
+            )
+        }
+    }
+
+    private fun createDeviceLockedInputConsumer(
+        context: Context,
+        resetGestureInputConsumer: ResetGestureInputConsumer?,
+        deviceState: RecentsAnimationDeviceState,
+        gestureState: GestureState,
+        taskAnimationManager: TaskAnimationManager,
+        inputMonitorCompat: InputMonitorCompat,
+        reasonString: CompoundString,
+    ): InputConsumer {
+        return if (
+            (deviceState.isFullyGesturalNavMode || gestureState.isTrackpadGesture) &&
+                gestureState.runningTask != null
+        ) {
+            reasonString.append(
+                "%sdevice is in gesture nav mode or 3-button mode with a trackpad " +
+                    "gesture and running task != null, using DeviceLockedInputConsumer",
+                SUBSTRING_PREFIX,
+            )
+            DeviceLockedInputConsumer(
+                context,
+                deviceState,
+                taskAnimationManager,
+                gestureState,
+                inputMonitorCompat,
+            )
+        } else {
+            getDefaultInputConsumer(
+                resetGestureInputConsumer,
+                reasonString.append(
+                    if (deviceState.isFullyGesturalNavMode || gestureState.isTrackpadGesture)
+                        "%srunning task == null, trying to use default input consumer"
+                    else
+                        ("%sdevice is not in gesture nav mode and it's not a trackpad gesture," +
+                            " trying to use default input consumer"),
+                    SUBSTRING_PREFIX,
+                ),
+            )
+        }
+    }
+
+    private fun <S : BaseState<S>, T> createOverviewInputConsumer(
+        resetGestureInputConsumer: ResetGestureInputConsumer?,
+        deviceState: RecentsAnimationDeviceState,
+        inputMonitorCompat: InputMonitorCompat,
+        previousGestureState: GestureState,
+        gestureState: GestureState,
+        event: MotionEvent,
+        reasonString: CompoundString,
+    ): InputConsumer where T : RecentsViewContainer, T : StatefulContainer<S> {
+        val container: T =
+            gestureState.getContainerInterface<S, T>().getCreatedContainer()
+                ?: return getDefaultInputConsumer(
+                    resetGestureInputConsumer,
+                    reasonString.append(
+                        "%sactivity == null, trying to use default input consumer",
+                        SUBSTRING_PREFIX,
+                    ),
+                )
+
+        val rootView = container.rootView
+        val hasWindowFocus = rootView?.hasWindowFocus() ?: false
+        val isPreviousGestureAnimatingToLauncher =
+            (previousGestureState.isRunningAnimationToLauncher ||
+                deviceState.isPredictiveBackToHomeInProgress)
+        val isInLiveTileMode: Boolean =
+            gestureState.getContainerInterface<S, T>().isInLiveTileMode()
+
+        reasonString.append(
+            if (hasWindowFocus) "%sactivity has window focus"
+            else
+                (if (isPreviousGestureAnimatingToLauncher)
+                    "%sprevious gesture is still animating to launcher"
+                else if (isInLiveTileMode) "%sdevice is in live mode"
+                else "%sall overview focus conditions failed"),
+            SUBSTRING_PREFIX,
+        )
+        return if (hasWindowFocus || isPreviousGestureAnimatingToLauncher || isInLiveTileMode) {
+            reasonString.append(
+                "%soverview should have focus, using OverviewInputConsumer",
+                SUBSTRING_PREFIX,
+            )
+            OverviewInputConsumer(
+                gestureState,
+                container,
+                inputMonitorCompat,
+                /* startingInActivityBounds= */ false,
+            )
+        } else {
+            reasonString.append(
+                "%soverview shouldn't have focus, using OverviewWithoutFocusInputConsumer",
+                SUBSTRING_PREFIX,
+            )
+            val disableHorizontalSwipe = deviceState.isInExclusionRegion(event)
+            OverviewWithoutFocusInputConsumer(
+                container.asContext(),
+                deviceState,
+                gestureState,
+                inputMonitorCompat,
+                disableHorizontalSwipe,
+            )
+        }
+    }
+
+    /** Returns the [ResetGestureInputConsumer] if user is unlocked, else NO_OP. */
+    private fun getDefaultInputConsumer(
+        resetGestureInputConsumer: ResetGestureInputConsumer?,
+        reasonString: CompoundString,
+    ): InputConsumer {
+        return if (resetGestureInputConsumer != null) {
+            reasonString.append(
+                "%smResetGestureInputConsumer initialized, using ResetGestureInputConsumer",
+                SUBSTRING_PREFIX,
+            )
+            resetGestureInputConsumer
+        } else {
+            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).
+            InputConsumer.NO_OP
+        }
+    }
+
+    private fun <S : BaseState<S>, T> createOtherActivityInputConsumer(
+        context: Context,
+        swipeUpHandlerFactory: AbsSwipeUpHandler.Factory,
+        overviewComponentObserver: OverviewComponentObserver,
+        deviceState: RecentsAnimationDeviceState,
+        taskAnimationManager: TaskAnimationManager,
+        inputMonitorCompat: InputMonitorCompat,
+        onCompleteCallback: Consumer<OtherActivityInputConsumer>,
+        inputEventReceiver: InputChannelCompat.InputEventReceiver,
+        gestureState: GestureState,
+        event: MotionEvent,
+    ): InputConsumer where T : RecentsViewContainer, T : StatefulContainer<S> {
+        val shouldDefer =
+            (!overviewComponentObserver.isHomeAndOverviewSame ||
+                gestureState
+                    .getContainerInterface<S, T>()
+                    .deferStartingActivity(deviceState, event))
+        val disableHorizontalSwipe = deviceState.isInExclusionRegion(event)
+        return OtherActivityInputConsumer(
+            /* base= */ context,
+            deviceState,
+            taskAnimationManager,
+            gestureState,
+            /* isDeferredDownTarget= */ shouldDefer,
+            onCompleteCallback,
+            inputMonitorCompat,
+            inputEventReceiver,
+            disableHorizontalSwipe,
+            swipeUpHandlerFactory,
+        )
+    }
+
+    private fun newCompoundString(substring: String): CompoundString {
+        return CompoundString("%s%s", NEWLINE_PREFIX, substring)
+    }
+
+    private fun logInputConsumerSelectionReason(
+        consumer: InputConsumer,
+        reasonString: CompoundString,
+    ) {
+        ActiveGestureProtoLogProxy.logSetInputConsumer(consumer.name, reasonString.toString())
+        if ((consumer.type and InputConsumer.TYPE_OTHER_ACTIVITY) != 0) {
+            ActiveGestureLog.INSTANCE.trackEvent(
+                ActiveGestureErrorDetector.GestureEvent.FLAG_USING_OTHER_ACTIVITY_INPUT_CONSUMER
+            )
+        }
+    }
+
+    private fun ignoreThreeFingerTrackpadForNavHandleLongPress(
+        gestureState: GestureState
+    ): Boolean {
+        return (com.android.launcher3.Flags.ignoreThreeFingerTrackpadForNavHandleLongPress() &&
+            gestureState.isThreeFingerTrackpadGesture)
+    }
+
+    private fun handleOrientationSetup(baseInputConsumer: InputConsumer) {
+        baseInputConsumer.notifyOrientationSetup()
+    }
+}
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/LauncherBackAnimationController.java b/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
index 1124aac..4bd9ffb 100644
--- a/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
+++ b/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
@@ -24,6 +24,7 @@
 import static com.android.launcher3.BaseActivity.INVISIBLE_ALL;
 import static com.android.launcher3.BaseActivity.INVISIBLE_BY_PENDING_FLAGS;
 import static com.android.launcher3.BaseActivity.PENDING_INVISIBLE_BY_WALLPAPER_ANIMATION;
+import static com.android.window.flags.Flags.predictiveBackThreeButtonNav;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -48,8 +49,8 @@
 import android.window.BackEvent;
 import android.window.BackMotionEvent;
 import android.window.BackProgressAnimator;
+import android.window.IBackAnimationHandoffHandler;
 import android.window.IOnBackInvokedCallback;
-
 import com.android.app.animation.Interpolators;
 import com.android.internal.policy.SystemBarUtils;
 import com.android.internal.view.AppearanceRegion;
@@ -60,6 +61,8 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.taskbar.LauncherTaskbarUIController;
 import com.android.launcher3.uioverrides.QuickstepLauncher;
+import com.android.launcher3.util.DisplayController;
+import com.android.launcher3.util.NavigationMode;
 import com.android.launcher3.widget.LauncherAppWidgetHostView;
 import com.android.quickstep.util.BackAnimState;
 import com.android.systemui.shared.system.QuickStepContract;
@@ -222,6 +225,12 @@
         public void setTriggerBack(boolean triggerBack) {
             // TODO(b/261654570): track touch from the Launcher process.
         }
+
+        @Override
+        public void setHandoffHandler(IBackAnimationHandoffHandler unused) {
+            // For now, Launcher handles this internally so it doesn't need to hand off the
+            // animation.
+        }
     }
 
     private static class RemoteAnimationRunnerStub extends IRemoteAnimationRunner.Stub {
@@ -295,8 +304,11 @@
 
         mStartRect.set(appTarget.windowConfiguration.getMaxBounds());
 
-        // inset bottom in case of pinned taskbar being present
-        mStartRect.inset(0, 0, 0, appTarget.contentInsets.bottom);
+        // inset bottom in case of taskbar being present
+        if (!predictiveBackThreeButtonNav() || mLauncher.getDeviceProfile().isTaskbarPresent
+                || DisplayController.getNavigationMode(mLauncher) == NavigationMode.NO_BUTTON) {
+            mStartRect.inset(0, 0, 0, appTarget.contentInsets.bottom);
+        }
 
         mLauncherTargetView = mQuickstepTransitionManager.findLauncherView(
                 new RemoteAnimationTarget[]{ mBackTarget });
@@ -378,10 +390,11 @@
         // Move the window along the Y axis.
         float top = (screenHeight - height) * 0.5f + deltaY;
         // Move the window along the X axis.
-        float left = event.getSwipeEdge() == BackEvent.EDGE_RIGHT
-                ? progress * mWindowScaleMarginX
-                : screenWidth - progress * mWindowScaleMarginX - width;
-
+        float left = switch (event.getSwipeEdge()) {
+            case BackEvent.EDGE_RIGHT -> progress * mWindowScaleMarginX;
+            case BackEvent.EDGE_LEFT -> screenWidth - progress * mWindowScaleMarginX - width;
+            default -> (screenWidth - width) / 2;
+        };
         mCurrentRect.set(left, top, left + width, top + height);
         float cornerRadius = Utilities.mapRange(
                 progress, mWindowScaleStartCornerRadius, mWindowScaleEndCornerRadius);
diff --git a/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java b/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java
index d2dcd7b..6087dc2 100644
--- a/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java
+++ b/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java
@@ -53,6 +53,7 @@
 import com.android.quickstep.views.FloatingWidgetView;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskView;
+import com.android.systemui.animation.TransitionAnimator;
 import com.android.systemui.shared.system.InputConsumerController;
 
 import java.util.Collections;
@@ -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);
     }
 
 
@@ -107,7 +108,9 @@
 
         mContainer.getRootView().setForceHideBackArrow(true);
 
-        if (!canUseWorkspaceView || appCanEnterPip || mIsSwipeForSplit) {
+        boolean handOffAnimation = TransitionAnimator.Companion.longLivedReturnAnimationsEnabled()
+                && mHandOffAnimationToHome;
+        if (handOffAnimation || !canUseWorkspaceView || appCanEnterPip || mIsSwipeForSplit) {
             return new LauncherHomeAnimationFactory() {
 
                 @Nullable
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/OrientationTouchTransformer.java b/quickstep/src/com/android/quickstep/OrientationTouchTransformer.java
index a03c0f8..ef103c4 100644
--- a/quickstep/src/com/android/quickstep/OrientationTouchTransformer.java
+++ b/quickstep/src/com/android/quickstep/OrientationTouchTransformer.java
@@ -37,6 +37,7 @@
 import com.android.launcher3.util.DisplayController.Info;
 import com.android.launcher3.util.NavigationMode;
 import com.android.launcher3.util.window.CachedDisplayInfo;
+import com.android.quickstep.util.ActiveGestureProtoLogProxy;
 import com.android.systemui.shared.Flags;
 
 import java.io.PrintWriter;
@@ -111,13 +112,13 @@
                 mNavBarGesturalHeight);
     }
 
-    private void refreshTouchRegion(Info info, Resources newRes) {
+    private void refreshTouchRegion(Info info, Resources newRes, String reason) {
         // Swipe touch regions are independent of nav mode, so we have to clear them explicitly
         // here to avoid, for ex, a nav region for 2-button rotation 0 being used for 3-button mode
         // It tries to cache and reuse swipe regions whenever possible based only on rotation
         mResources = newRes;
         mSwipeTouchRegions.clear();
-        resetSwipeRegions(info);
+        resetSwipeRegions(info, reason);
     }
 
     void setNavigationMode(NavigationMode newMode, Info info, Resources newRes) {
@@ -128,7 +129,7 @@
             return;
         }
         this.mMode = newMode;
-        refreshTouchRegion(info, newRes);
+        refreshTouchRegion(info, newRes, "setNavigationMode");
     }
 
     void setGesturalHeight(int newGesturalHeight, Info info, Resources newRes) {
@@ -136,7 +137,7 @@
             return;
         }
         mNavBarGesturalHeight = newGesturalHeight;
-        refreshTouchRegion(info, newRes);
+        refreshTouchRegion(info, newRes, "setGesturalHeight");
     }
 
     /**
@@ -147,14 +148,14 @@
      *
      * @see #enableMultipleRegions(boolean, Info)
      */
-    void createOrAddTouchRegion(Info info) {
+    void createOrAddTouchRegion(Info info, String reason) {
         mCachedDisplayInfo = new CachedDisplayInfo(info.currentSize, info.rotation);
 
         if (mQuickStepStartingRotation > QUICKSTEP_ROTATION_UNINITIALIZED
                 && mCachedDisplayInfo.rotation == mQuickStepStartingRotation) {
             // User already was swiping and the current screen is same rotation as the starting one
             // Remove active nav bars in other rotations except for the one we started out in
-            resetSwipeRegions(info);
+            resetSwipeRegions(info, reason);
             return;
         }
         OrientationRectF region = mSwipeTouchRegions.get(mCachedDisplayInfo);
@@ -163,9 +164,9 @@
         }
 
         if (mEnableMultipleRegions) {
-            mSwipeTouchRegions.put(mCachedDisplayInfo, createRegionForDisplay(info));
+            mSwipeTouchRegions.put(mCachedDisplayInfo, createRegionForDisplay(info, reason));
         } else {
-            resetSwipeRegions(info);
+            resetSwipeRegions(info, reason);
         }
     }
 
@@ -184,7 +185,7 @@
             mActiveTouchRotation = 0;
             mQuickStepStartingRotation = QUICKSTEP_ROTATION_UNINITIALIZED;
         }
-        resetSwipeRegions(info);
+        resetSwipeRegions(info, "enableMultipleRegions");
     }
 
     /**
@@ -198,7 +199,7 @@
      */
     void setSingleActiveRegion(Info displayInfo) {
         mActiveTouchRotation = displayInfo.rotation;
-        resetSwipeRegions(displayInfo);
+        resetSwipeRegions(displayInfo, "setSingleActiveRegion");
     }
 
     /**
@@ -207,19 +208,21 @@
      * To be called whenever we want to stop tracking more than one swipe region.
      * Ok to call multiple times.
      */
-    private void resetSwipeRegions(Info region) {
+    private void resetSwipeRegions(Info region, String reason) {
         if (enableLog()) {
-            Log.d(TAG, "clearing all regions except rotation: " + mCachedDisplayInfo.rotation);
+            Log.d(TAG, "clearing all regions except rotation: " + mCachedDisplayInfo.rotation
+                    + " reason=" + reason);
         }
 
         mCachedDisplayInfo = new CachedDisplayInfo(region.currentSize, region.rotation);
         OrientationRectF regionToKeep = mSwipeTouchRegions.get(mCachedDisplayInfo);
         if (regionToKeep == null) {
-            regionToKeep = createRegionForDisplay(region);
+            regionToKeep = createRegionForDisplay(region, reason);
         }
         mSwipeTouchRegions.clear();
         mSwipeTouchRegions.put(mCachedDisplayInfo, regionToKeep);
         updateAssistantRegions(regionToKeep);
+        updateOneHandedRegions(regionToKeep);
     }
 
     private void resetSwipeRegions() {
@@ -228,15 +231,17 @@
         if (regionToKeep != null) {
             mSwipeTouchRegions.put(mCachedDisplayInfo, regionToKeep);
             updateAssistantRegions(regionToKeep);
+            updateOneHandedRegions(regionToKeep);
         }
     }
 
-    private OrientationRectF createRegionForDisplay(Info display) {
+    private OrientationRectF createRegionForDisplay(Info display, String reason) {
         if (enableLog()) {
             Log.d(TAG, "creating rotation region for: " + mCachedDisplayInfo.rotation
             + " with mode: " + mMode + " displayRotation: " + display.rotation +
                     " displaySize: " + display.currentSize +
-                    " navBarHeight: " + mNavBarGesturalHeight);
+                    " navBarHeight: " + mNavBarGesturalHeight +
+                    " reason: " + reason);
         }
 
         Point size = display.currentSize;
@@ -264,9 +269,10 @@
                     orientationRectF.top = orientationRectF.bottom - touchHeight;
             }
         }
-        // One handed gestural only active on portrait mode
-        mOneHandedModeRegion.set(0, orientationRectF.bottom - mNavBarLargerGesturalHeight,
-                size.x, size.y);
+        updateOneHandedRegions(orientationRectF);
+        ActiveGestureProtoLogProxy.logCreateTouchRegionForDisplay(rotation, size, orientationRectF,
+                mOneHandedModeRegion, mNavBarGesturalHeight, mNavBarLargerGesturalHeight,
+                reason);
 
         return orientationRectF;
     }
@@ -286,6 +292,12 @@
         mAssistantRightRegion.left = orientationRectF.right - assistantWidth;
     }
 
+    private void updateOneHandedRegions(OrientationRectF orientationRectF) {
+        // One handed gestural only active on portrait mode
+        mOneHandedModeRegion.set(0, orientationRectF.bottom - mNavBarLargerGesturalHeight,
+                orientationRectF.right, orientationRectF.bottom);
+    }
+
     boolean touchInAssistantRegion(MotionEvent ev) {
         return mAssistantLeftRegion.contains(ev.getX(), ev.getY())
                 || mAssistantRightRegion.contains(ev.getX(), ev.getY());
@@ -411,9 +423,11 @@
             OrientationRectF rectF = mSwipeTouchRegions.get(key);
             regions.append(rectF).append(" ");
         }
-        pw.println(regions.toString());
+        pw.println(regions);
         pw.println("  mNavBarGesturalHeight=" + mNavBarGesturalHeight);
         pw.println("  mNavBarLargerGesturalHeight=" + mNavBarLargerGesturalHeight);
+        pw.println("  mAssistantLeftRegion=" + mAssistantLeftRegion);
+        pw.println("  mAssistantRightRegion=" + mAssistantRightRegion);
         pw.println("  mOneHandedModeRegion=" + mOneHandedModeRegion);
     }
 }
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..055aadb 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationController.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationController.java
@@ -17,14 +17,15 @@
 
 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;
 import android.util.Log;
+import android.view.RemoteAnimationTarget;
 import android.view.SurfaceControl;
 import android.view.WindowManagerGlobal;
 import android.window.PictureInPictureSurfaceTransaction;
+import android.window.WindowAnimationState;
 
 import androidx.annotation.UiThread;
 
@@ -32,7 +33,8 @@
 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.animation.TransitionAnimator;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
 import com.android.systemui.shared.system.RecentsAnimationControllerCompat;
@@ -89,6 +91,16 @@
     }
 
     @UiThread
+    public void handOffAnimation(RemoteAnimationTarget[] targets, WindowAnimationState[] states) {
+        if (TransitionAnimator.Companion.longLivedReturnAnimationsEnabled()) {
+            UI_HELPER_EXECUTOR.execute(() -> mController.handOffAnimation(targets, states));
+        } else {
+            Log.e(TAG, "Tried to hand off the animation, but the feature is disabled",
+                    new Exception());
+        }
+    }
+
+    @UiThread
     public void finishAnimationToHome() {
         finishController(true /* toRecents */, null, false /* sendUserLeaveHint */);
     }
@@ -132,10 +144,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 +153,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..91d0776 100644
--- a/quickstep/src/com/android/quickstep/RemoteTargetGluer.java
+++ b/quickstep/src/com/android/quickstep/RemoteTargetGluer.java
@@ -93,8 +93,7 @@
             BaseContainerInterface sizingStrategy, int numHandles, boolean forDesktop) {
         RemoteTargetHandle[] handles = new RemoteTargetHandle[numHandles];
         for (int i = 0; i < numHandles; i++) {
-            TaskViewSimulator tvs = new TaskViewSimulator(context, sizingStrategy);
-            tvs.setIsDesktopTask(forDesktop);
+            TaskViewSimulator tvs = new TaskViewSimulator(context, sizingStrategy, 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..909cc35 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() {
@@ -234,7 +236,8 @@
             return;
         }
 
-        mOrientationTouchTransformer.createOrAddTouchRegion(mDisplayController.getInfo());
+        mOrientationTouchTransformer.createOrAddTouchRegion(mDisplayController.getInfo(),
+                "RTH.updateGestureTouchRegions");
     }
 
     /**
@@ -271,7 +274,8 @@
 
             if (hasGestures(mMode)) {
                 updateGestureTouchRegions();
-                mOrientationTouchTransformer.createOrAddTouchRegion(info);
+                mOrientationTouchTransformer.createOrAddTouchRegion(info,
+                        "RTH.onDisplayInfoChanged");
                 mCurrentAppRotation = mDisplayRotation;
 
                 /* Update nav bars on the following:
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..fef4c30 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;
         }
@@ -348,7 +348,7 @@
         }
 
         /** Called when the snapshot has updated its full screen drawing parameters. */
-        public void setFullscreenParams(TaskView.FullscreenDrawParams fullscreenParams) {}
+        public void setFullscreenParams(FullscreenDrawParams fullscreenParams) {}
 
         /** Sets visibility for the overlay associated elements. */
         public void setVisibility(int visibility) {}
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..084cede 100644
--- a/quickstep/src/com/android/quickstep/TaskViewUtils.java
+++ b/quickstep/src/com/android/quickstep/TaskViewUtils.java
@@ -47,19 +47,21 @@
 import android.content.Context;
 import android.graphics.Matrix;
 import android.graphics.Matrix.ScaleToFit;
+import android.graphics.PointF;
 import android.graphics.Rect;
 import android.graphics.RectF;
+import android.util.Pair;
 import android.view.RemoteAnimationTarget;
 import android.view.SurfaceControl;
 import android.view.View;
 import android.window.TransitionInfo;
+import android.window.WindowAnimationState;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 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 +70,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 +83,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 +159,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 +209,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 +423,8 @@
 
         if (depthController != null) {
             out.setFloat(depthController.stateDepth, MULTI_PROPERTY_VALUE,
-                    BACKGROUND_APP.getDepth(baseActivity), TOUCH_RESPONSE);
+                    BACKGROUND_APP.getDepth(container),
+                    TOUCH_RESPONSE);
         }
     }
 
@@ -780,4 +787,44 @@
         animatorHandler.accept(dockFadeAnimator);
         return dockFadeAnimator;
     }
+
+    /**
+     * Creates an array of {@link RemoteAnimationTarget}s and a matching array of
+     * {@link WindowAnimationState}s from the provided handles.
+     * Important: the ordering of the two arrays is the same, so the state at each index of the
+     * second applies to the target in the same index of the first.
+     *
+     * @param handles The handles wrapping each target.
+     * @param timestamp The start time of the current frame.
+     * @param velocityPxPerMs The current velocity of the target animations.
+     */
+    @NonNull
+    public static Pair<RemoteAnimationTarget[], WindowAnimationState[]> extractTargetsAndStates(
+            @NonNull RemoteTargetHandle[] handles, long timestamp,
+            @NonNull PointF velocityPxPerMs) {
+        RemoteAnimationTarget[] targets = new RemoteAnimationTarget[handles.length];
+        WindowAnimationState[] animationStates = new WindowAnimationState[handles.length];
+
+        for (int i = 0; i < handles.length; i++) {
+            targets[i] = handles[i].getTransformParams().getTargetSet().apps[i];
+
+            TaskViewSimulator taskViewSimulator = handles[i].getTaskViewSimulator();
+            RectF startRect = taskViewSimulator.getCurrentRect();
+            float cornerRadius = taskViewSimulator.getCurrentCornerRadius();
+
+            WindowAnimationState state = new WindowAnimationState();
+            state.timestamp = timestamp;
+            state.bounds = new RectF(
+                    startRect.left, startRect.top, startRect.right, startRect.bottom);
+            state.topLeftRadius = cornerRadius;
+            state.topRightRadius = cornerRadius;
+            state.bottomRightRadius = cornerRadius;
+            state.bottomLeftRadius = cornerRadius;
+            state.velocityPxPerMs = velocityPxPerMs;
+
+            animationStates[i] = state;
+        }
+
+        return new Pair<>(targets, animationStates);
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/TopTaskTracker.java b/quickstep/src/com/android/quickstep/TopTaskTracker.java
index 3cf0542..210065a 100644
--- a/quickstep/src/com/android/quickstep/TopTaskTracker.java
+++ b/quickstep/src/com/android/quickstep/TopTaskTracker.java
@@ -18,16 +18,20 @@
 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.launcher3.util.SplitConfigurationOptions.STAGE_TYPE_A;
+import static com.android.wm.shell.Flags.enableFlexibleSplit;
+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 +49,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,50 +66,95 @@
  */
 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);
 
+        // Workaround for b/372067617, if the home task is being brought to front, then it will
+        // occlude all other tasks, so mark them as not-visible
+        if (taskInfo.getActivityType() == ACTIVITY_TYPE_HOME) {
+            // We've moved the task to the front of the list above, so only iterate the tasks after
+            for (int i = 1; i < mOrderedTaskList.size(); i++) {
+                final TaskInfo info = mOrderedTaskList.get(i);
+                if (info.displayId != taskInfo.displayId) {
+                    // Only fall through to reset visibility for tasks on the same display as the
+                    // home task being brought forward
+                    continue;
+                }
+                info.isVisible = false;
+                info.isVisibleRequested = false;
+            }
+        }
+
         // Keep the home display's top running task in the first while adding a non-home
         // 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 +164,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 +177,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 +217,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) {
@@ -147,7 +246,8 @@
             return;
         }
 
-        if (stage == SplitConfigurationOptions.STAGE_TYPE_MAIN) {
+        if (stage == SplitConfigurationOptions.STAGE_TYPE_MAIN
+                || (enableFlexibleSplit() && stage == STAGE_TYPE_A)) {
             mMainStagePosition.taskId = taskId;
         } else {
             mSideStagePosition.taskId = taskId;
@@ -156,11 +256,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 +277,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 +337,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 +379,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 +459,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 +479,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 +510,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..8edb16f 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -24,7 +24,6 @@
 
 import static com.android.launcher3.Flags.enableCursorHoverStates;
 import static com.android.launcher3.Flags.enableHandleDelayedGestureCallbacks;
-import static com.android.launcher3.Flags.useActivityOverlay;
 import static com.android.launcher3.LauncherPrefs.backedUpItem;
 import static com.android.launcher3.MotionEventsUtils.isTrackpadMotionEvent;
 import static com.android.launcher3.MotionEventsUtils.isTrackpadMultiFingerSwipe;
@@ -35,17 +34,12 @@
 import static com.android.quickstep.GestureState.DEFAULT_STATE;
 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.quickstep.InputConsumerUtils.newConsumer;
+import static com.android.quickstep.InputConsumerUtils.tryCreateAssistantInputConsumer;
 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;
 import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_UNLOCK_ANIMATION_CONTROLLER;
-import static com.android.wm.shell.Flags.enableBubblesLongPressNavHandle;
 import static com.android.wm.shell.shared.ShellSharedConstants.KEY_EXTRA_SHELL_BACK_ANIMATION;
 import static com.android.wm.shell.shared.ShellSharedConstants.KEY_EXTRA_SHELL_BUBBLES;
 import static com.android.wm.shell.shared.ShellSharedConstants.KEY_EXTRA_SHELL_DESKTOP_MODE;
@@ -78,17 +72,18 @@
 import android.view.MotionEvent;
 
 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,25 +102,16 @@
 import com.android.launcher3.util.ScreenOnTracker;
 import com.android.launcher3.util.TraceHelper;
 import com.android.quickstep.OverviewCommandHelper.CommandType;
-import com.android.quickstep.inputconsumers.AccessibilityInputConsumer;
-import com.android.quickstep.inputconsumers.AssistantInputConsumer;
+import com.android.quickstep.fallback.window.RecentsWindowManager;
+import com.android.quickstep.fallback.window.RecentsWindowSwipeHandler;
 import com.android.quickstep.inputconsumers.BubbleBarInputConsumer;
-import com.android.quickstep.inputconsumers.DeviceLockedInputConsumer;
-import com.android.quickstep.inputconsumers.NavHandleLongPressInputConsumer;
 import com.android.quickstep.inputconsumers.OneHandedModeInputConsumer;
-import com.android.quickstep.inputconsumers.OtherActivityInputConsumer;
-import com.android.quickstep.inputconsumers.OverviewInputConsumer;
-import com.android.quickstep.inputconsumers.OverviewWithoutFocusInputConsumer;
-import com.android.quickstep.inputconsumers.ProgressDelegateInputConsumer;
 import com.android.quickstep.inputconsumers.ResetGestureInputConsumer;
-import com.android.quickstep.inputconsumers.ScreenPinnedInputConsumer;
-import com.android.quickstep.inputconsumers.SysUiOverlayInputConsumer;
-import com.android.quickstep.inputconsumers.TaskbarUnstashInputConsumer;
-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;
@@ -151,6 +137,7 @@
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.lang.ref.WeakReference;
+import java.util.HashSet;
 import java.util.Set;
 import java.util.function.Consumer;
 import java.util.function.Function;
@@ -177,7 +164,7 @@
 
         private final WeakReference<TouchInteractionService> mTis;
 
-        @Nullable private Runnable mOnOverviewTargetChangeListener = null;
+        private final Set<Runnable> mOnOverviewTargetChangeListeners = new HashSet<>();
 
         private TISBinder(TouchInteractionService tis) {
             mTis = new WeakReference<>(tis);
@@ -299,7 +286,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 +312,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 +331,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
@@ -525,15 +513,19 @@
                     tis -> tis.mDeviceState.setGestureBlockingTaskId(taskId));
         }
 
-        /** Sets a listener to be run on Overview Target updates. */
-        public void setOverviewTargetChangeListener(@Nullable Runnable listener) {
-            mOnOverviewTargetChangeListener = listener;
+        /** Registers a listener to be run on Overview Target updates. */
+        public void registerOverviewTargetChangeListener(@NonNull Runnable listener) {
+            mOnOverviewTargetChangeListeners.add(listener);
+        }
+
+        /** Unregisters an OverviewTargetChange listener. */
+        public void unregisterOverviewTargetChangeListener(@NonNull Runnable listener) {
+            mOnOverviewTargetChangeListeners.remove(listener);
         }
 
         protected void onOverviewTargetChange() {
-            if (mOnOverviewTargetChangeListener != null) {
-                mOnOverviewTargetChangeListener.run();
-                mOnOverviewTargetChangeListener = null;
+            for (Runnable listener : mOnOverviewTargetChangeListeners) {
+                listener.run();
             }
         }
 
@@ -551,11 +543,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 +561,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 +582,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 +608,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 +645,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 +654,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 +679,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 +696,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 +734,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 +755,8 @@
 
         mOverviewComponentObserver.setOverviewChangeListener(this::onOverviewTargetChange);
         onOverviewTargetChange(mOverviewComponentObserver.isHomeAndOverviewSame());
+
+        mTaskbarManager.onUserUnlocked();
     }
 
     public OverviewCommandHelper getOverviewCommandHelper() {
@@ -761,11 +781,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 +819,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 +843,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 +879,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 +888,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 +916,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 +931,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 +941,28 @@
             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);
+                    mUncheckedConsumer = tryCreateAssistantInputConsumer(
+                            this, mDeviceState, mInputMonitorCompat, 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);
@@ -952,23 +970,40 @@
                         getTrackpadGestureType(event));
                 mConsumer.onConsumerAboutToBeSwitched();
                 mGestureState = newGestureState;
-                mConsumer = newConsumer(prevGestureState, mGestureState, event);
+                mConsumer = newConsumer(
+                        getBaseContext(),
+                        this,
+                        mResetGestureInputConsumer,
+                        mOverviewComponentObserver,
+                        mDeviceState,
+                        prevGestureState,
+                        mGestureState,
+                        mTaskAnimationManager,
+                        mInputMonitorCompat,
+                        getSwipeUpHandlerFactory(),
+                        this::onConsumerInactive,
+                        mInputEventReceiver,
+                        mTaskbarManager,
+                        mSwipeUpProxyProvider,
+                        mOverviewCommandHelper,
+                        event);
                 mUncheckedConsumer = mConsumer;
             } 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);
+                mUncheckedConsumer = tryCreateAssistantInputConsumer(
+                        this, mDeviceState, mInputMonitorCompat, 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 +1021,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()));
                 }
             }
         }
@@ -1063,28 +1082,6 @@
         return event.isHoverEvent() && event.getSource() == InputDevice.SOURCE_MOUSE;
     }
 
-    private InputConsumer tryCreateAssistantInputConsumer(
-            GestureState gestureState, MotionEvent motionEvent) {
-        return tryCreateAssistantInputConsumer(
-                InputConsumer.NO_OP, gestureState, motionEvent, CompoundString.NO_OP);
-    }
-
-    private InputConsumer tryCreateAssistantInputConsumer(
-            InputConsumer base,
-            GestureState gestureState,
-            MotionEvent motionEvent,
-            CompoundString reasonString) {
-        if (mDeviceState.isGestureBlockedTask(gestureState.getRunningTask())) {
-            reasonString.append(SUBSTRING_PREFIX)
-                    .append("is gesture-blocked task, using base input consumer");
-            return base;
-        } else {
-            reasonString.append(SUBSTRING_PREFIX).append("using AssistantInputConsumer");
-            return new AssistantInputConsumer(
-                    this, gestureState, base, mInputMonitorCompat, mDeviceState, motionEvent);
-        }
-    }
-
     public GestureState createGestureState(GestureState previousGestureState,
             GestureState.TrackpadGestureType trackpadGestureType) {
         final GestureState gestureState;
@@ -1110,404 +1107,17 @@
         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;
     }
 
-    private InputConsumer newConsumer(
-            GestureState previousGestureState, GestureState newGestureState, MotionEvent event) {
-        TaskbarActivityContext tac = mTaskbarManager.getCurrentActivityContext();
-        BubbleControllers bubbleControllers = tac != null ? tac.getBubbleControllers() : null;
-        if (bubbleControllers != null && BubbleBarInputConsumer.isEventOnBubbles(tac, event)) {
-            InputConsumer consumer = new BubbleBarInputConsumer(this, bubbleControllers,
-                    mInputMonitorCompat);
-            logInputConsumerSelectionReason(consumer, newCompoundString(
-                    "event is on bubbles, creating new input consumer"));
-            return consumer;
-        }
-        AnimatedFloat progressProxy = mSwipeUpProxyProvider.apply(mGestureState);
-        if (progressProxy != null) {
-            InputConsumer consumer = new ProgressDelegateInputConsumer(
-                    this, mTaskAnimationManager, mGestureState, mInputMonitorCompat, progressProxy);
-
-            logInputConsumerSelectionReason(consumer, newCompoundString(
-                    "mSwipeUpProxyProvider has been set, using ProgressDelegateInputConsumer"));
-
-            return consumer;
-        }
-
-        boolean canStartSystemGesture =
-                mGestureState.isTrackpadGesture() ? mDeviceState.canStartTrackpadGesture()
-                        : mDeviceState.canStartSystemGesture();
-
-        if (!LockedUserState.get(this).isUserUnlocked()) {
-            CompoundString reasonString = newCompoundString("device locked");
-            InputConsumer consumer;
-            if (canStartSystemGesture) {
-                // 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"));
-            } else {
-                consumer = getDefaultInputConsumer(
-                        reasonString.append(SUBSTRING_PREFIX)
-                                .append("cannot start system gesture"));
-            }
-            logInputConsumerSelectionReason(consumer, reasonString);
-            return consumer;
-        }
-
-        CompoundString reasonString;
-        InputConsumer base;
-        // When there is an existing recents animation running, bypass systemState check as this is
-        // 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");
-            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");
-            base = getDefaultInputConsumer(reasonString);
-        }
-        if (mDeviceState.isGesturalNavMode() || newGestureState.isTrackpadGesture()) {
-            handleOrientationSetup(base);
-        }
-        if (mDeviceState.isFullyGesturalNavMode() || newGestureState.isTrackpadGesture()) {
-            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");
-                base = tryCreateAssistantInputConsumer(base, newGestureState, event, reasonString);
-            }
-
-            // If Taskbar is present, we listen for swipe or cursor hover events to unstash it.
-            if (tac != null && !(base instanceof AssistantInputConsumer)) {
-                // Present always on large screen or on small screen w/ flag
-                boolean useTaskbarConsumer = tac.getDeviceProfile().isTaskbarPresent
-                        && !tac.isPhoneMode()
-                        && !tac.isInStashedLauncherState();
-                if (canStartSystemGesture && useTaskbarConsumer) {
-                    reasonString.append(NEWLINE_PREFIX)
-                            .append(reasonPrefix)
-                            .append(SUBSTRING_PREFIX)
-                            .append("TaskbarActivityContext != null, ")
-                            .append("using TaskbarUnstashInputConsumer");
-                    base = new TaskbarUnstashInputConsumer(this, base, mInputMonitorCompat, tac,
-                            mOverviewCommandHelper, mGestureState);
-                }
-            }
-            if (enableBubblesLongPressNavHandle()) {
-                // 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");
-                    // Bubbles can handle home gesture itself.
-                    base = getDefaultInputConsumer(reasonString);
-                }
-            }
-
-            NavHandle navHandle = tac != null ? tac.getNavHandle()
-                    : SystemUiProxy.INSTANCE.get(this);
-            if (canStartSystemGesture && !previousGestureState.isRecentsAnimationRunning()
-                    && navHandle.canNavHandleBeLongPressed()
-                    && !ignoreThreeFingerTrackpadForNavHandleLongPress(mGestureState)) {
-                reasonString.append(NEWLINE_PREFIX)
-                        .append(reasonPrefix)
-                        .append(SUBSTRING_PREFIX)
-                        .append("Not running recents animation, ");
-                if (tac != null && tac.getNavHandle().canNavHandleBeLongPressed()) {
-                    reasonString.append("stashed handle is long-pressable, ");
-                }
-                reasonString.append("using NavHandleLongPressInputConsumer");
-                base = new NavHandleLongPressInputConsumer(this, base, mInputMonitorCompat,
-                        mDeviceState, navHandle, mGestureState);
-            }
-
-            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");
-                    // 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");
-                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");
-                base = new TrackpadStatusBarInputConsumer(getBaseContext(), base,
-                        mInputMonitorCompat);
-            }
-
-            if (mDeviceState.isScreenPinningActive()) {
-                reasonString = newCompoundString(reasonPrefix)
-                        .append(SUBSTRING_PREFIX)
-                        .append("screen pinning is active, using ScreenPinnedInputConsumer");
-                // 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");
-                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");
-                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");
-                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");
-                base = new OneHandedModeInputConsumer(
-                        this, mDeviceState, base, mInputMonitorCompat);
-            }
-        }
-        logInputConsumerSelectionReason(base, reasonString);
-        return base;
-    }
-
-    private CompoundString newCompoundString(String substring) {
-        return new CompoundString(NEWLINE_PREFIX).append(substring);
-    }
-
-    private boolean ignoreThreeFingerTrackpadForNavHandleLongPress(GestureState gestureState) {
-        return Flags.ignoreThreeFingerTrackpadForNavHandleLongPress()
-                && gestureState.isThreeFingerTrackpadGesture();
-    }
-
-    private void logInputConsumerSelectionReason(
-            InputConsumer consumer, CompoundString reasonString) {
-        ActiveGestureLog.INSTANCE.addLog(new CompoundString("setInputConsumer: ")
-                .append(consumer.getName())
-                .append(". reason(s):")
-                .append(reasonString));
-        if ((consumer.getType() & InputConsumer.TYPE_OTHER_ACTIVITY) != 0) {
-            ActiveGestureLog.INSTANCE.trackEvent(FLAG_USING_OTHER_ACTIVITY_INPUT_CONSUMER);
-        }
-    }
-
-    private void handleOrientationSetup(InputConsumer baseInputConsumer) {
-        baseInputConsumer.notifyOrientationSetup();
-    }
-
-    private InputConsumer newBaseConsumer(
-            GestureState previousGestureState,
-            GestureState gestureState,
-            MotionEvent event,
-            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"));
-        }
-
-        reasonString.append(SUBSTRING_PREFIX).append("keyguard is not showing occluded");
-
-        TopTaskTracker.CachedTaskInfo runningTask = gestureState.getRunningTask();
-        // Use overview input consumer for sharesheets on top of home.
-        boolean forceOverviewInputConsumer = gestureState.getContainerInterface().isStarted()
-                && 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);
-        }
-
-        boolean previousGestureAnimatedToLauncher =
-                previousGestureState.isRunningAnimationToLauncher()
-                        || mDeviceState.isPredictiveBackToHomeInProgress();
-        // with shell-transitions, home is resumed during recents animation, so
-        // explicitly check against recents animation too.
-        boolean launcherResumedThroughShellTransition =
-                gestureState.getContainerInterface().isResumed()
-                        && !previousGestureState.isRecentsAnimationRunning();
-        // If a task fragment within Launcher is resumed
-        boolean launcherChildActivityResumed = useActivityOverlay()
-                && runningTask != null
-                && runningTask.isHomeTask()
-                && mOverviewComponentObserver.isHomeAndOverviewSame()
-                && !launcherResumedThroughShellTransition
-                && !previousGestureState.isRecentsAnimationRunning();
-
-        if (gestureState.getContainerInterface().isInLiveTileMode()) {
-            return createOverviewInputConsumer(
-                    previousGestureState,
-                    gestureState,
-                    event,
-                    forceOverviewInputConsumer,
-                    reasonString.append(SUBSTRING_PREFIX)
-                            .append("is in live tile mode, trying to use overview input consumer"));
-        } else if (runningTask == null) {
-            return getDefaultInputConsumer(reasonString.append(SUBSTRING_PREFIX)
-                    .append("running task == null"));
-        } else if (previousGestureAnimatedToLauncher
-                || launcherResumedThroughShellTransition
-                || forceOverviewInputConsumer) {
-            return createOverviewInputConsumer(
-                    previousGestureState,
-                    gestureState,
-                    event,
-                    forceOverviewInputConsumer,
-                    reasonString.append(SUBSTRING_PREFIX)
-                            .append(previousGestureAnimatedToLauncher
-                                    ? "previous gesture animated to launcher"
-                                    : (launcherResumedThroughShellTransition
-                                            ? "launcher resumed through a shell transition"
-                                            : "forceOverviewInputConsumer == true"))
-                            .append(", trying to use overview input consumer"));
-        } 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"));
-        } else {
-            reasonString.append(SUBSTRING_PREFIX)
-                    .append("using OtherActivityInputConsumer");
-            return createOtherActivityInputConsumer(gestureState, event);
-        }
-    }
-
     public AbsSwipeUpHandler.Factory getSwipeUpHandlerFactory() {
-        return !mOverviewComponentObserver.isHomeAndOverviewSame()
-                ? mFallbackSwipeHandlerFactory : mLauncherSwipeHandlerFactory;
-    }
-
-    private InputConsumer createOtherActivityInputConsumer(GestureState gestureState,
-            MotionEvent event) {
-
-        final AbsSwipeUpHandler.Factory factory = getSwipeUpHandlerFactory();
-        final boolean shouldDefer = !mOverviewComponentObserver.isHomeAndOverviewSame()
-                || gestureState.getContainerInterface().deferStartingActivity(mDeviceState, event);
-        final boolean disableHorizontalSwipe = mDeviceState.isInExclusionRegion(event);
-        return new OtherActivityInputConsumer(this, mDeviceState, mTaskAnimationManager,
-                gestureState, shouldDefer, this::onConsumerInactive,
-                mInputMonitorCompat, mInputEventReceiver, disableHorizontalSwipe, factory);
-    }
-
-    private InputConsumer createDeviceLockedInputConsumer(
-            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");
-            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"));
-        }
-    }
-
-    public InputConsumer createOverviewInputConsumer(
-            GestureState previousGestureState,
-            GestureState gestureState,
-            MotionEvent event,
-            boolean forceOverviewInputConsumer,
-            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"));
-        }
-
-        boolean hasWindowFocus = container.getRootView().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"));
-        if (hasWindowFocus
-                || isPreviousGestureAnimatingToLauncher
-                || isInLiveTileMode) {
-            reasonString.append(SUBSTRING_PREFIX)
-                    .append("overview should have focus, using OverviewInputConsumer");
-            return new OverviewInputConsumer(gestureState, container, mInputMonitorCompat,
-                    false /* startingInActivityBounds */);
-        } else {
-            reasonString.append(SUBSTRING_PREFIX).append(
-                    "overview shouldn't have focus, using OverviewWithoutFocusInputConsumer");
-            final boolean disableHorizontalSwipe = mDeviceState.isInExclusionRegion(event);
-            return new OverviewWithoutFocusInputConsumer(container.asContext(), mDeviceState,
-                    gestureState, mInputMonitorCompat, disableHorizontalSwipe);
-        }
+        boolean recentsInWindow =
+                Flags.enableFallbackOverviewInWindow() || Flags.enableLauncherOverviewInWindow();
+        return mOverviewComponentObserver.isHomeAndOverviewSame()
+                ? mLauncherSwipeHandlerFactory : (recentsInWindow
+                ? mRecentsWindowSwipeHandlerFactory : mFallbackSwipeHandlerFactory);
     }
 
     /**
@@ -1540,12 +1150,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 +1187,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 +1201,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 +1210,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 +1260,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 +1272,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 +1297,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..e158975
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowManager.kt
@@ -0,0 +1,461 @@
+/*
+ * 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() {
+        startHome(/* finishRecentsAnimation= */ true)
+    }
+
+    fun startHome(finishRecentsAnimation: Boolean) {
+        val recentsView: RecentsView<*, *> = getOverviewPanel()
+
+        if (!finishRecentsAnimation) {
+            recentsView.switchToScreenshot(/* onFinishRunnable= */ null)
+            startHomeInternal()
+            return
+        }
+        recentsView.switchToScreenshot {
+            recentsView.finishRecentsAnimation(/* toRecents= */ true) { startHomeInternal() }
+        }
+    }
+
+    private fun startHomeInternal() {
+        val runner = LauncherAnimationRunner(mainThreadHandler, 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..be71385
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowSwipeHandler.java
@@ -0,0 +1,466 @@
+/*
+ * 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.PointF;
+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 android.view.animation.Interpolator;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+
+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();
+        }
+    }
+
+    @UiThread
+    @Override
+    protected void animateGestureEnd(
+            float startShift,
+            float endShift,
+            long duration,
+            @NonNull Interpolator interpolator,
+            @NonNull GestureState.GestureEndTarget endTarget,
+            @NonNull PointF velocityPxPerMs) {
+        boolean fromHomeToHome = mRunningOverHome
+                && endTarget == GestureState.GestureEndTarget.HOME;
+        if (fromHomeToHome) {
+            mRecentsWindowManager.startHome(/* finishRecentsAnimation= */ false);
+        }
+        super.animateGestureEnd(
+                startShift,
+                endShift,
+                fromHomeToHome ? 0 : duration,
+                interpolator,
+                endTarget,
+                velocityPxPerMs);
+    }
+
+    private void updateHomeActivityTransformDuringSwipeUp(SurfaceProperties builder,
+            RemoteAnimationTarget app, TransformParams params) {
+        if (mActiveAnimationFactory != null) {
+            return;
+        }
+        setHomeScaleAndAlpha(builder, app, mCurrentShift.value,
+                Utilities.boundToRange(1 - mCurrentShift.value, 0, 1));
+    }
+
+    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/interaction/AllSetActivity.java b/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java
index f7f3157..3eba9c0 100644
--- a/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java
+++ b/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java
@@ -121,6 +121,8 @@
 
     private TextView mHintView;
 
+    private final Runnable mOverviewTargetChangeRunnable = this::onOverviewTargetChanged;
+
     @Override
     protected void onCreate(@Nullable Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
@@ -285,7 +287,7 @@
     private void onTISConnected(TISBinder binder) {
         setSetupUIVisible(isResumed());
         binder.setSwipeUpProxy(isResumed() ? this::createSwipeUpProxy : null);
-        binder.setOverviewTargetChangeListener(binder::preloadOverviewForSUWAllSet);
+        binder.registerOverviewTargetChangeListener(mOverviewTargetChangeRunnable);
         binder.preloadOverviewForSUWAllSet();
         TaskbarManager taskbarManager = binder.getTaskbarManager();
         if (taskbarManager != null) {
@@ -293,6 +295,14 @@
         }
     }
 
+    private void onOverviewTargetChanged() {
+        TISBinder binder = mTISBindHelper.getBinder();
+        if (binder != null) {
+            binder.preloadOverviewForSUWAllSet();
+            binder.unregisterOverviewTargetChangeListener(mOverviewTargetChangeRunnable);
+        }
+    }
+
     @Override
     protected void onPause() {
         super.onPause();
@@ -309,7 +319,7 @@
         if (binder != null) {
             setSetupUIVisible(false);
             binder.setSwipeUpProxy(null);
-            binder.setOverviewTargetChangeListener(null);
+            binder.unregisterOverviewTargetChangeListener(mOverviewTargetChangeRunnable);
         }
     }
 
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..b78e214 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,9 +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.TaskViewData
-import com.android.quickstep.task.viewmodel.TaskViewModel
-import com.android.quickstep.views.TaskViewType
+import com.android.quickstep.task.viewmodel.TaskThumbnailViewModelImpl
 import com.android.systemui.shared.recents.model.Task
 import kotlinx.coroutines.CoroutineName
 import kotlinx.coroutines.CoroutineScope
@@ -62,11 +61,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 +79,7 @@
                         iconCache,
                         taskVisualsChangedDelegate,
                         recentsCoroutineScope,
-                        ProductionDispatchers
+                        ProductionDispatchers,
                     )
                 }
             set(RecentTasksRepository::class.java.simpleName, recentTasksRepository)
@@ -112,6 +112,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 +151,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 +166,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,23 +178,18 @@
                             iconCache,
                             get(),
                             get(),
-                            ProductionDispatchers
+                            ProductionDispatchers,
                         )
                     }
                 }
                 RecentsViewData::class.java -> RecentsViewData()
-                TaskViewModel::class.java -> TaskViewModel(taskViewData = inject(scopeId, extras))
-                TaskViewData::class.java -> {
-                    val taskViewType = extras["TaskViewType"] as TaskViewType
-                    TaskViewData(taskViewType)
-                }
                 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 +200,7 @@
                         task = task,
                         recentsViewData = inject(),
                         recentTasksRepository = inject(),
-                        getThumbnailPositionUseCase = inject()
+                        getThumbnailPositionUseCase = inject(),
                     )
                 }
                 GetThumbnailUseCase::class.java -> GetThumbnailUseCase(taskRepository = inject())
@@ -203,7 +210,7 @@
                     GetThumbnailPositionUseCase(
                         deviceProfileRepository = inject(),
                         rotationStateRepository = inject(),
-                        tasksRepository = inject()
+                        tasksRepository = inject(),
                     )
                 SplashAlphaUseCase::class.java ->
                     SplashAlphaUseCase(
@@ -218,7 +225,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 +259,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/RecentsViewData.kt b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewData.kt
index 87446b0..6ccf372 100644
--- a/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewData.kt
+++ b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewData.kt
@@ -22,9 +22,6 @@
 class RecentsViewData {
     val fullscreenProgress = MutableStateFlow(1f)
 
-    // This is typically a View concern but it is used to invalidate rendering in other Views
-    val scale = MutableStateFlow(1f)
-
     // Whether the current RecentsView state supports task overlays.
     // TODO(b/331753115): Derive from RecentsView state flow once migrated to MVVM.
     val overlayEnabled = MutableStateFlow(false)
diff --git a/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt
index 5cf6823..cfebb81 100644
--- a/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt
+++ b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt
@@ -31,11 +31,7 @@
     }
 
     fun updateVisibleTasks(visibleTaskIdList: List<Int>) {
-        recentsTasksRepository.setVisibleTasks(visibleTaskIdList)
-    }
-
-    fun updateScale(scale: Float) {
-        recentsViewData.scale.value = scale
+        recentsTasksRepository.setVisibleTasks(visibleTaskIdList.toSet())
     }
 
     fun updateFullscreenProgress(fullscreenProgress: 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..0c783d3 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
@@ -17,31 +17,27 @@
 package com.android.quickstep.task.thumbnail
 
 import android.content.Context
-import android.content.res.Configuration
 import android.graphics.Color
 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
 import androidx.constraintlayout.widget.ConstraintLayout
 import androidx.core.view.isInvisible
 import com.android.launcher3.R
-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
 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.SnapshotSplash
 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
 import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel
-import com.android.quickstep.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 +47,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
 
@@ -64,18 +59,15 @@
     private val splashIcon: FixedSizeImageView by lazy { findViewById(R.id.splash_icon) }
 
     private var uiState: TaskThumbnailUiState = Uninitialized
-    private var inheritedScale: Float = 1f
 
-    private val _measuredBounds = Rect()
-    private val measuredBounds: Rect
-        get() {
-            _measuredBounds.set(0, 0, measuredWidth, measuredHeight)
-            return _measuredBounds
+    private val bounds = Rect()
+
+    var cornerRadius: Float = 0f
+        set(value) {
+            field = value
+            invalidateOutline()
         }
 
-    private var overviewCornerRadius: Float = TaskCornerRadius.get(context)
-    private var fullscreenCornerRadius: Float = QuickStepContract.getWindowCornerRadius(context)
-
     constructor(context: Context) : super(context)
 
     constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
@@ -90,8 +82,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) {
@@ -111,19 +107,12 @@
                 splashIcon.alpha = splashAlpha
             }
             .launchIn(viewAttachedScope)
-        viewModel.cornerRadiusProgress.onEach { invalidateOutline() }.launchIn(viewAttachedScope)
-        viewModel.inheritedScale
-            .onEach { viewModelInheritedScale ->
-                inheritedScale = viewModelInheritedScale
-                invalidateOutline()
-            }
-            .launchIn(viewAttachedScope)
 
         clipToOutline = true
         outlineProvider =
             object : ViewOutlineProvider() {
                 override fun getOutline(view: View, outline: Outline) {
-                    outline.setRoundRect(measuredBounds, getCurrentCornerRadius())
+                    outline.setRoundRect(bounds, cornerRadius)
                 }
             }
     }
@@ -140,16 +129,22 @@
     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) {
             setImageMatrix()
         }
+        bounds.set(0, 0, w, h)
+        invalidateOutline()
     }
 
     override fun setScaleX(scaleX: Float) {
@@ -164,14 +159,6 @@
         splashIcon.scaleY = 1 / scaleY
     }
 
-    override fun onConfigurationChanged(newConfig: Configuration?) {
-        super.onConfigurationChanged(newConfig)
-
-        overviewCornerRadius = TaskCornerRadius.get(context)
-        fullscreenCornerRadius = QuickStepContract.getWindowCornerRadius(context)
-        invalidateOutline()
-    }
-
     private fun resetViews() {
         liveTileView.isInvisible = true
         thumbnailView.isInvisible = true
@@ -207,10 +194,7 @@
         thumbnailView.imageMatrix = viewModel.getThumbnailPositionState(width, height, isLayoutRtl)
     }
 
-    private fun getCurrentCornerRadius() =
-        Utilities.mapRange(
-            viewModel.cornerRadiusProgress.value,
-            overviewCornerRadius,
-            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..a048a1d 100644
--- a/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt
+++ b/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt
@@ -10,134 +10,30 @@
  * 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
 
-@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 {
+    /** Provides the level of dimming that the View should have */
+    val dimProgress: Flow<Float>
 
-    /**
-     * 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()
+    /** Provides the alpha of the splash icon */
+    val splashAlpha: Flow<Float>
 
-    val inheritedScale =
-        combine(recentsViewData.scale, taskViewData.scale) { recentsScale, taskScale ->
-            recentsScale * taskScale
-        }
+    /** Provides the UiState by which the task thumbnail can be represented */
+    val uiState: Flow<TaskThumbnailUiState>
 
-    val dimProgress: Flow<Float> =
-        combine(taskContainerData.taskMenuOpenProgress, recentsViewData.tintAmount) {
-            taskMenuOpenProgress,
-            tintAmount ->
-            max(taskMenuOpenProgress * MAX_SCRIM_ALPHA, tintAmount)
-        }
-    val splashAlpha = splashProgress.flatMapLatest { it }
+    /** Attaches this ViewModel to a specific task id for it to provide data from. */
+    fun bind(taskId: Int)
 
-    private val isLiveTile =
-        combine(
-                task.flatMapLatest { it }.map { it?.key?.id }.distinctUntilChanged(),
-                recentsViewData.runningTaskIds,
-                recentsViewData.runningTaskShowScreenshot
-            ) { taskId, runningTaskIds, runningTaskShowScreenshot ->
-                runningTaskIds.contains(taskId) && !runningTaskShowScreenshot
-            }
-            .distinctUntilChanged()
-
-    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()
-
-    fun bind(taskId: Int) {
-        this.taskId = taskId
-        task.value = tasksRepository.getTaskDataById(taskId)
-        splashProgress.value = splashAlphaUseCase.execute(taskId)
-    }
-
-    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..b6cb984
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModelImpl.kt
@@ -0,0 +1,144 @@
+/*
+ * 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.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,
+    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 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/task/viewmodel/TaskViewData.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskViewData.kt
deleted file mode 100644
index 7a9ecf2..0000000
--- a/quickstep/src/com/android/quickstep/task/viewmodel/TaskViewData.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.quickstep.task.viewmodel
-
-import com.android.quickstep.views.TaskViewType
-import kotlinx.coroutines.flow.MutableStateFlow
-
-class TaskViewData(taskViewType: TaskViewType) {
-    // This is typically a View concern but it is used to invalidate rendering in other Views
-    val scale = MutableStateFlow(1f)
-
-    // TODO(b/331753115): This property should not be in TaskViewData once TaskView is MVVM.
-    /** Whether outline of TaskView is formed by outline thumbnail view(s). */
-    val isOutlineFormedByThumbnailView: Boolean = taskViewType != TaskViewType.DESKTOP
-}
diff --git a/quickstep/src/com/android/quickstep/util/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..334ff06
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/ContextualSearchStateManager.java
@@ -0,0 +1,282 @@
+/*
+ * 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.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);
+
+    // 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);
+
+        SettingsCache.INSTANCE.get(context).register(SEARCH_ALL_ENTRYPOINTS_ENABLED_URI,
+                mContextualSearchSettingChangedListener);
+        onContextualSearchSettingChanged(
+                SettingsCache.INSTANCE.get(context).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();
+        SettingsCache.INSTANCE.get(mContext).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/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/StaggeredWorkspaceAnim.java b/quickstep/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java
index 997a842..12ca257 100644
--- a/quickstep/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java
+++ b/quickstep/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java
@@ -49,6 +49,7 @@
 import com.android.launcher3.statehandlers.DepthController;
 import com.android.launcher3.states.StateAnimationConfig;
 import com.android.launcher3.uioverrides.QuickstepLauncher;
+import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.DynamicResource;
 import com.android.quickstep.views.RecentsView;
 import com.android.systemui.plugins.ResourceProvider;
@@ -63,8 +64,7 @@
     private static final int APP_CLOSE_ROW_START_DELAY_MS = 10;
     // Should be used for animations running alongside this StaggeredWorkspaceAnim.
     public static final int DURATION_MS = 250;
-    public static final int DURATION_TASKBAR_MS =
-            QuickstepTransitionManager.getTaskbarToHomeDuration();
+    private final int mTaskbarDurationInMs;
 
     private static final float MAX_VELOCITY_PX_PER_S = 22f;
 
@@ -81,6 +81,8 @@
 
     public StaggeredWorkspaceAnim(QuickstepLauncher launcher, float velocity,
             boolean animateOverviewScrim, @Nullable View ignoredView, boolean staggerWorkspace) {
+        mTaskbarDurationInMs = QuickstepTransitionManager.getTaskbarToHomeDuration(
+                DisplayController.isPinnedTaskbar(launcher));
         prepareToAnimate(launcher, animateOverviewScrim);
 
         mIgnoredView = ignoredView;
@@ -93,7 +95,7 @@
                 .getDimensionPixelSize(R.dimen.swipe_up_max_workspace_trans_y);
 
         DeviceProfile grid = launcher.getDeviceProfile();
-        long duration = grid.isTaskbarPresent ? DURATION_TASKBAR_MS : DURATION_MS;
+        long duration = grid.isTaskbarPresent ? mTaskbarDurationInMs : DURATION_MS;
         if (staggerWorkspace) {
             Workspace<?> workspace = launcher.getWorkspace();
             Hotseat hotseat = launcher.getHotseat();
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..706cfe4 100644
--- a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
+++ b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
@@ -50,9 +50,10 @@
 import com.android.launcher3.util.TraceHelper;
 import com.android.quickstep.BaseActivityInterface;
 import com.android.quickstep.BaseContainerInterface;
+import com.android.quickstep.DesktopFullscreenDrawParams;
+import com.android.quickstep.FullscreenDrawParams;
 import com.android.quickstep.TaskAnimationManager;
 import com.android.quickstep.util.SurfaceTransaction.SurfaceProperties;
-import com.android.quickstep.views.TaskView.FullscreenDrawParams;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.recents.utilities.PreviewPositionHelper;
 
@@ -116,19 +117,25 @@
     private SplitBounds mSplitBounds;
     private Boolean mDrawsBelowRecents = null;
     private boolean mIsGridTask;
-    private boolean mIsDesktopTask;
+    private final boolean mIsDesktopTask;
     private boolean mScaleToCarouselTaskSize = false;
     private int mTaskRectTranslationX;
     private int mTaskRectTranslationY;
+    private int mDesktopTaskIndex = 0;
 
-    public TaskViewSimulator(Context context, BaseContainerInterface sizeStrategy) {
+    public TaskViewSimulator(Context context, BaseContainerInterface sizeStrategy,
+            boolean isDesktop, int desktopTaskIndex) {
         mContext = context;
         mSizeStrategy = sizeStrategy;
+        mIsDesktopTask = isDesktop;
+        mDesktopTaskIndex = desktopTaskIndex;
 
         mOrientationState = TraceHelper.allowIpcs("TaskViewSimulator.init",
                 () -> new RecentsOrientedState(context, sizeStrategy, i -> { }));
         mOrientationState.setGestureActive(true);
-        mCurrentFullscreenParams = new FullscreenDrawParams(context);
+        mCurrentFullscreenParams = mIsDesktopTask
+                ? new DesktopFullscreenDrawParams(context)
+                : new FullscreenDrawParams(context);
         mOrientationStateId = mOrientationState.getStateId();
         Resources resources = context.getResources();
         mIsRecentsRtl = mOrientationState.getOrientationHandler().getRecentsRtlSetting(resources);
@@ -288,13 +295,6 @@
     }
 
     /**
-     * Sets whether this task is part of desktop tasks in overview.
-     */
-    public void setIsDesktopTask(boolean desktop) {
-        mIsDesktopTask = desktop;
-    }
-
-    /**
      * Apply translations on TaskRect's starting location.
      */
     public void setTaskRectTranslation(int taskRectTranslationX, int taskRectTranslationY) {
@@ -305,6 +305,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 +545,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);
         }
     }
 
@@ -548,7 +556,7 @@
      * TaskView
      */
     public float getCurrentCornerRadius() {
-        float visibleRadius = mCurrentFullscreenParams.getCurrentDrawnCornerRadius();
+        float visibleRadius = mCurrentFullscreenParams.getCurrentCornerRadius();
         mTempPoint[0] = visibleRadius;
         mTempPoint[1] = 0;
         mInversePositionMatrix.mapVectors(mTempPoint);
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..ef044f4
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/DesktopTaskContentView.kt
@@ -0,0 +1,53 @@
+/*
+ * 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
+
+class DesktopTaskContentView
+@JvmOverloads
+constructor(context: Context, attrs: AttributeSet? = null) : FrameLayout(context, attrs) {
+    var cornerRadius: Float = 0f
+        set(value) {
+            field = value
+            invalidateOutline()
+        }
+
+    private val bounds = Rect()
+
+    init {
+        clipToOutline = true
+        outlineProvider =
+            object : ViewOutlineProvider() {
+                override fun getOutline(view: View, outline: Outline) {
+                    outline.setRoundRect(bounds, cornerRadius)
+                }
+            }
+    }
+
+    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..576a56e 100644
--- a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
@@ -20,12 +20,9 @@
 import android.graphics.Point
 import android.graphics.PointF
 import android.graphics.Rect
-import android.graphics.drawable.ShapeDrawable
-import android.graphics.drawable.shapes.RoundRectShape
 import android.util.AttributeSet
 import android.util.Log
 import android.view.Gravity
-import android.view.LayoutInflater
 import android.view.View
 import androidx.core.content.res.ResourcesCompat
 import androidx.core.view.updateLayoutParams
@@ -39,52 +36,54 @@
 import com.android.launcher3.util.ViewPool
 import com.android.launcher3.util.rects.set
 import com.android.quickstep.BaseContainerInterface
+import com.android.quickstep.DesktopFullscreenDrawParams
+import com.android.quickstep.FullscreenDrawParams
 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
 
 /** TaskView that contains all tasks that are part of the desktop. */
 class DesktopTaskView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
-    TaskView(context, attrs, type = TaskViewType.DESKTOP) {
+    TaskView(
+        context,
+        attrs,
+        type = TaskViewType.DESKTOP,
+        thumbnailFullscreenParams = DesktopFullscreenDrawParams(context),
+    ) {
+    private val contentViewFullscreenParams = FullscreenDrawParams(context)
 
-    private val snapshotDrawParams =
-        object : FullscreenDrawParams(context) {
-            // DesktopTaskView thumbnail's corner radius is independent of fullscreenProgress.
-            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: DesktopTaskContentView
+    private lateinit var backgroundView: View
 
     override fun onFinishInflate() {
         super.onFinishInflate()
-        backgroundView =
-            findViewById<View>(R.id.background)!!.apply {
-                updateLayoutParams<LayoutParams> {
-                    topMargin = container.deviceProfile.overviewTaskThumbnailTopMarginPx
-                }
-                background =
-                    ShapeDrawable(RoundRectShape(FloatArray(8) { taskCornerRadius }, null, null))
-                        .apply {
-                            setTint(
-                                resources.getColor(
-                                    android.R.color.system_neutral2_300,
-                                    context.theme
-                                )
-                            )
-                        }
-            }
         iconView =
             getOrInflateIconView(R.id.icon).apply {
                 setIcon(
@@ -92,19 +91,29 @@
                     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<DesktopTaskContentView>(R.id.desktop_content).apply {
+                updateLayoutParams<LayoutParams> {
+                    topMargin = container.deviceProfile.overviewTaskThumbnailTopMarginPx
+                }
+                cornerRadius = contentViewFullscreenParams.currentCornerRadius
+                backgroundView = findViewById(R.id.background)
+                backgroundView.setBackgroundColor(
+                    resources.getColor(android.R.color.system_neutral2_300, context.theme)
+                )
+            }
     }
 
     /** 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()
@@ -113,21 +122,17 @@
             Log.d(TAG, sb.toString())
         }
         cancelPendingLoadTasks()
+        val backgroundViewIndex = contentView.indexOfChild(backgroundView)
         taskContainers =
             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, backgroundViewIndex + 1)
 
-                addView(
-                    snapshotView,
-                    // Add snapshotView to the front after initial views e.g. icon and
-                    // background.
-                    childCountAtInflation
-                )
                 TaskContainer(
                     this,
                     task,
@@ -137,20 +142,21 @@
                     SplitConfigurationOptions.STAGE_POSITION_UNDEFINED,
                     digitalWellBeingToast = null,
                     showWindowsView = null,
-                    taskOverlayFactory
+                    taskOverlayFactory,
                 )
             }
-        taskContainers.forEach { it.bind() }
-        setOrientationState(orientedState)
+        onBind(orientedState)
     }
 
     override fun onRecycle() {
         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 +165,12 @@
     override fun updateTaskSize(
         lastComputedTaskSize: Rect,
         lastComputedGridTaskSize: Rect,
-        lastComputedCarouselTaskSize: Rect
+        lastComputedCarouselTaskSize: Rect,
     ) {
         super.updateTaskSize(
             lastComputedTaskSize,
             lastComputedGridTaskSize,
-            lastComputedCarouselTaskSize
+            lastComputedCarouselTaskSize,
         )
         if (taskContainers.isEmpty()) {
             return
@@ -186,7 +192,7 @@
             Log.d(
                 TAG,
                 "onMeasure: container=[$containerWidth,$containerHeight]" +
-                    "window=[$windowWidth,$windowHeight] scale=[$scaleWidth,$scaleHeight]"
+                    "window=[$windowWidth,$windowHeight] scale=[$scaleWidth,$scaleHeight]",
             )
         }
 
@@ -209,24 +215,26 @@
                 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]",
                     )
                 }
             }
         }
     }
 
-    override fun needsUpdate(dataChange: Int, flag: Int) =
-        if (flag == FLAG_UPDATE_CORNER_RADIUS) false else super.needsUpdate(dataChange, flag)
+    override fun onTaskListVisibilityChanged(visible: Boolean, changes: Int) {
+        super.onTaskListVisibilityChanged(visible, changes)
+        if (needsUpdate(changes, FLAG_UPDATE_CORNER_RADIUS)) {
+            contentViewFullscreenParams.updateCornerRadius(context)
+        }
+    }
 
     override fun onIconLoaded(taskContainer: TaskContainer) {
         // Update contentDescription of snapshotView only, individual task icon is unused.
@@ -241,9 +249,9 @@
 
     override fun getThumbnailBounds(bounds: Rect, relativeToDragLayer: Boolean) {
         if (relativeToDragLayer) {
-            container.dragLayer.getDescendantRectRelativeToSelf(backgroundView, bounds)
+            container.dragLayer.getDescendantRectRelativeToSelf(contentView, bounds)
         } else {
-            bounds.set(backgroundView)
+            bounds.set(contentView)
         }
     }
 
@@ -252,7 +260,7 @@
         TestLogging.recordEvent(
             TestProtocol.SEQUENCE_MAIN,
             "launchDesktopFromRecents",
-            taskIds.contentToString()
+            taskIds.contentToString(),
         )
         val endCallback = RunnableList()
         val desktopController = recentsView.desktopRecentsController
@@ -262,7 +270,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 +282,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) {}
@@ -287,17 +297,21 @@
         backgroundView.alpha = 1 - fullscreenProgress
     }
 
-    override fun updateCurrentFullscreenParams() {
-        super.updateCurrentFullscreenParams()
-        updateFullscreenParams(snapshotDrawParams)
+    override fun updateFullscreenParams() {
+        super.updateFullscreenParams()
+        updateFullscreenParams(contentViewFullscreenParams)
+        contentView.cornerRadius = contentViewFullscreenParams.currentCornerRadius
     }
 
-    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/GroupedTaskView.kt b/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
index 92c1e93..0d9583d 100644
--- a/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
@@ -104,7 +104,7 @@
                     R.id.show_windows,
                     R.id.digital_wellbeing_toast,
                     STAGE_POSITION_TOP_OR_LEFT,
-                    taskOverlayFactory
+                    taskOverlayFactory,
                 ),
                 createTaskContainer(
                     secondaryTask,
@@ -113,14 +113,12 @@
                     R.id.show_windows_right,
                     R.id.bottomRight_digital_wellbeing_toast,
                     STAGE_POSITION_BOTTOM_OR_RIGHT,
-                    taskOverlayFactory
-                )
+                    taskOverlayFactory,
+                ),
             )
-        taskContainers.forEach { it.bind() }
-
         this.splitBoundsConfig = splitBoundsConfig
         taskContainers.forEach { it.digitalWellBeingToast?.splitBounds = splitBoundsConfig }
-        setOrientationState(orientedState)
+        onBind(orientedState)
     }
 
     override fun setOrientationState(orientationState: RecentsOrientedState) {
@@ -131,7 +129,7 @@
                         container.deviceProfile,
                         it,
                         layoutParams.width,
-                        layoutParams.height
+                        layoutParams.height,
                     )
                 val iconViewMarginStart =
                     resources.getDimensionPixelSize(
@@ -168,7 +166,7 @@
                     container.deviceProfile,
                     splitBoundsConfig,
                     layoutParams.width,
-                    layoutParams.height
+                    layoutParams.height,
                 )
             pagedOrientationHandler.setSplitIconParams(
                 taskContainers[0].iconView.asView(),
@@ -181,7 +179,7 @@
                 isRtl,
                 container.deviceProfile,
                 splitBoundsConfig,
-                inSplitSelection
+                inSplitSelection,
             )
         } else {
             pagedOrientationHandler.setSplitIconParams(
@@ -195,7 +193,7 @@
                 isRtl,
                 container.deviceProfile,
                 splitBoundsConfig,
-                inSplitSelection
+                inSplitSelection,
             )
         }
     }
@@ -216,7 +214,7 @@
         InteractionJankMonitorWrapper.begin(
             this,
             Cuj.CUJ_SPLIT_SCREEN_ENTER,
-            "Enter form GroupedTaskView"
+            "Enter form GroupedTaskView",
         )
         launchTaskInternal(isQuickSwitch = false, launchingExistingTaskView = true) {
             endCallback.executeAllAndDestroy()
@@ -230,7 +228,7 @@
 
     override fun launchWithoutAnimation(
         isQuickSwitch: Boolean,
-        callback: (launched: Boolean) -> Unit
+        callback: (launched: Boolean) -> Unit,
     ) {
         launchTaskInternal(isQuickSwitch, launchingExistingTaskView = false, callback)
     }
@@ -244,7 +242,7 @@
     private fun launchTaskInternal(
         isQuickSwitch: Boolean,
         launchingExistingTaskView: Boolean,
-        callback: (launched: Boolean) -> Unit
+        callback: (launched: Boolean) -> Unit,
     ) {
         recentsView?.let {
             it.splitSelectController.launchExistingSplitPair(
@@ -254,11 +252,11 @@
                 STAGE_POSITION_TOP_OR_LEFT,
                 callback,
                 isQuickSwitch,
-                snapPosition
+                snapPosition,
             )
             Log.d(
                 TAG,
-                "launchTaskInternal - launchExistingSplitPair: ${taskIds.contentToString()}, launchingExistingTaskView: $launchingExistingTaskView"
+                "launchTaskInternal - launchExistingSplitPair: ${taskIds.contentToString()}, launchingExistingTaskView: $launchingExistingTaskView",
             )
         }
     }
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..743fa40 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;
                 }
             };
 
@@ -424,9 +417,6 @@
                 public void setValue(RecentsView view, float scale) {
                     view.setScaleX(scale);
                     view.setScaleY(scale);
-                    if (enableRefactorTaskThumbnail()) {
-                        view.mRecentsViewModel.updateScale(scale);
-                    }
                     view.mLastComputedTaskStartPushOutDistance = null;
                     view.mLastComputedTaskEndPushOutDistance = null;
                     view.runActionOnRemoteHandles(new Consumer<RemoteTargetHandle>() {
@@ -464,6 +454,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 +580,7 @@
     private int mClampedScrollOffsetBound;
 
     private float mAdjacentPageHorizontalOffset = 0;
+    private float mDesktopCarouselDetachProgress = 0;
     protected float mTaskViewsSecondaryTranslation = 0;
     protected float mTaskViewsPrimarySplitTranslation = 0;
     protected float mTaskViewsSecondarySplitTranslation = 0;
@@ -601,6 +607,8 @@
     private int mKeyboardTaskFocusSnapAnimationDuration;
     private int mKeyboardTaskFocusIndex = INVALID_PAGE;
 
+    private int[] mDismissPrimaryTranslations;
+
     /**
      * TODO: Call reloadIdNeeded in onTaskStackChanged.
      */
@@ -681,13 +689,11 @@
     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 +805,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
@@ -844,6 +844,8 @@
     private final RecentsViewModelHelper mHelper;
     private final RecentsViewUtils mUtils = new RecentsViewUtils();
 
+    private final Matrix mTmpMatrix = new Matrix();
+
     public RecentsView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
             BaseContainerInterface sizeStrategy) {
         super(context, attrs, defStyleAttr);
@@ -1178,7 +1180,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 +1211,7 @@
     @Override
     protected void onDetachedFromWindow() {
         super.onDetachedFromWindow();
+
         updateTaskStackListenerState();
         mModel.getThumbnailCache().getHighResLoadingState().removeCallback(this);
         mContainer.removeMultiWindowModeChangedListener(mMultiWindowModeChangedListener);
@@ -1241,21 +1244,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);
@@ -1409,7 +1415,7 @@
         if (showAsGrid()) {
             int screenStart = getPagedOrientationHandler().getPrimaryScroll(this);
             int screenEnd = screenStart + getPagedOrientationHandler().getMeasuredSize(this);
-            return isTaskViewWithinBounds(tv, screenStart, screenEnd);
+            return isTaskViewWithinBounds(tv, screenStart, screenEnd, /*taskViewTranslation=*/ 0);
         } else {
             // For now, just check if it's the active task or an adjacent task
             return Math.abs(indexOfChild(tv) - getNextPage()) <= 1;
@@ -1456,14 +1462,28 @@
         return clearAllScroll + (mIsRtl ? distance : -distance);
     }
 
-    private boolean isTaskViewWithinBounds(TaskView tv, int start, int end) {
-        int taskStart = getPagedOrientationHandler().getChildStart(tv)
-                + (int) tv.getOffsetAdjustment(showAsGrid());
-        int taskSize = (int) (getPagedOrientationHandler().getMeasuredSize(tv)
-                * tv.getSizeAdjustment(showAsFullscreen()));
+    /*
+     * Returns if TaskView is within screen bounds defined in [screenStart, screenEnd].
+     *
+     * @param taskViewTranslation taskView is considered within bounds if either translated or
+     * original position of taskView is within screen bounds.
+     */
+    private boolean isTaskViewWithinBounds(TaskView taskView, int screenStart, int screenEnd,
+            int taskViewTranslation) {
+        int taskStart = getPagedOrientationHandler().getChildStart(taskView)
+                + (int) taskView.getOffsetAdjustment(showAsGrid());
+        int taskSize = (int) (getPagedOrientationHandler().getMeasuredSize(taskView)
+                * taskView.getSizeAdjustment(showAsFullscreen()));
         int taskEnd = taskStart + taskSize;
-        return (taskStart >= start && taskStart <= end) || (taskEnd >= start
-                && taskEnd <= end);
+
+        int translatedTaskStart = taskStart + taskViewTranslation;
+        int translatedTaskEnd = taskEnd + taskViewTranslation;
+
+        taskStart = Math.min(taskStart, translatedTaskStart);
+        taskEnd = Math.max(taskEnd, translatedTaskEnd);
+
+        return (taskStart >= screenStart && taskStart <= screenEnd) || (taskEnd >= screenStart
+                && taskEnd <= screenEnd);
     }
 
     private boolean isTaskViewFullyWithinBounds(TaskView tv, int start, int end) {
@@ -1541,9 +1561,6 @@
         updateTaskStackListenerState();
         mOrientationState.setRotationWatcherEnabled(enabled);
         if (!enabled) {
-            // Reset the running task when leaving overview since it can still have a reference to
-            // its thumbnail
-            mTmpRunningTasks = null;
             mSplitBoundsConfig = null;
             mTaskOverlayFactory.clearAllActiveState();
         }
@@ -1586,8 +1603,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 +1720,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;
                 }
@@ -1751,26 +1768,17 @@
     }
 
     /**
-     * Moves the running task to the front of the carousel in tablets, to minimize animation
-     * required to move the running task in grid.
+     * Moves the running task to the expected position in the carousel. In tablets, this minimize
+     * animation required to move the running task into focused task position.
      */
-    public void moveRunningTaskToFront() {
-        if (!mContainer.getDeviceProfile().isTablet) {
-            return;
-        }
-
+    public void moveRunningTaskToExpectedPosition() {
         TaskView runningTaskView = getRunningTaskView();
-        if (runningTaskView == null) {
+        if (runningTaskView == null || mCurrentPage != indexOfChild(runningTaskView)) {
             return;
         }
 
-        if (indexOfChild(runningTaskView) != mCurrentPage) {
-            return;
-        }
-
-        int frontIndex = enableLargeDesktopWindowingTile() ? getDesktopTaskViewCount() : 0;
-
-        if (mCurrentPage <= frontIndex) {
+        int runningTaskExpectedIndex = getRunningTaskExpectedIndex(runningTaskView);
+        if (mCurrentPage == runningTaskExpectedIndex) {
             return;
         }
 
@@ -1783,16 +1791,34 @@
         mMovingTaskView = null;
         runningTaskView.resetPersistentViewTransforms();
 
-        addView(runningTaskView, frontIndex);
-        setCurrentPage(frontIndex);
+        addView(runningTaskView, runningTaskExpectedIndex);
+        setCurrentPage(runningTaskExpectedIndex);
 
         updateTaskSize();
     }
 
+    private int getRunningTaskExpectedIndex(TaskView runningTaskView) {
+        if (mContainer.getDeviceProfile().isTablet) {
+            if (runningTaskView instanceof DesktopTaskView) {
+                return 0; // Desktop running task is always in front.
+            } else if (enableLargeDesktopWindowingTile()) {
+                return getDesktopTaskViewCount(); // Other running task is behind desktop tasks.
+            } else {
+                return 0;
+            }
+        } else {
+            int currentIndex = indexOfChild(runningTaskView);
+            if (currentIndex != -1) {
+                return currentIndex; // Keep the position if running task already in layout.
+            } else {
+                return 0; // New running task are added to the front to begin with.
+            }
+        }
+    }
+
     @Override
     protected void onScrollerAnimationAborted() {
-        ActiveGestureLog.INSTANCE.addLog("scroller animation aborted",
-                ActiveGestureErrorDetector.GestureEvent.SCROLLER_ANIMATION_ABORTED);
+        ActiveGestureProtoLogProxy.logOnScrollerAnimationAborted();
     }
 
     @Override
@@ -1872,7 +1898,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 +1908,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 +1939,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 +1958,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 +1986,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);
                 }
@@ -2458,7 +2488,8 @@
             }
             boolean visible;
             if (showAsGrid()) {
-                visible = isTaskViewWithinBounds(taskView, visibleStart, visibleEnd);
+                visible = isTaskViewWithinBounds(taskView, visibleStart, visibleEnd,
+                        mDismissPrimaryTranslations != null ? mDismissPrimaryTranslations[i] : 0);
             } else {
                 visible = lower <= i && i <= upper;
             }
@@ -2467,13 +2498,6 @@
                 List<Task> tasksToUpdate = containers.stream()
                         .map(TaskContainer::getTask)
                         .collect(Collectors.toCollection(ArrayList::new));
-                if (mTmpRunningTasks != null) {
-                    for (Task t : mTmpRunningTasks) {
-                        // Skip loading if this is the task that we are animating into
-                        // TODO(b/280812109) change this equality check to use A.equals(B)
-                        tasksToUpdate.removeIf(task -> task == t);
-                    }
-                }
                 if (enableRefactorTaskThumbnail()) {
                     visibleTaskIds.addAll(
                             tasksToUpdate.stream().map((task) -> task.key.id).toList());
@@ -2481,6 +2505,7 @@
                 if (tasksToUpdate.isEmpty()) {
                     continue;
                 }
+                int visibilityChanges = 0;
                 for (Task task : tasksToUpdate) {
                     if (!mHasVisibleTaskData.get(task.key.id)) {
                         // Ignore thumbnail update if it's current running task during the gesture
@@ -2489,21 +2514,28 @@
                         if (taskView == getRunningTaskView() && isGestureActive()) {
                             changes &= ~TaskView.FLAG_UPDATE_THUMBNAIL;
                         }
-                        taskView.onTaskListVisibilityChanged(true /* visible */, changes);
+                        visibilityChanges |= changes;
                     }
                     mHasVisibleTaskData.put(task.key.id, true);
                 }
+                if (visibilityChanges != 0) {
+                    taskView.onTaskListVisibilityChanged(true /* visible */, visibilityChanges);
+                }
             } else {
+                int visibilityChanges = 0;
                 for (TaskContainer container : containers) {
                     if (container == null) {
                         continue;
                     }
 
                     if (mHasVisibleTaskData.get(container.getTask().key.id)) {
-                        taskView.onTaskListVisibilityChanged(false /* visible */, dataChanges);
+                        visibilityChanges = dataChanges;
                     }
                     mHasVisibleTaskData.delete(container.getTask().key.id);
                 }
+                if (visibilityChanges != 0) {
+                    taskView.onTaskListVisibilityChanged(false /* visible */, visibilityChanges);
+                }
             }
         }
         if (enableRefactorTaskThumbnail()) {
@@ -2739,13 +2771,10 @@
             updateSizeAndPadding();
         }
 
-        showCurrentTask(mActiveGestureRunningTasks);
+        showCurrentTask(mActiveGestureRunningTasks, "onGestureAnimationStart");
         setEnableFreeScroll(false);
         setEnableDrawingLiveTile(false);
         setRunningTaskHidden(true);
-        if (enableLargeDesktopWindowingTile()) {
-            setNonRunningTaskCategoryHidden(true);
-        }
         setTaskIconScaledDown(true);
     }
 
@@ -2869,6 +2898,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 +2921,6 @@
         setEnableDrawingLiveTile(mCurrentGestureEndTarget == GestureState.GestureEndTarget.RECENTS);
         Log.d(TAG, "onGestureAnimationEnd - mEnableDrawingLiveTile: " + mEnableDrawingLiveTile);
         setRunningTaskHidden(false);
-        if (enableLargeDesktopWindowingTile()) {
-            setNonRunningTaskCategoryHidden(false);
-        }
         animateUpTaskIconScale();
         animateActionsViewIn();
 
@@ -2918,8 +2952,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;
         }
@@ -2933,25 +2968,20 @@
             final TaskView taskView;
             if (needDesktopTask) {
                 taskView = getTaskViewFromPool(TaskViewType.DESKTOP);
-                mTmpRunningTasks = Arrays.copyOf(runningTasks, runningTasks.length);
-                ((DesktopTaskView) taskView).bind(Arrays.asList(mTmpRunningTasks),
+                ((DesktopTaskView) taskView).bind(Arrays.asList(runningTasks),
                         mOrientationState, mTaskOverlayFactory);
             } else if (needGroupTaskView) {
                 taskView = getTaskViewFromPool(TaskViewType.GROUPED);
-                mTmpRunningTasks = new Task[]{runningTasks[0], runningTasks[1]};
                 // When we create a placeholder task view mSplitBoundsConfig will be null, but with
                 // the actual app running we won't need to show the thumbnail until all the tasks
                 // load later anyways
-                ((GroupedTaskView) taskView).bind(mTmpRunningTasks[0], mTmpRunningTasks[1],
+                ((GroupedTaskView) taskView).bind(runningTasks[0], runningTasks[1],
                         mOrientationState, mTaskOverlayFactory, mSplitBoundsConfig);
             } else {
                 taskView = getTaskViewFromPool(TaskViewType.SINGLE);
-                // The temporary running task is only used for the duration between the start of the
-                // gesture and the task list is loaded and applied
-                mTmpRunningTasks = new Task[]{runningTasks[0]};
-                taskView.bind(mTmpRunningTasks[0], mOrientationState, mTaskOverlayFactory);
+                taskView.bind(runningTasks[0], mOrientationState, mTaskOverlayFactory);
             }
-            addView(taskView, 0);
+            addView(taskView, getRunningTaskExpectedIndex(taskView));
             runningTaskViewId = taskView.getTaskViewId();
             if (wasEmpty) {
                 addView(mClearAllButton);
@@ -2969,10 +2999,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 +3077,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 +3091,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 +3178,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 +3203,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 +3216,6 @@
                 taskView.setGridTranslationY((mLastComputedTaskSize.height() + taskTopMargin
                         - taskView.getLayoutParams().height) / 2f);
 
-                if (taskView.getTaskViewId() == mFocusedTaskViewId) {
-                    focusedTaskIndex = i;
-                }
                 largeTasksIndices.add(i);
                 largeTaskWidthAndSpacing = taskWidthAndSpacing;
 
@@ -3186,8 +3224,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 +3625,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 +3641,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 +3652,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 +3693,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 +3712,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 +3760,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 +3841,42 @@
         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;
+        }
+        mDismissPrimaryTranslations = new int[taskCount];
         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, i);
                     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 +3903,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 +3921,29 @@
                                     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) {
+                    float finalTranslation = mIsRtl ? primaryTranslation : -primaryTranslation;
                     anim.setFloat(taskView, taskView.getPrimaryDismissTranslationProperty(),
-                            mIsRtl ? primaryTranslation : -primaryTranslation,
+                            finalTranslation,
                             clampToProgress(dismissInterpolator, animationStartProgress,
                                     animationEndProgress));
+                    mDismissPrimaryTranslations[i] = (int) finalTranslation;
+                    distanceFromDismissedTask++;
                 }
             }
         }
+        if (dismissingForSplitSelection) {
+            createInitialSplitSelectAnimation(anim);
+        }
 
         if (needsCurveUpdates) {
             anim.addOnFrameCallback(this::updateCurveProperties);
@@ -3962,21 +3952,22 @@
         // 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);
         }
-
+        loadVisibleTaskData(TaskView.FLAG_UPDATE_ALL);
         mPendingAnimation = anim;
         final TaskView finalNextFocusedTaskView = nextFocusedTaskView;
         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 +3981,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 +4137,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.
@@ -4174,11 +4173,100 @@
                 updateCurrentTaskActionsVisibility();
                 onDismissAnimationEnds();
                 mPendingAnimation = null;
+                mDismissPrimaryTranslations = null;
             }
         });
     }
 
     /**
+     * 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,
+            int index) {
+        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 (view instanceof TaskView) {
+            mDismissPrimaryTranslations[index] = scrollDiffPerPage;
+        }
+        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 +4359,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 +4634,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 +4782,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 +4805,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 +4823,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 +4836,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 +4917,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 +4984,7 @@
             }
             distanceToOffscreen -= mLastComputedTaskEndPushOutDistance;
         }
-        return distanceToOffscreen * offsetProgress;
+        return distanceToOffscreen;
     }
 
     /**
@@ -4892,7 +5032,7 @@
 
     private void updateTaskViewsSnapshotRadius() {
         for (TaskView taskView : getTaskViews()) {
-            taskView.updateSnapshotRadius();
+            taskView.updateFullscreenParams();
         }
     }
 
@@ -4942,7 +5082,6 @@
         mSplitSelectStateController.setAnimateCurrentTaskDismissal(
                 true /*animateCurrentTaskDismissal*/);
         mSplitHiddenTaskViewIndex = indexOfChild(taskView);
-        updateDesktopTaskVisibility(false /* visible */);
     }
 
     /**
@@ -4956,7 +5095,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 +5105,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);
+                }
+            }
         }
     }
 
@@ -5001,7 +5165,7 @@
                 if (!enableRefactorTaskThumbnail()) {
                     taskContainer.getThumbnailViewDeprecated().refreshSplashView();
                 }
-                mSplitHiddenTaskView.updateSnapshotRadius();
+                mSplitHiddenTaskView.updateFullscreenParams();
             });
         } else if (isInitiatingSplitFromTaskView) {
             if (Flags.enableHoverOfChildElementsInTaskview()) {
@@ -5012,7 +5176,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 +5350,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 +5516,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 +5644,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 +5682,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;
     }
@@ -5504,43 +5699,6 @@
         updateCurrentTaskActionsVisibility();
         loadVisibleTaskData(TaskView.FLAG_UPDATE_ALL);
         updateEnabledOverlays();
-
-        if (enableRefactorTaskThumbnail()) {
-            int screenStart = 0;
-            int screenEnd = 0;
-            int centerPageIndex = 0;
-            if (showAsGrid()) {
-                screenStart = getPagedOrientationHandler().getPrimaryScroll(this);
-                int pageOrientedSize = getPagedOrientationHandler().getMeasuredSize(this);
-                screenEnd = screenStart + pageOrientedSize;
-            } else {
-                centerPageIndex = getPageNearestToCenterOfScreen();
-            }
-
-            Set<Integer> fullyVisibleTaskIds = new HashSet<>();
-
-            // Update the task data for the in/visible children
-            for (int i = 0; i < getTaskViewCount(); i++) {
-                TaskView taskView = requireTaskViewAt(i);
-                List<TaskContainer> containers = taskView.getTaskContainers();
-                if (containers.isEmpty()) {
-                    continue;
-                }
-                boolean isFullyVisible;
-                if (showAsGrid()) {
-                    isFullyVisible = isTaskViewFullyWithinBounds(taskView, screenStart,
-                            screenEnd);
-                } else {
-                    isFullyVisible = i == centerPageIndex;
-                }
-                if (isFullyVisible) {
-                    List<Integer> taskIds = containers.stream().map(
-                            taskContainer -> taskContainer.getTask().key.id).toList();
-                    fullyVisibleTaskIds.addAll(taskIds);
-                }
-            }
-            mRecentsViewModel.updateTasksFullyVisible(fullyVisibleTaskIds);
-        }
     }
 
     @Override
@@ -5638,6 +5796,14 @@
         // mSyncTransactionApplier doesn't get transferred over
         runActionOnRemoteHandles(remoteTargetHandle -> {
             final TransformParams params = remoteTargetHandle.getTransformParams();
+            if (Flags.enableFallbackOverviewInWindow() || Flags.enableLauncherOverviewInWindow()) {
+                params.setHomeBuilderProxy((builder, app, transformParams) -> {
+                    mTmpMatrix.setScale(
+                            1f, 1f, app.localBounds.exactCenterX(), app.localBounds.exactCenterY());
+                    builder.setMatrix(mTmpMatrix).setAlpha(1f).setShow();
+                });
+            }
+
             if (mSyncTransactionApplier != null) {
                 params.setSyncTransactionApplier(mSyncTransactionApplier);
                 params.getTargetSet().addReleaseCheck(mSyncTransactionApplier);
@@ -5806,16 +5972,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 +6004,8 @@
                 lastView = mUtils.getLastLargeTaskView(getTaskViews());
             }
         } else {
-            lastView = mUtils.getLastTaskViewInCarousel(mNonRunningTaskCategoryHidden,
+            lastView = mUtils.getLastTaskViewInCarousel(
+                    /*nonRunningTaskCarouselHidden=*/mDesktopCarouselDetachProgress > 0,
                     getTaskViews(), getRunningTaskView());
         }
         return indexOfChild(lastView);
@@ -6088,17 +6257,27 @@
     }
 
     private void updateEnabledOverlays() {
-        TaskView focusedTaskView = getFocusedTaskView();
-        for (TaskView taskView : getTaskViews()) {
-            if (taskView == focusedTaskView) {
-                continue;
+        if (enableRefactorTaskThumbnail()) {
+            Set<Integer> fullyVisibleTaskIds = new HashSet<>();
+            for (TaskView taskView : getTaskViews()) {
+                if (isTaskViewFullyVisible(taskView)) {
+                    fullyVisibleTaskIds.addAll(taskView.getTaskIdSet());
+                }
             }
-            taskView.setOverlayEnabled(mOverlayEnabled && isTaskViewFullyVisible(taskView));
-        }
-        // Focus task overlay should be enabled and refreshed at last
-        if (focusedTaskView != null) {
-            focusedTaskView.setOverlayEnabled(
-                    mOverlayEnabled && isTaskViewFullyVisible(focusedTaskView));
+            mRecentsViewModel.updateTasksFullyVisible(fullyVisibleTaskIds);
+        } else {
+            TaskView focusedTaskView = getFocusedTaskView();
+            for (TaskView taskView : getTaskViews()) {
+                if (taskView == focusedTaskView) {
+                    continue;
+                }
+                taskView.setOverlayEnabled(mOverlayEnabled && isTaskViewFullyVisible(taskView));
+            }
+            // Focus task overlay should be enabled and refreshed at last
+            if (focusedTaskView != null) {
+                focusedTaskView.setOverlayEnabled(
+                        mOverlayEnabled && isTaskViewFullyVisible(focusedTaskView));
+            }
         }
     }
 
@@ -6552,6 +6731,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..9f2bb9a 100644
--- a/quickstep/src/com/android/quickstep/views/TaskThumbnailViewDeprecated.java
+++ b/quickstep/src/com/android/quickstep/views/TaskThumbnailViewDeprecated.java
@@ -50,9 +50,9 @@
 import com.android.launcher3.util.SystemUiController;
 import com.android.launcher3.util.SystemUiController.SystemUiControllerFlags;
 import com.android.launcher3.util.ViewPool;
+import com.android.quickstep.FullscreenDrawParams;
 import com.android.quickstep.TaskOverlayFactory.TaskOverlay;
 import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
-import com.android.quickstep.views.TaskView.FullscreenDrawParams;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.recents.utilities.PreviewPositionHelper;
@@ -107,9 +107,10 @@
     // Contains the portion of the thumbnail that is clipped when fullscreen progress = 0.
     private final Rect mPreviewRect = new Rect();
     private final PreviewPositionHelper mPreviewPositionHelper = new PreviewPositionHelper();
-    private TaskView.FullscreenDrawParams mFullscreenParams;
+    private 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);
@@ -277,7 +279,7 @@
         canvas.save();
         // Draw the insets if we're being drawn fullscreen (we do this for quick switch).
         drawOnCanvas(canvas, 0, 0, getMeasuredWidth(), getMeasuredHeight(),
-                mFullscreenParams.getCurrentDrawnCornerRadius());
+                mFullscreenParams.getCurrentCornerRadius());
         canvas.restore();
     }
 
@@ -285,15 +287,15 @@
         return mPreviewPositionHelper;
     }
 
-    public void setFullscreenParams(TaskView.FullscreenDrawParams fullscreenParams) {
+    public void setFullscreenParams(FullscreenDrawParams fullscreenParams) {
         mFullscreenParams = fullscreenParams;
         invalidate();
     }
 
     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.updateFullscreenParams();
         invalidate();
     }
 
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index 2ed6ae6..7e489ea 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -59,13 +59,11 @@
 import com.android.launcher3.testing.TestLogging
 import com.android.launcher3.testing.shared.TestProtocol
 import com.android.launcher3.util.CancellableTask
-import com.android.launcher3.util.DisplayController
 import com.android.launcher3.util.Executors
 import com.android.launcher3.util.MultiPropertyFactory
 import com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE
 import com.android.launcher3.util.MultiValueAlpha
 import com.android.launcher3.util.RunnableList
-import com.android.launcher3.util.SafeCloseable
 import com.android.launcher3.util.SplitConfigurationOptions
 import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED
 import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption
@@ -74,15 +72,13 @@
 import com.android.launcher3.util.TransformingTouchDelegate
 import com.android.launcher3.util.ViewPool
 import com.android.launcher3.util.rects.set
-import com.android.launcher3.views.ActivityContext
+import com.android.quickstep.FullscreenDrawParams
 import com.android.quickstep.RecentsModel
 import com.android.quickstep.RemoteAnimationTargets
 import com.android.quickstep.TaskOverlayFactory
 import com.android.quickstep.TaskViewUtils
 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.viewmodel.TaskViewModel
+import com.android.quickstep.task.thumbnail.TaskThumbnailView
 import com.android.quickstep.util.ActiveGestureErrorDetector
 import com.android.quickstep.util.ActiveGestureLog
 import com.android.quickstep.util.BorderAnimator
@@ -94,7 +90,6 @@
 import com.android.systemui.shared.recents.model.Task
 import com.android.systemui.shared.recents.model.ThumbnailData
 import com.android.systemui.shared.system.ActivityManagerWrapper
-import com.android.systemui.shared.system.QuickStepContract
 
 /** A task in the Recents view. */
 open class TaskView
@@ -107,6 +102,7 @@
     focusBorderAnimator: BorderAnimator? = null,
     hoverBorderAnimator: BorderAnimator? = null,
     private val type: TaskViewType = TaskViewType.SINGLE,
+    protected val thumbnailFullscreenParams: FullscreenDrawParams = FullscreenDrawParams(context),
 ) : FrameLayout(context, attrs), ViewPool.Reusable {
     /**
      * Used in conjunction with [onTaskListVisibilityChanged], providing more granularity on which
@@ -116,8 +112,6 @@
     @IntDef(FLAG_UPDATE_ALL, FLAG_UPDATE_ICON, FLAG_UPDATE_THUMBNAIL, FLAG_UPDATE_CORNER_RADIUS)
     annotation class TaskDataChanges
 
-    private lateinit var taskViewModel: TaskViewModel
-
     val taskIds: IntArray
         /** Returns a copy of integer array containing taskIds of all tasks in the TaskView. */
         get() = taskContainers.map { it.task.key.id }.toIntArray()
@@ -141,9 +135,6 @@
             this == recentsView?.focusedTaskView ||
                 (enableLargeDesktopWindowingTile() && type == TaskViewType.DESKTOP)
 
-    val taskCornerRadius: Float
-        get() = currentFullscreenParams.cornerRadius
-
     val recentsView: RecentsView<*, *>?
         get() = parent as? RecentsView<*, *>
 
@@ -156,15 +147,9 @@
         get() = taskContainers[0].task
 
     @get:Deprecated("Use [taskContainers] instead.")
-    val firstSnapshotView: View
-        /** Returns the first snapshotView of the TaskView. */
-        get() = taskContainers[0].snapshotView
-
-    @get:Deprecated("Use [taskContainers] instead.")
     val firstItemInfo: ItemInfo
         get() = taskContainers[0].itemInfo
 
-    private val currentFullscreenParams = FullscreenDrawParams(context)
     protected val container: RecentsViewContainer =
         RecentsViewContainer.containerFromContext(context)
     protected val lastTouchDownPosition = PointF()
@@ -402,6 +387,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
@@ -479,17 +473,13 @@
     init {
         setOnClickListener { _ -> onClick() }
 
-        if (enableRefactorTaskThumbnail()) {
-            taskViewModel = RecentsDependencies.get(this, "TaskViewType" to type)
-        }
-
         val cursorHoverStatesEnabled = enableCursorHoverStates()
         setWillNotDraw(!cursorHoverStatesEnabled)
         context.obtainStyledAttributes(attrs, R.styleable.TaskView, defStyleAttr, defStyleRes).use {
             this.focusBorderAnimator =
                 focusBorderAnimator
                     ?: createSimpleBorderAnimator(
-                        currentFullscreenParams.cornerRadius.toInt(),
+                        TaskCornerRadius.get(context).toInt(),
                         context.resources.getDimensionPixelSize(
                             R.dimen.keyboard_quick_switch_border_width
                         ),
@@ -504,7 +494,7 @@
                 hoverBorderAnimator
                     ?: if (cursorHoverStatesEnabled)
                         createSimpleBorderAnimator(
-                            currentFullscreenParams.cornerRadius.toInt(),
+                            TaskCornerRadius.get(context).toInt(),
                             context.resources.getDimensionPixelSize(
                                 R.dimen.task_hover_border_width
                             ),
@@ -606,6 +596,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 +616,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 +683,6 @@
         orientedState: RecentsOrientedState,
         taskOverlayFactory: TaskOverlayFactory,
     ) {
-
         cancelPendingLoadTasks()
         taskContainers =
             listOf(
@@ -701,7 +696,16 @@
                     taskOverlayFactory,
                 )
             )
-        taskContainers.forEach { it.bind() }
+        onBind(orientedState)
+    }
+
+    open fun onBind(orientedState: RecentsOrientedState) {
+        taskContainers.forEach {
+            it.bind()
+            if (enableRefactorTaskThumbnail()) {
+                it.thumbnailView.cornerRadius = thumbnailFullscreenParams.currentCornerRadius
+            }
+        }
         setOrientationState(orientedState)
     }
 
@@ -714,19 +718,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 +742,7 @@
             iconView,
             TransformingTouchDelegate(iconView.asView()),
             stagePosition,
-            digitalWellBeingToast,
+            findViewById(digitalWellbeingBannerId)!!,
             findViewById(showWindowViewId)!!,
             taskOverlayFactory,
         )
@@ -916,7 +924,7 @@
             }
         }
         if (needsUpdate(changes, FLAG_UPDATE_CORNER_RADIUS)) {
-            currentFullscreenParams.updateCornerRadius(context)
+            thumbnailFullscreenParams.updateCornerRadius(context)
         }
     }
 
@@ -1202,8 +1210,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 {
@@ -1497,10 +1503,7 @@
         val scale = persistentScale * dismissScale
         scaleX = scale
         scaleY = scale
-        if (enableRefactorTaskThumbnail()) {
-            taskViewModel.updateScale(scale)
-        }
-        updateSnapshotRadius()
+        updateFullscreenParams()
     }
 
     protected open fun applyThumbnailSplashAlpha() {
@@ -1543,30 +1546,25 @@
         }
         focusTransitionFullscreen.value =
             FOCUS_TRANSITION_FAST_OUT_INTERPOLATOR.getInterpolation(1 - fullscreenProgress)
-        updateSnapshotRadius()
+        updateFullscreenParams()
     }
 
-    protected open fun updateSnapshotRadius() {
-        updateCurrentFullscreenParams()
+    protected open fun updateFullscreenParams() {
+        updateFullscreenParams(thumbnailFullscreenParams)
         taskContainers.forEach {
-            if (!enableRefactorTaskThumbnail()) {
-                it.thumbnailViewDeprecated.setFullscreenParams(getThumbnailFullscreenParams())
+            if (enableRefactorTaskThumbnail()) {
+                it.thumbnailView.cornerRadius = thumbnailFullscreenParams.currentCornerRadius
+            } else {
+                it.thumbnailViewDeprecated.setFullscreenParams(thumbnailFullscreenParams)
             }
-            it.overlay.setFullscreenParams(getThumbnailFullscreenParams())
+            it.overlay.setFullscreenParams(thumbnailFullscreenParams)
         }
     }
 
-    protected open fun updateCurrentFullscreenParams() {
-        updateFullscreenParams(currentFullscreenParams)
-    }
-
     protected fun updateFullscreenParams(fullscreenParams: FullscreenDrawParams) {
         recentsView?.let { fullscreenParams.setProgress(fullscreenProgress, it.scaleX, scaleX) }
     }
 
-    protected open fun getThumbnailFullscreenParams(): FullscreenDrawParams =
-        currentFullscreenParams
-
     private fun onModalnessUpdated(modalness: Float) {
         taskContainers.forEach {
             it.iconView.setModalAlpha(1 - modalness)
@@ -1613,56 +1611,6 @@
     private fun getNonGridTrans(endTranslation: Float) =
         endTranslation - getGridTrans(endTranslation)
 
-    /** We update and subsequently draw these in [fullscreenProgress]. */
-    open class FullscreenDrawParams(context: Context) : SafeCloseable {
-        var cornerRadius = 0f
-        private var windowCornerRadius = 0f
-        var currentDrawnCornerRadius = 0f
-
-        init {
-            updateCornerRadius(context)
-        }
-
-        /** Recomputes the start and end corner radius for the given Context. */
-        fun updateCornerRadius(context: Context) {
-            cornerRadius = computeTaskCornerRadius(context)
-            windowCornerRadius = computeWindowCornerRadius(context)
-        }
-
-        @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
-        open fun computeTaskCornerRadius(context: Context): Float {
-            return TaskCornerRadius.get(context)
-        }
-
-        @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
-        open fun computeWindowCornerRadius(context: Context): Float {
-            val activityContext: ActivityContext? = ActivityContext.lookupContextNoThrow(context)
-
-            // The corner radius is fixed to match when Taskbar is persistent mode
-            return if (
-                activityContext != null &&
-                    activityContext.deviceProfile?.isTaskbarPresent == true &&
-                    DisplayController.isTransientTaskbar(context)
-            ) {
-                context.resources
-                    .getDimensionPixelSize(R.dimen.persistent_taskbar_corner_radius)
-                    .toFloat()
-            } else {
-                QuickStepContract.getWindowCornerRadius(context)
-            }
-        }
-
-        /** Sets the progress in range [0, 1] */
-        fun setProgress(fullscreenProgress: Float, parentScale: Float, taskViewScale: Float) {
-            currentDrawnCornerRadius =
-                Utilities.mapRange(fullscreenProgress, cornerRadius, windowCornerRadius) /
-                    parentScale /
-                    taskViewScale
-        }
-
-        override fun close() {}
-    }
-
     private fun MotionEvent.isWithinThumbnailBounds(): Boolean {
         return thumbnailBounds.contains(x.toInt(), y.toInt())
     }
@@ -1687,8 +1635,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..f25f6f4
--- /dev/null
+++ b/quickstep/src_protolog/com/android/quickstep/util/ActiveGestureProtoLogProxy.java
@@ -0,0 +1,522 @@
+/*
+ * 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.graphics.Point;
+import android.graphics.RectF;
+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 logHandOffAnimation() {
+        ActiveGestureLog.INSTANCE.addLog("AbsSwipeUpHandler.handOffAnimation");
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "AbsSwipeUpHandler.handOffAnimation");
+    }
+
+    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);
+    }
+
+    public static void logCreateTouchRegionForDisplay(int displayRotation,
+            @NonNull Point displaySize, @NonNull RectF swipeRegion, @NonNull RectF ohmRegion,
+            int gesturalHeight, int largerGesturalHeight, @NonNull String reason) {
+        if (!enableActiveGestureProtoLog()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, 
+                "OrientationTouchTransformer.createRegionForDisplay: "
+                        + "dispRot=%d, dispSize=%s, swipeRegion=%s, ohmRegion=%s, "
+                        + "gesturalHeight=%d, largerGesturalHeight=%d, reason=%s",
+                displayRotation, displaySize.flattenToString(), swipeRegion.toShortString(),
+                ohmRegion.toShortString(), gesturalHeight, largerGesturalHeight, reason);
+    }
+}
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..47d2bfc
--- /dev/null
+++ b/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/FakeTaskThumbnailViewModel.kt
@@ -0,0 +1,35 @@
+/*
+ * 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 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..49fe614
--- /dev/null
+++ b/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewScreenshotTest.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.task.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)
+                as TaskThumbnailView
+        taskThumbnailView.cornerRadius = CORNER_RADIUS
+        val ttvDiScopeId = di.getScope(taskThumbnailView).scopeId
+        di.provide(TaskThumbnailViewData::class.java, ttvDiScopeId) { TaskThumbnailViewData() }
+        di.provide(TaskThumbnailViewModel::class.java, ttvDiScopeId) { taskThumbnailViewModel }
+
+        return taskThumbnailView
+    }
+
+    companion object {
+        @Parameters(name = "{0}")
+        @JvmStatic
+        fun getTestSpecs() =
+            DeviceEmulationSpec.forDisplays(
+                Displays.Phone,
+                isDarkTheme = false,
+                isLandscape = false,
+            )
+
+        const val CORNER_RADIUS = 56f
+    }
+}
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..c682990 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java
@@ -15,9 +15,11 @@
 import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_RECENTS;
 import static com.android.launcher3.taskbar.TaskbarNavButtonController.SCREEN_PIN_LONG_PRESS_THRESHOLD;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING;
+import static com.android.window.flags.Flags.FLAG_PREDICTIVE_BACK_THREE_BUTTON_NAV;
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.junit.Assert.assertEquals;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
@@ -28,6 +30,10 @@
 import static org.mockito.Mockito.when;
 
 import android.os.Handler;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.view.KeyEvent;
 import android.view.View;
 import android.view.inputmethod.Flags;
 
@@ -39,12 +45,14 @@
 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;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
@@ -64,7 +72,7 @@
     @Mock
     Handler mockHandler;
     @Mock
-    AssistUtils mockAssistUtils;
+    ContextualSearchInvoker mockContextualSearchInvoker;
     @Mock
     StatsLogManager mockStatsLogManager;
     @Mock
@@ -76,6 +84,9 @@
     @Mock
     View mockView;
 
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
     private int mHomePressCount;
     private int mOverviewToggleCount;
     private final TaskbarNavButtonCallbacks mCallbacks = new TaskbarNavButtonCallbacks() {
@@ -109,13 +120,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 +177,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());
     }
 
@@ -333,4 +344,46 @@
         verify(mockStatsLogger, times(1)).log(LAUNCHER_TASKBAR_BACK_BUTTON_LONGPRESS);
         verify(mockStatsLogger, times(0)).log(LAUNCHER_TASKBAR_BACK_BUTTON_TAP);
     }
+
+    @Test
+    @RequiresFlagsEnabled(FLAG_PREDICTIVE_BACK_THREE_BUTTON_NAV)
+    public void testPredictiveBackInvoked() {
+        ArgumentCaptor<KeyEvent> keyEventCaptor = ArgumentCaptor.forClass(KeyEvent.class);
+        mNavButtonController.sendBackKeyEvent(KeyEvent.ACTION_DOWN, false);
+        mNavButtonController.sendBackKeyEvent(KeyEvent.ACTION_UP, false);
+        verify(mockSystemUiProxy, times(2)).onBackEvent(keyEventCaptor.capture());
+        verifyKeyEvent(keyEventCaptor.getAllValues().getFirst(), KeyEvent.ACTION_DOWN, false);
+        verifyKeyEvent(keyEventCaptor.getAllValues().getLast(), KeyEvent.ACTION_UP, false);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(FLAG_PREDICTIVE_BACK_THREE_BUTTON_NAV)
+    public void testPredictiveBackCancelled() {
+        ArgumentCaptor<KeyEvent> keyEventCaptor = ArgumentCaptor.forClass(KeyEvent.class);
+        mNavButtonController.sendBackKeyEvent(KeyEvent.ACTION_DOWN, false);
+        mNavButtonController.sendBackKeyEvent(KeyEvent.ACTION_UP, true);
+        verify(mockSystemUiProxy, times(2)).onBackEvent(keyEventCaptor.capture());
+        verifyKeyEvent(keyEventCaptor.getAllValues().getFirst(), KeyEvent.ACTION_DOWN, false);
+        verifyKeyEvent(keyEventCaptor.getAllValues().getLast(), KeyEvent.ACTION_UP, true);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(FLAG_PREDICTIVE_BACK_THREE_BUTTON_NAV)
+    public void testButtonsDisabledWhileBackPressed() {
+        mNavButtonController.sendBackKeyEvent(KeyEvent.ACTION_DOWN, false);
+        mNavButtonController.onButtonClick(BUTTON_HOME, mockView);
+        mNavButtonController.onButtonClick(BUTTON_RECENTS, mockView);
+        mNavButtonController.onButtonLongClick(BUTTON_A11Y, mockView);
+        mNavButtonController.onButtonClick(BUTTON_IME_SWITCH, mockView);
+        mNavButtonController.sendBackKeyEvent(KeyEvent.ACTION_UP, false);
+        assertThat(mHomePressCount).isEqualTo(0);
+        verify(mockSystemUiProxy, never()).notifyAccessibilityButtonLongClicked();
+        assertThat(mOverviewToggleCount).isEqualTo(0);
+        verify(mockSystemUiProxy, never()).onImeSwitcherPressed();
+    }
+
+    private void verifyKeyEvent(KeyEvent keyEvent, int action, boolean isCancelled) {
+        assertEquals(isCancelled, keyEvent.isCanceled());
+        assertEquals(action, KeyEvent.ACTION_DOWN, keyEvent.getAction());
+    }
 }
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarOverflowTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarOverflowTest.kt
new file mode 100644
index 0000000..cc8582c
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarOverflowTest.kt
@@ -0,0 +1,313 @@
+/*
+ * 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.content.ComponentName
+import android.content.Intent
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
+import com.android.launcher3.Flags.FLAG_TASKBAR_OVERFLOW
+import com.android.launcher3.taskbar.TaskbarControllerTestUtil.runOnMainSync
+import com.android.launcher3.taskbar.bubbles.BubbleBarViewController
+import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController
+import com.android.launcher3.taskbar.rules.MockedRecentsModelTestRule
+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.TestUtil.getOnUiThread
+import com.android.quickstep.SystemUiProxy
+import com.android.quickstep.util.DesktopTask
+import com.android.systemui.shared.recents.model.Task
+import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE
+import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS
+import com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_BAR
+import com.android.wm.shell.desktopmode.IDesktopTaskListener
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(LauncherMultivalentJUnit::class)
+@EmulatedDevices(["pixelTablet2023"])
+@EnableFlags(
+    FLAG_TASKBAR_OVERFLOW,
+    FLAG_ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS,
+    FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+    FLAG_ENABLE_BUBBLE_BAR,
+)
+class TaskbarOverflowTest {
+    @get:Rule(order = 0) val setFlagsRule = SetFlagsRule()
+
+    @get:Rule(order = 1)
+    val context =
+        TaskbarWindowSandboxContext.create { builder ->
+            builder.bindSystemUiProxy(
+                object : SystemUiProxy(this) {
+                    override fun setDesktopTaskListener(listener: IDesktopTaskListener?) {
+                        desktopTaskListener = listener
+                    }
+                }
+            )
+        }
+
+    @get:Rule(order = 2) val recentsModel = MockedRecentsModelTestRule(context)
+
+    @get:Rule(order = 3) val taskbarModeRule = TaskbarModeRule(context)
+
+    @get:Rule(order = 4) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context)
+
+    @InjectController lateinit var taskbarViewController: TaskbarViewController
+    @InjectController lateinit var recentAppsController: TaskbarRecentAppsController
+    @InjectController lateinit var bubbleBarViewController: BubbleBarViewController
+    @InjectController lateinit var bubbleStashController: BubbleStashController
+
+    private var desktopTaskListener: IDesktopTaskListener? = null
+
+    @Before
+    fun ensureRunningAppsShowing() {
+        runOnMainSync {
+            if (!recentAppsController.canShowRunningApps) {
+                recentAppsController.onDestroy()
+                recentAppsController.canShowRunningApps = true
+                recentAppsController.init(taskbarUnitTestRule.activityContext.controllers)
+            }
+            recentsModel.resolvePendingTaskRequests()
+        }
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testTaskbarWithMaxNumIcons_pinned() {
+        addRunningAppsAndVerifyOverflowState(0)
+
+        assertThat(taskbarIconsCentered).isTrue()
+        assertThat(taskbarEndMargin).isAtLeast(navButtonEndSpacing)
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testTaskbarWithMaxNumIcons_transient() {
+        addRunningAppsAndVerifyOverflowState(0)
+
+        assertThat(taskbarIconsCentered).isTrue()
+        assertThat(taskbarEndMargin).isAtLeast(navButtonEndSpacing)
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testOverflownTaskbar_pinned() {
+        addRunningAppsAndVerifyOverflowState(5)
+
+        assertThat(taskbarIconsCentered).isTrue()
+        assertThat(taskbarEndMargin).isAtLeast(navButtonEndSpacing)
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testOverflownTaskbar_transient() {
+        addRunningAppsAndVerifyOverflowState(5)
+
+        assertThat(taskbarIconsCentered).isTrue()
+        assertThat(taskbarEndMargin).isAtLeast(navButtonEndSpacing)
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testBubbleBarReducesTaskbarMaxNumIcons_pinned() {
+        var initialMaxNumIconViews = maxNumberOfTaskbarIcons
+        assertThat(initialMaxNumIconViews).isGreaterThan(0)
+
+        runOnMainSync { bubbleBarViewController.setHiddenForBubbles(false) }
+
+        val maxNumIconViews = addRunningAppsAndVerifyOverflowState(2)
+        assertThat(maxNumIconViews).isLessThan(initialMaxNumIconViews)
+
+        assertThat(taskbarIconsCentered).isTrue()
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testBubbleBarReducesTaskbarMaxNumIcons_transient() {
+        var initialMaxNumIconViews = maxNumberOfTaskbarIcons
+        assertThat(initialMaxNumIconViews).isGreaterThan(0)
+
+        runOnMainSync { bubbleBarViewController.setHiddenForBubbles(false) }
+
+        val maxNumIconViews = addRunningAppsAndVerifyOverflowState(2)
+        assertThat(maxNumIconViews).isLessThan(initialMaxNumIconViews)
+
+        assertThat(taskbarIconsCentered).isTrue()
+        assertThat(taskbarEndMargin)
+            .isAtLeast(
+                navButtonEndSpacing +
+                    bubbleBarViewController.collapsedWidthWithMaxVisibleBubbles.toInt()
+            )
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testBubbleBarReducesTaskbarMaxNumIcons_transientBubbleInitiallyStashed() {
+        var initialMaxNumIconViews = maxNumberOfTaskbarIcons
+        assertThat(initialMaxNumIconViews).isGreaterThan(0)
+        runOnMainSync {
+            bubbleStashController.stashBubbleBarImmediate()
+            bubbleBarViewController.setHiddenForBubbles(false)
+        }
+
+        val maxNumIconViews = addRunningAppsAndVerifyOverflowState(2)
+        assertThat(maxNumIconViews).isLessThan(initialMaxNumIconViews)
+
+        assertThat(taskbarIconsCentered).isTrue()
+        assertThat(taskbarEndMargin)
+            .isAtLeast(
+                navButtonEndSpacing +
+                    bubbleBarViewController.collapsedWidthWithMaxVisibleBubbles.toInt()
+            )
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testStashingBubbleBarMaintainsMaxNumIcons_transient() {
+        runOnMainSync { bubbleBarViewController.setHiddenForBubbles(false) }
+
+        val initialNumIcons = currentNumberOfTaskbarIcons
+        val maxNumIconViews = addRunningAppsAndVerifyOverflowState(2)
+
+        runOnMainSync { bubbleStashController.stashBubbleBarImmediate() }
+        assertThat(maxNumberOfTaskbarIcons).isEqualTo(maxNumIconViews)
+        assertThat(currentNumberOfTaskbarIcons).isEqualTo(maxNumIconViews)
+        assertThat(taskbarOverflowIconIndex).isEqualTo(initialNumIcons.coerceAtLeast(2))
+    }
+
+    @Test
+    @TaskbarMode(PINNED)
+    fun testHidingBubbleBarIncreasesMaxNumIcons_pinned() {
+        runOnMainSync { bubbleBarViewController.setHiddenForBubbles(false) }
+
+        val initialNumIcons = currentNumberOfTaskbarIcons
+        val initialMaxNumIconViews = addRunningAppsAndVerifyOverflowState(5)
+
+        runOnMainSync { bubbleBarViewController.setHiddenForBubbles(true) }
+
+        val maxNumIconViews = maxNumberOfTaskbarIcons
+        assertThat(maxNumIconViews).isGreaterThan(initialMaxNumIconViews)
+        assertThat(currentNumberOfTaskbarIcons).isEqualTo(maxNumIconViews)
+        assertThat(taskbarOverflowIconIndex).isEqualTo(initialNumIcons.coerceAtLeast(2))
+
+        assertThat(taskbarIconsCentered).isTrue()
+    }
+
+    @Test
+    @TaskbarMode(TRANSIENT)
+    fun testHidingBubbleBarIncreasesMaxNumIcons_transient() {
+        runOnMainSync { bubbleBarViewController.setHiddenForBubbles(false) }
+
+        val initialNumIcons = currentNumberOfTaskbarIcons
+        val initialMaxNumIconViews = addRunningAppsAndVerifyOverflowState(5)
+
+        runOnMainSync { bubbleBarViewController.setHiddenForBubbles(true) }
+
+        val maxNumIconViews = maxNumberOfTaskbarIcons
+        assertThat(maxNumIconViews).isGreaterThan(initialMaxNumIconViews)
+        assertThat(currentNumberOfTaskbarIcons).isEqualTo(maxNumIconViews)
+        assertThat(taskbarOverflowIconIndex).isEqualTo(initialNumIcons.coerceAtLeast(2))
+
+        assertThat(taskbarIconsCentered).isTrue()
+    }
+
+    private fun createDesktopTask(tasksToAdd: Int) {
+        val tasks =
+            (0..<tasksToAdd).map {
+                Task(Task.TaskKey(it, 0, Intent(), ComponentName("", ""), 0, 2000))
+            }
+        recentsModel.updateRecentTasks(listOf(DesktopTask(tasks)))
+        desktopTaskListener?.onTasksVisibilityChanged(
+            context.virtualDisplay.display.displayId,
+            tasksToAdd,
+        )
+        runOnMainSync { recentsModel.resolvePendingTaskRequests() }
+    }
+
+    private val navButtonEndSpacing: Int
+        get() {
+            return taskbarUnitTestRule.activityContext.resources.getDimensionPixelSize(
+                taskbarUnitTestRule.activityContext.deviceProfile.inv.inlineNavButtonsEndSpacing
+            )
+        }
+
+    private val taskbarOverflowIconIndex: Int
+        get() {
+            return getOnUiThread {
+                taskbarViewController.iconViews.indexOfFirst { it is TaskbarOverflowView }
+            }
+        }
+
+    private val maxNumberOfTaskbarIcons: Int
+        get() = getOnUiThread { taskbarViewController.maxNumIconViews }
+
+    private val currentNumberOfTaskbarIcons: Int
+        get() = getOnUiThread { taskbarViewController.iconViews.size }
+
+    private val taskbarIconsCentered: Boolean
+        get() {
+            return getOnUiThread {
+                val iconLayoutBounds = taskbarViewController.iconLayoutBounds
+                val availableWidth = taskbarUnitTestRule.activityContext.deviceProfile.widthPx
+                iconLayoutBounds.left - (availableWidth - iconLayoutBounds.right) < 2
+            }
+        }
+
+    private val taskbarEndMargin: Int
+        get() {
+            return getOnUiThread {
+                taskbarUnitTestRule.activityContext.deviceProfile.widthPx -
+                    taskbarViewController.iconLayoutBounds.right
+            }
+        }
+
+    /**
+     * Adds enough running apps for taskbar to enter overflow of `targetOverflowSize`, and verifies
+     * * max number of icons in the taskbar remains unchanged
+     * * number of icons in the taskbar is at most max number of icons
+     * * whether the taskbar overflow icon is shown, and its position in taskbar.
+     *
+     * Returns max number of icons.
+     */
+    private fun addRunningAppsAndVerifyOverflowState(targetOverflowSize: Int): Int {
+        val maxNumIconViews = maxNumberOfTaskbarIcons
+        assertThat(maxNumIconViews).isGreaterThan(0)
+        // Assume there are at least all apps and divider icon, as they would appear once running
+        // apps are added, even if not present initially.
+        val initialIconCount = currentNumberOfTaskbarIcons.coerceAtLeast(2)
+        assertThat(initialIconCount).isLessThan(maxNumIconViews)
+
+        createDesktopTask(maxNumIconViews - initialIconCount + targetOverflowSize)
+
+        assertThat(maxNumberOfTaskbarIcons).isEqualTo(maxNumIconViews)
+        assertThat(currentNumberOfTaskbarIcons).isEqualTo(maxNumIconViews)
+        assertThat(taskbarOverflowIconIndex)
+            .isEqualTo(if (targetOverflowSize > 0) initialIconCount else -1)
+        return maxNumIconViews
+    }
+}
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..5e438bd
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarStashControllerTest.kt
@@ -0,0 +1,681 @@
+/*
+ * 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.QuickstepTransitionManager.PINNED_TASKBAR_TRANSITION_DURATION
+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(PINNED_TASKBAR_TRANSITION_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/TaskbarViewTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewTest.kt
new file mode 100644
index 0000000..0bb404b
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewTest.kt
@@ -0,0 +1,157 @@
+/*
+ * 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.platform.test.flag.junit.FlagsParameterization
+import android.platform.test.flag.junit.FlagsParameterization.allCombinationsOf
+import android.platform.test.flag.junit.SetFlagsRule
+import com.android.launcher3.Flags.FLAG_TASKBAR_RECENTS_LAYOUT_TRANSITION
+import com.android.launcher3.R
+import com.android.launcher3.taskbar.TaskbarControllerTestUtil.runOnMainSync
+import com.android.launcher3.taskbar.TaskbarIconType.ALL_APPS
+import com.android.launcher3.taskbar.TaskbarIconType.DIVIDER
+import com.android.launcher3.taskbar.TaskbarIconType.HOTSEAT
+import com.android.launcher3.taskbar.TaskbarIconType.RECENT
+import com.android.launcher3.taskbar.TaskbarViewTestUtil.assertThat
+import com.android.launcher3.taskbar.TaskbarViewTestUtil.createHotseatItems
+import com.android.launcher3.taskbar.TaskbarViewTestUtil.createRecents
+import com.android.launcher3.taskbar.rules.TaskbarDeviceEmulationRule
+import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule
+import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.ForceRtl
+import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext
+import com.android.launcher3.util.LauncherMultivalentJUnit.Companion.isRunningInRobolectric
+import org.junit.Before
+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
+
+@RunWith(ParameterizedAndroidJunit4::class)
+class TaskbarViewTest(deviceName: String, flags: FlagsParameterization) {
+
+    companion object {
+        @JvmStatic
+        @Parameters(name = "{0},{1}")
+        fun getParams(): List<Array<Any>> {
+            val devices =
+                if (isRunningInRobolectric) {
+                    listOf("pixelFoldable2023", "pixelTablet2023")
+                } else {
+                    listOf("onDevice") // Unused.
+                }
+            val flags = allCombinationsOf(FLAG_TASKBAR_RECENTS_LAYOUT_TRANSITION)
+            return devices.flatMap { d -> flags.map { f -> arrayOf(d, f) } } // Cartesian product.
+        }
+    }
+
+    @get:Rule(order = 0) val setFlagsRule = SetFlagsRule(flags)
+    @get:Rule(order = 1) val context = TaskbarWindowSandboxContext.create()
+    @get:Rule(order = 2) val deviceEmulationRule = TaskbarDeviceEmulationRule(context, deviceName)
+    @get:Rule(order = 3) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context)
+
+    private lateinit var taskbarView: TaskbarView
+
+    @Before
+    fun obtainView() {
+        taskbarView = taskbarUnitTestRule.activityContext.dragLayer.findViewById(R.id.taskbar_view)
+    }
+
+    @Test
+    fun testUpdateItems_noItems_hasOnlyAllApps() {
+        runOnMainSync { taskbarView.updateItems(emptyArray(), emptyList()) }
+        assertThat(taskbarView).hasIconTypes(ALL_APPS)
+    }
+
+    @Test
+    fun testUpdateItems_hotseatItems_hasDividerBetweenAllAppsAndHotseat() {
+        runOnMainSync { taskbarView.updateItems(createHotseatItems(2), emptyList()) }
+        assertThat(taskbarView).hasIconTypes(ALL_APPS, DIVIDER, HOTSEAT, HOTSEAT)
+    }
+
+    @Test
+    @ForceRtl
+    fun testUpdateItems_rtlWithHotseatItems_hasDividerBetweenHotseatAndAllApps() {
+        runOnMainSync { taskbarView.updateItems(createHotseatItems(2), emptyList()) }
+        assertThat(taskbarView).hasIconTypes(HOTSEAT, HOTSEAT, DIVIDER, ALL_APPS)
+    }
+
+    @Test
+    fun testUpdateItems_withNullHotseatItem_filtersNullItem() {
+        runOnMainSync {
+            taskbarView.updateItems(arrayOf(*createHotseatItems(2), null), emptyList())
+        }
+        assertThat(taskbarView).hasIconTypes(ALL_APPS, DIVIDER, HOTSEAT, HOTSEAT)
+    }
+
+    @Test
+    @ForceRtl
+    fun testUpdateItems_rtlWithNullHotseatItem_filtersNullItem() {
+        runOnMainSync {
+            taskbarView.updateItems(arrayOf(*createHotseatItems(2), null), emptyList())
+        }
+        assertThat(taskbarView).hasIconTypes(HOTSEAT, HOTSEAT, DIVIDER, ALL_APPS)
+    }
+
+    @Test
+    fun testUpdateItems_recentsItems_hasDividerBetweenAllAppsAndRecents() {
+        runOnMainSync { taskbarView.updateItems(emptyArray(), createRecents(4)) }
+        assertThat(taskbarView).hasIconTypes(ALL_APPS, DIVIDER, *RECENT * 4)
+    }
+
+    @Test
+    fun testUpdateItems_hotseatItemsAndRecents_hasDividerBetweenHotseatAndRecents() {
+        runOnMainSync { taskbarView.updateItems(createHotseatItems(3), createRecents(2)) }
+        assertThat(taskbarView).hasIconTypes(ALL_APPS, *HOTSEAT * 3, DIVIDER, *RECENT * 2)
+    }
+
+    @Test
+    fun testUpdateItems_addHotseatItem_updatesHotseat() {
+        runOnMainSync {
+            taskbarView.updateItems(createHotseatItems(1), createRecents(1))
+            taskbarView.updateItems(createHotseatItems(2), createRecents(1))
+        }
+        assertThat(taskbarView).hasIconTypes(ALL_APPS, *HOTSEAT * 2, DIVIDER, RECENT)
+    }
+
+    @Test
+    fun testUpdateItems_removeHotseatItem_updatesHotseat() {
+        runOnMainSync {
+            taskbarView.updateItems(createHotseatItems(2), createRecents(1))
+            taskbarView.updateItems(createHotseatItems(1), createRecents(1))
+        }
+        assertThat(taskbarView).hasIconTypes(ALL_APPS, HOTSEAT, DIVIDER, RECENT)
+    }
+
+    @Test
+    fun testUpdateItems_addRecentsItem_updatesRecents() {
+        runOnMainSync {
+            taskbarView.updateItems(createHotseatItems(1), createRecents(1))
+            taskbarView.updateItems(createHotseatItems(1), createRecents(2))
+        }
+        assertThat(taskbarView).hasIconTypes(ALL_APPS, HOTSEAT, DIVIDER, *RECENT * 2)
+    }
+
+    @Test
+    fun testUpdateItems_removeRecentsItem_updatesRecents() {
+        runOnMainSync {
+            taskbarView.updateItems(createHotseatItems(1), createRecents(2))
+            taskbarView.updateItems(createHotseatItems(1), createRecents(1))
+        }
+        assertThat(taskbarView).hasIconTypes(ALL_APPS, HOTSEAT, DIVIDER, RECENT)
+    }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewTestUtil.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewTestUtil.kt
new file mode 100644
index 0000000..a6bdbb0
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewTestUtil.kt
@@ -0,0 +1,123 @@
+/*
+ * 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.content.ComponentName
+import android.content.Intent
+import android.os.Process
+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.taskbar.TaskbarIconType.ALL_APPS
+import com.android.launcher3.taskbar.TaskbarIconType.DIVIDER
+import com.android.launcher3.taskbar.TaskbarIconType.HOTSEAT
+import com.android.launcher3.taskbar.TaskbarIconType.OVERFLOW
+import com.android.launcher3.taskbar.TaskbarIconType.RECENT
+import com.android.quickstep.util.GroupTask
+import com.android.systemui.shared.recents.model.Task
+import com.android.systemui.shared.recents.model.Task.TaskKey
+import com.google.common.truth.FailureMetadata
+import com.google.common.truth.Subject
+import com.google.common.truth.Truth.assertAbout
+import com.google.common.truth.Truth.assertThat
+
+/** Common utilities for testing [TaskbarView]. */
+object TaskbarViewTestUtil {
+
+    /** Begins an assertion about a [TaskbarView]. */
+    fun assertThat(view: TaskbarView): TaskbarViewSubject {
+        return assertAbout(::TaskbarViewSubject).that(view)
+    }
+
+    /** Creates an array of fake hotseat items. */
+    fun createHotseatItems(size: Int): Array<ItemInfo> {
+        return Array(size) {
+            WorkspaceItemInfo(
+                    AppInfo(TEST_COMPONENT, "Test App $it", Process.myUserHandle(), Intent())
+                )
+                .apply { id = it }
+        }
+    }
+
+    /** Creates a list of fake recent tasks. */
+    fun createRecents(size: Int): List<GroupTask> {
+        return List(size) {
+            GroupTask(
+                Task().apply {
+                    key =
+                        TaskKey(
+                            it,
+                            5,
+                            TEST_INTENT,
+                            TEST_COMPONENT,
+                            Process.myUserHandle().identifier,
+                            System.currentTimeMillis(),
+                        )
+                }
+            )
+        }
+    }
+}
+
+/** A `Truth` [Subject] with extensions for verifying [TaskbarView]. */
+class TaskbarViewSubject(failureMetadata: FailureMetadata, private val view: TaskbarView) :
+    Subject(failureMetadata, view) {
+
+    /** Verifies that the types of icons match [expectedTypes] in order. */
+    fun hasIconTypes(vararg expectedTypes: TaskbarIconType) {
+        val actualTypes =
+            view.iconViews.map {
+                when (it) {
+                    view.allAppsButtonContainer -> ALL_APPS
+                    view.taskbarDividerViewContainer -> DIVIDER
+                    view.taskbarOverflowView -> OVERFLOW
+                    else ->
+                        when (it.tag) {
+                            is ItemInfo -> HOTSEAT
+                            is GroupTask -> RECENT
+                            else -> throw IllegalStateException("Unknown type for $it")
+                        }
+                }
+            }
+        assertThat(actualTypes).containsExactly(*expectedTypes).inOrder()
+    }
+
+    /** Verifies that recents from [startIndex] have IDs that match [expectedIds] in order. */
+    fun hasRecentsOrder(startIndex: Int, expectedIds: List<Int>) {
+        val actualIds =
+            view.iconViews.slice(startIndex..<expectedIds.size).map {
+                assertThat(it.tag).isInstanceOf(GroupTask::class.java)
+                (it.tag as? GroupTask)?.task1?.key?.id
+            }
+        assertThat(actualIds).containsExactlyElementsIn(expectedIds).inOrder()
+    }
+}
+
+/** Types of icons in the [TaskbarView]. */
+enum class TaskbarIconType {
+    ALL_APPS,
+    DIVIDER,
+    HOTSEAT,
+    RECENT,
+    OVERFLOW;
+
+    operator fun times(size: Int) = Array(size) { this }
+}
+
+private const val TEST_PACKAGE = "com.android.launcher3.taskbar"
+private val TEST_COMPONENT = ComponentName(TEST_PACKAGE, "Activity")
+private val TEST_INTENT = Intent().apply { `package` = TEST_PACKAGE }
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewWithLayoutTransitionTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewWithLayoutTransitionTest.kt
new file mode 100644
index 0000000..78d8e5d
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewWithLayoutTransitionTest.kt
@@ -0,0 +1,134 @@
+/*
+ * 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.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
+import com.android.launcher3.Flags.FLAG_TASKBAR_RECENTS_LAYOUT_TRANSITION
+import com.android.launcher3.R
+import com.android.launcher3.taskbar.TaskbarControllerTestUtil.runOnMainSync
+import com.android.launcher3.taskbar.TaskbarIconType.ALL_APPS
+import com.android.launcher3.taskbar.TaskbarIconType.DIVIDER
+import com.android.launcher3.taskbar.TaskbarIconType.HOTSEAT
+import com.android.launcher3.taskbar.TaskbarIconType.RECENT
+import com.android.launcher3.taskbar.TaskbarViewTestUtil.assertThat
+import com.android.launcher3.taskbar.TaskbarViewTestUtil.createHotseatItems
+import com.android.launcher3.taskbar.TaskbarViewTestUtil.createRecents
+import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule
+import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.ForceRtl
+import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext
+import com.android.launcher3.util.LauncherMultivalentJUnit
+import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(LauncherMultivalentJUnit::class)
+@EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"])
+@EnableFlags(FLAG_TASKBAR_RECENTS_LAYOUT_TRANSITION)
+class TaskbarViewWithLayoutTransitionTest {
+
+    @get:Rule(order = 0) val setFlagsRule = SetFlagsRule()
+    @get:Rule(order = 1) val context = TaskbarWindowSandboxContext.create()
+    @get:Rule(order = 2) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context)
+
+    private lateinit var taskbarView: TaskbarView
+
+    @Before
+    fun obtainView() {
+        taskbarView = taskbarUnitTestRule.activityContext.dragLayer.findViewById(R.id.taskbar_view)
+    }
+
+    @Test
+    @ForceRtl
+    fun testUpdateItems_rtl_hotseatItems_hasDividerBetweenHotseatAndAllApps() {
+        runOnMainSync { taskbarView.updateItems(createHotseatItems(2), emptyList()) }
+        assertThat(taskbarView).hasIconTypes(*HOTSEAT * 2, DIVIDER, ALL_APPS)
+    }
+
+    @Test
+    @ForceRtl
+    fun testUpdateItems_rtl_recentsItems_hasDividerBetweenRecentsAndAllApps() {
+        runOnMainSync { taskbarView.updateItems(emptyArray(), createRecents(4)) }
+        assertThat(taskbarView).hasIconTypes(*RECENT * 4, DIVIDER, ALL_APPS)
+    }
+
+    @Test
+    @ForceRtl
+    fun testUpdateItems_rtl_recentsItems_recentsAreReversed() {
+        runOnMainSync { taskbarView.updateItems(emptyArray(), createRecents(4)) }
+        assertThat(taskbarView).hasRecentsOrder(startIndex = 0, expectedIds = listOf(3, 2, 1, 0))
+    }
+
+    @Test
+    @ForceRtl
+    fun testUpdateItems_rtl_hotseatItemsAndRecents_hasDividerBetweenRecentsAndHotseat() {
+        runOnMainSync { taskbarView.updateItems(createHotseatItems(3), createRecents(2)) }
+        assertThat(taskbarView).hasIconTypes(*RECENT * 2, DIVIDER, *HOTSEAT * 3, ALL_APPS)
+    }
+
+    @Test
+    @ForceRtl
+    fun testUpdateItems_rtl_addHotseatItemWithoutRecents_updatesHotseat() {
+        runOnMainSync {
+            taskbarView.updateItems(createHotseatItems(1), emptyList())
+            taskbarView.updateItems(createHotseatItems(2), emptyList())
+        }
+        assertThat(taskbarView).hasIconTypes(*HOTSEAT * 2, DIVIDER, ALL_APPS)
+    }
+
+    @Test
+    @ForceRtl
+    fun testUpdateItems_rtl_addHotseatItemWithRecents_updatesHotseat() {
+        runOnMainSync {
+            taskbarView.updateItems(createHotseatItems(1), createRecents(1))
+            taskbarView.updateItems(createHotseatItems(2), createRecents(1))
+        }
+        assertThat(taskbarView).hasIconTypes(RECENT, DIVIDER, *HOTSEAT * 2, ALL_APPS)
+    }
+
+    @Test
+    @ForceRtl
+    fun testUpdateItems_rtl_removeHotseatItem_updatesHotseat() {
+        runOnMainSync {
+            taskbarView.updateItems(createHotseatItems(2), createRecents(1))
+            taskbarView.updateItems(createHotseatItems(1), createRecents(1))
+        }
+        assertThat(taskbarView).hasIconTypes(RECENT, DIVIDER, HOTSEAT, ALL_APPS)
+    }
+
+    @Test
+    @ForceRtl
+    fun testUpdateItems_rtl_addRecentsItem_updatesRecents() {
+        runOnMainSync {
+            taskbarView.updateItems(createHotseatItems(1), createRecents(1))
+            taskbarView.updateItems(createHotseatItems(1), createRecents(2))
+        }
+        assertThat(taskbarView).hasIconTypes(*RECENT * 2, DIVIDER, HOTSEAT, ALL_APPS)
+    }
+
+    @Test
+    @ForceRtl
+    fun testUpdateItems_rtl_removeRecentsItem_updatesRecents() {
+        runOnMainSync {
+            taskbarView.updateItems(createHotseatItems(1), createRecents(2))
+            taskbarView.updateItems(createHotseatItems(1), createRecents(1))
+        }
+        assertThat(taskbarView).hasIconTypes(RECENT, DIVIDER, HOTSEAT, ALL_APPS)
+    }
+}
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..e12876f 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,17 +38,25 @@
 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
 import com.android.wm.shell.shared.bubbles.BubbleInfo
 import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.Semaphore
+import java.util.concurrent.TimeUnit
 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.atLeastOnce
+import org.mockito.kotlin.eq
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.verify
 import org.mockito.kotlin.whenever
@@ -63,13 +73,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 +101,9 @@
             BubbleBarViewAnimator(
                 bubbleBarView,
                 bubbleStashController,
+                flyoutController,
                 onExpandedNoOp,
-                animatorScheduler
+                animatorScheduler,
             )
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
@@ -106,10 +123,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 +155,9 @@
             BubbleBarViewAnimator(
                 bubbleBarView,
                 bubbleStashController,
+                flyoutController,
                 onExpandedNoOp,
-                animatorScheduler
+                animatorScheduler,
             )
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
@@ -157,10 +179,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 +208,9 @@
             BubbleBarViewAnimator(
                 bubbleBarView,
                 bubbleStashController,
+                flyoutController,
                 onExpandedNoOp,
-                animatorScheduler
+                animatorScheduler,
             )
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
@@ -227,8 +254,9 @@
             BubbleBarViewAnimator(
                 bubbleBarView,
                 bubbleStashController,
+                flyoutController,
                 onExpandedNoOp,
-                animatorScheduler
+                animatorScheduler,
             )
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
@@ -239,10 +267,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 +305,9 @@
             BubbleBarViewAnimator(
                 bubbleBarView,
                 bubbleStashController,
+                flyoutController,
                 onExpandedNoOp,
-                animatorScheduler
+                animatorScheduler,
             )
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
@@ -310,8 +343,9 @@
             BubbleBarViewAnimator(
                 bubbleBarView,
                 bubbleStashController,
+                flyoutController,
                 onExpanded,
-                animatorScheduler
+                animatorScheduler,
             )
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
@@ -354,8 +388,9 @@
             BubbleBarViewAnimator(
                 bubbleBarView,
                 bubbleStashController,
+                flyoutController,
                 onExpanded,
-                animatorScheduler
+                animatorScheduler,
             )
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
@@ -404,8 +439,9 @@
             BubbleBarViewAnimator(
                 bubbleBarView,
                 bubbleStashController,
+                flyoutController,
                 onExpanded,
-                animatorScheduler
+                animatorScheduler,
             )
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
@@ -418,6 +454,9 @@
         PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
 
         assertThat(animator.isAnimating).isTrue()
+
+        waitForFlyoutToShow()
+
         // verify the hide bubble animation is pending
         assertThat(animatorScheduler.delayedBlock).isNotNull()
 
@@ -428,6 +467,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 +494,9 @@
             BubbleBarViewAnimator(
                 bubbleBarView,
                 bubbleStashController,
+                flyoutController,
                 onExpandedNoOp,
-                animatorScheduler
+                animatorScheduler,
             )
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
@@ -469,9 +511,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 +549,9 @@
             BubbleBarViewAnimator(
                 bubbleBarView,
                 bubbleStashController,
+                flyoutController,
                 onExpanded,
-                animatorScheduler
+                animatorScheduler,
             )
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
@@ -537,8 +584,9 @@
             BubbleBarViewAnimator(
                 bubbleBarView,
                 bubbleStashController,
+                flyoutController,
                 onExpandedNoOp,
-                animatorScheduler
+                animatorScheduler,
             )
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
@@ -553,9 +601,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 +628,9 @@
             BubbleBarViewAnimator(
                 bubbleBarView,
                 bubbleStashController,
+                flyoutController,
                 onExpanded,
-                animatorScheduler
+                animatorScheduler,
             )
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
@@ -624,8 +677,9 @@
             BubbleBarViewAnimator(
                 bubbleBarView,
                 bubbleStashController,
+                flyoutController,
                 onExpanded,
-                animatorScheduler
+                animatorScheduler,
             )
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
@@ -636,6 +690,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 +700,8 @@
             animator.expandedWhileAnimating()
         }
 
+        waitForFlyoutToHide()
+
         // verify that the hide animation was canceled
         assertThat(animatorScheduler.delayedBlock).isNull()
 
@@ -665,8 +723,9 @@
             BubbleBarViewAnimator(
                 bubbleBarView,
                 bubbleStashController,
+                flyoutController,
                 onExpandedNoOp,
-                animatorScheduler
+                animatorScheduler,
             )
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
@@ -687,9 +746,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)
@@ -704,16 +767,19 @@
         whenever(bubbleStashController.bubbleBarTranslationY)
             .thenReturn(BAR_TRANSLATION_Y_FOR_HOTSEAT)
 
-        val barAnimator = PhysicsAnimator.getInstance(bubbleBarView)
-
+        val semaphore = Semaphore(0)
         var notifiedExpanded = false
-        val onExpanded = Runnable { notifiedExpanded = true }
+        val onExpanded = Runnable {
+            notifiedExpanded = true
+            semaphore.release()
+        }
         val animator =
             BubbleBarViewAnimator(
                 bubbleBarView,
                 bubbleStashController,
+                flyoutController,
                 onExpanded,
-                animatorScheduler
+                animatorScheduler,
             )
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
@@ -731,7 +797,12 @@
 
         // the lift animation is complete; the spring back animation should start now
         InstrumentationRegistry.getInstrumentation().runOnMainSync {}
-        barAnimator.assertIsRunning()
+
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+
+        assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue()
+        // we should be expanded now
+        assertThat(bubbleBarView.isExpanded).isTrue()
         PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
 
         // verify there is no hide animation
@@ -739,7 +810,6 @@
 
         assertThat(animator.isAnimating).isFalse()
         assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT)
-        assertThat(bubbleBarView.isExpanded).isTrue()
         verify(bubbleStashController).showBubbleBarImmediate()
         assertThat(notifiedExpanded).isTrue()
     }
@@ -759,8 +829,9 @@
             BubbleBarViewAnimator(
                 bubbleBarView,
                 bubbleStashController,
+                flyoutController,
                 onExpanded,
-                animatorScheduler
+                animatorScheduler,
             )
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
@@ -773,23 +844,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 +892,9 @@
             BubbleBarViewAnimator(
                 bubbleBarView,
                 bubbleStashController,
+                flyoutController,
                 onExpanded,
-                animatorScheduler
+                animatorScheduler,
             )
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
@@ -843,6 +919,8 @@
         assertThat(animatorScheduler.delayedBlock).isNotNull()
         assertThat(animator.isAnimating).isTrue()
 
+        waitForFlyoutToShow()
+
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
             animator.expandedWhileAnimating()
         }
@@ -850,6 +928,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 +937,388 @@
         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()
+    }
+
+    @Test
+    fun interruptForIme() {
+        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)
+        }
+
+        // wait for the animation to start
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+        PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(handleAnimator) { true }
+
+        handleAnimator.assertIsRunning()
+        assertThat(animator.isAnimating).isTrue()
+        // verify the hide bubble animation is pending
+        assertThat(animatorScheduler.delayedBlock).isNotNull()
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync { animator.interruptForIme() }
+
+        // verify that the hide animation was canceled
+        assertThat(animatorScheduler.delayedBlock).isNull()
+        assertThat(animator.isAnimating).isFalse()
+        verify(bubbleStashController).onNewBubbleAnimationInterrupted(eq(true), any())
+
+        // PhysicsAnimatorTestUtils posts the cancellation to the main thread so we need to wait
+        // again
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+        handleAnimator.assertIsNotRunning()
+    }
+
     private fun setUpBubbleBar() {
         bubbleBarView = BubbleBarView(context)
         InstrumentationRegistry.getInstrumentation().runOnMainSync {
@@ -870,11 +1332,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 +1375,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 +1413,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 +1448,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/MockedRecentsModelTestRule.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/MockedRecentsModelTestRule.kt
new file mode 100644
index 0000000..ed1443d
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/MockedRecentsModelTestRule.kt
@@ -0,0 +1,97 @@
+/*
+ * 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 com.android.quickstep.RecentsModel
+import com.android.quickstep.RecentsModel.RecentTasksChangedListener
+import com.android.quickstep.TaskIconCache
+import com.android.quickstep.util.GroupTask
+import java.util.function.Consumer
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+import org.mockito.kotlin.any
+import org.mockito.kotlin.anyOrNull
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+
+class MockedRecentsModelTestRule(private val context: TaskbarWindowSandboxContext) : TestRule {
+
+    private val mockIconCache: TaskIconCache = mock()
+
+    private val mockRecentsModel: RecentsModel = mock {
+        on { iconCache } doReturn mockIconCache
+
+        on { unregisterRecentTasksChangedListener() } doAnswer { recentTasksChangedListener = null }
+
+        on { registerRecentTasksChangedListener(any<RecentTasksChangedListener>()) } doAnswer
+            {
+                recentTasksChangedListener = it.getArgument<RecentTasksChangedListener>(0)
+            }
+
+        on { getTasks(anyOrNull(), anyOrNull()) } doAnswer
+            {
+                val request = it.getArgument<Consumer<List<GroupTask>>?>(0)
+                if (request != null) {
+                    taskRequests.add { response -> request.accept(response) }
+                }
+                taskListId
+            }
+
+        on { getTasks(anyOrNull()) } doAnswer
+            {
+                val request = it.getArgument<Consumer<List<GroupTask>>?>(0)
+                if (request != null) {
+                    taskRequests.add { response -> request.accept(response) }
+                }
+                taskListId
+            }
+
+        on { isTaskListValid(any()) } doAnswer { taskListId == it.getArgument(0) }
+    }
+
+    private var recentTasks: List<GroupTask> = emptyList()
+    private var taskListId = 0
+    private var recentTasksChangedListener: RecentTasksChangedListener? = null
+    private var taskRequests: MutableList<(List<GroupTask>) -> Unit> = mutableListOf()
+
+    override fun apply(base: Statement?, description: Description?): Statement {
+        return object : Statement() {
+            override fun evaluate() {
+                context.putObject(RecentsModel.INSTANCE, mockRecentsModel)
+                base?.evaluate()
+            }
+        }
+    }
+
+    // NOTE: For the update to take effect, `resolvePendingTaskRequests()` needs to be called, so
+    // calbacks to any pending `RecentsModel.getTasks()` get called with the updated task list.
+    fun updateRecentTasks(tasks: List<GroupTask>) {
+        ++taskListId
+        recentTasks = tasks
+        recentTasksChangedListener?.onRecentTasksChanged()
+    }
+
+    fun resolvePendingTaskRequests() {
+        val requests = mutableListOf<(List<GroupTask>) -> Unit>()
+        requests.addAll(taskRequests)
+        taskRequests.clear()
+
+        requests.forEach { it(recentTasks) }
+    }
+}
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..cd4e78b 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,27 @@
 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.Locale
+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 +76,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 +86,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 +97,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 {
@@ -153,23 +120,24 @@
                         }
                     }
 
+                if (description.getAnnotation(ForceRtl::class.java) != null) {
+                    // Needs to be set on window context instead of sandbox context, because it does
+                    // does not propagate between them. However, this change will impact created
+                    // TaskbarActivityContext instances, since they wrap the window context.
+                    taskbarManager.windowContext.resources.configuration.setLayoutDirection(
+                        RTL_LOCALE
+                    )
+                }
+
                 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 +148,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.
@@ -215,24 +202,10 @@
     @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)
-    }
+    /** Forces RTL UI for tests. */
+    @Retention(AnnotationRetention.RUNTIME)
+    @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
+    annotation class ForceRtl
 }
+
+private val RTL_LOCALE = Locale.of("ar", "XB")
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..b8b0b5d 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,25 @@
 
 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.Utilities
 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.ForceRtl
 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 +44,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 +135,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() }
@@ -153,6 +199,14 @@
         }
     }
 
+    @Test
+    fun testForceRtlAnnotation_setsActivityContextLayoutDirection() {
+        @ForceRtl class Rtl
+        onSetup(description = Description.createSuiteDescription(Rtl::class.java)) {
+            assertThat(Utilities.isRtl(activityContext.resources)).isTrue()
+        }
+    }
+
     /**
      * Executes [runTest] after the [testRule] setup phase completes.
      *
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..970bdec 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/AbsSwipeUpHandlerTestCase.java
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/AbsSwipeUpHandlerTestCase.java
@@ -16,15 +16,24 @@
 
 package com.android.quickstep;
 
+import static com.android.quickstep.AbsSwipeUpHandler.STATE_HANDLER_INVALIDATED;
+import static com.android.wm.shell.shared.ShellSharedConstants.KEY_EXTRA_SHELL_CAN_HAND_OFF_ANIMATION;
+
+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.never;
 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;
@@ -33,27 +42,34 @@
 import android.graphics.Rect;
 import android.os.Bundle;
 import android.os.SystemClock;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
 import android.view.RemoteAnimationTarget;
 import android.view.SurfaceControl;
 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.Flags;
 import com.android.systemui.shared.system.InputConsumerController;
 
 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 +78,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 =
@@ -97,18 +109,15 @@
             /* startBounds= */ null,
             /* taskInfo= */ mRunningTaskInfo,
             /* allowEnterPip= */ false);
-    protected final RecentsAnimationTargets mRecentsAnimationTargets = new RecentsAnimationTargets(
-            new RemoteAnimationTarget[] {mRemoteAnimationTarget},
-            new RemoteAnimationTarget[] {mRemoteAnimationTarget},
-            new RemoteAnimationTarget[] {mRemoteAnimationTarget},
-            /* homeContentInsets= */ new Rect(),
-            /* minimizedHomeBounds= */ null,
-            new Bundle());
 
-    @Mock protected ACTIVITY_INTERFACE mActivityInterface;
-    @Mock protected ActivityInitListener<?> mActivityInitListener;
+    protected RecentsAnimationTargets mRecentsAnimationTargets;
+    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;
@@ -118,6 +127,22 @@
     @Rule
     public final MockitoRule mMockitoRule = MockitoJUnit.rule();
 
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+    @Before
+    public void setUpAnimationTargets() {
+        Bundle extras = new Bundle();
+        extras.putBoolean(KEY_EXTRA_SHELL_CAN_HAND_OFF_ANIMATION, true);
+        mRecentsAnimationTargets = new RecentsAnimationTargets(
+                new RemoteAnimationTarget[] {mRemoteAnimationTarget},
+                new RemoteAnimationTarget[] {mRemoteAnimationTarget},
+                new RemoteAnimationTarget[] {mRemoteAnimationTarget},
+                /* homeContentInsets= */ new Rect(),
+                /* minimizedHomeBounds= */ null,
+                extras);
+    }
+
     @Before
     public void setUpRunningTaskInfo() {
         mRunningTaskInfo.baseIntent = new Intent(Intent.ACTION_MAIN)
@@ -148,6 +173,7 @@
 
     @Before
     public void setUpRecentsContainer() {
+        mTaskAnimationManager = new TaskAnimationManager(mContext, getRecentsWindowManager());
         RecentsViewContainer recentsContainer = getRecentsContainer();
         RECENTS_VIEW recentsView = getRecentsView();
 
@@ -157,7 +183,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 +191,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 +210,7 @@
         createSwipeHandler()
                 .onRecentsAnimationCanceled(new HashMap<>());
 
-        runOnMainSync(() -> verify(mActivityInitListener)
+        runOnMainSync(() -> verify(mContextInitListener)
                 .unregister(eq("AbsSwipeUpHandler.onRecentsAnimationCanceled")));
     }
 
@@ -186,7 +218,7 @@
     public void testOnConsumerAboutToBeSwitched_unregistersActivityInitListener() {
         createSwipeHandler().onConsumerAboutToBeSwitched();
 
-        runOnMainSync(() -> verify(mActivityInitListener)
+        runOnMainSync(() -> verify(mContextInitListener)
                 .unregister("AbsSwipeUpHandler.invalidateHandler"));
     }
 
@@ -195,7 +227,7 @@
         createSwipeUpHandlerForGesture(GestureState.GestureEndTarget.NEW_TASK)
                 .onConsumerAboutToBeSwitched();
 
-        runOnMainSync(() -> verify(mActivityInitListener)
+        runOnMainSync(() -> verify(mContextInitListener)
                 .unregister(eq("AbsSwipeUpHandler.cancelCurrentAnimation")));
     }
 
@@ -207,7 +239,7 @@
 
         runOnMainSync(() -> {
             absSwipeUpHandler.startNewTask(unused -> {});
-            verify(mRecentsAnimationController).finish(anyBoolean(), any());
+            verifyRecentsAnimationFinishedAndCallCallback();
         });
     }
 
@@ -217,10 +249,81 @@
 
         runOnMainSync(() -> {
             verify(mRecentsAnimationController).detachNavigationBarFromApp(true);
-            verify(mRecentsAnimationController).finish(anyBoolean(), any(), anyBoolean());
+            verifyRecentsAnimationFinishedAndCallCallback();
         });
     }
 
+    @EnableFlags({Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY,
+            Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED})
+    @Test
+    public void testHomeGesture_handsOffAnimation() {
+        createSwipeUpHandlerForGesture(GestureState.GestureEndTarget.HOME);
+
+        runOnMainSync(() -> {
+            verify(mRecentsAnimationController).handOffAnimation(any(), any());
+            verifyRecentsAnimationFinishedAndCallCallback();
+        });
+    }
+
+    @DisableFlags({Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY,
+            Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED})
+    @Test
+    public void testHomeGesture_doesNotHandOffAnimation_withFlagsDisabled() {
+        createSwipeUpHandlerForGesture(GestureState.GestureEndTarget.HOME);
+
+        runOnMainSync(() -> {
+            verify(mRecentsAnimationController, never()).handOffAnimation(any(), any());
+            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 +351,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 +364,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/DesktopFullscreenDrawParamsTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/DesktopFullscreenDrawParamsTest.kt
new file mode 100644
index 0000000..e62455f
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/DesktopFullscreenDrawParamsTest.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.quickstep
+
+import android.content.Context
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
+
+/** Test for [DesktopFullscreenDrawParams] class. */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class DesktopFullscreenDrawParamsTest() {
+    private val params =
+        DesktopFullscreenDrawParams(mock<Context>(), cornerRadiusProvider = { CORNER_RADIUS })
+
+    @Test
+    fun setMiddleProgress_invariantCornerRadiusForDesktop() {
+        params.setProgress(fullscreenProgress = 0f, parentScale = 1f, taskViewScale = 1f)
+        assertThat(params.currentCornerRadius).isEqualTo(CORNER_RADIUS)
+
+        params.setProgress(fullscreenProgress = 0.67f, parentScale = 1f, taskViewScale = 1f)
+        assertThat(params.currentCornerRadius).isEqualTo(CORNER_RADIUS)
+
+        params.setProgress(fullscreenProgress = 1f, parentScale = 1f, taskViewScale = 1f)
+        assertThat(params.currentCornerRadius).isEqualTo(CORNER_RADIUS)
+    }
+
+    companion object {
+        const val CORNER_RADIUS = 32f
+    }
+}
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/FullscreenDrawParamsTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/FullscreenDrawParamsTest.kt
index 5d62a4c..99b81e0 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/FullscreenDrawParamsTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/FullscreenDrawParamsTest.kt
@@ -20,21 +20,17 @@
 import androidx.test.filters.SmallTest
 import com.android.launcher3.FakeInvariantDeviceProfileTest
 import com.android.quickstep.util.TaskCornerRadius
-import com.android.quickstep.views.TaskView.FullscreenDrawParams
 import com.android.systemui.shared.system.QuickStepContract
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.Mockito.doReturn
-import org.mockito.Mockito.mock
-import org.mockito.Mockito.spy
+import org.mockito.kotlin.mock
 
-/** Test for FullscreenDrawParams class. */
+/** Test for [FullscreenDrawParams] class. */
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class FullscreenDrawParamsTest : FakeInvariantDeviceProfileTest() {
-
     private lateinit var params: FullscreenDrawParams
 
     @Before
@@ -46,115 +42,108 @@
     fun setStartProgress_correctCornerRadiusForTablet() {
         initializeVarsForTablet()
 
-        params.setProgress(
-            /* fullscreenProgress= */ 0f,
-            /* parentScale= */ 1.0f,
-            /* taskViewScale= */ 1.0f
-        )
+        params.setProgress(fullscreenProgress = 0f, parentScale = 1.0f, taskViewScale = 1.0f)
 
         val expectedRadius = TaskCornerRadius.get(context)
-        assertThat(params.currentDrawnCornerRadius).isEqualTo(expectedRadius)
+        assertThat(params.currentCornerRadius).isEqualTo(expectedRadius)
     }
 
     @Test
     fun setFullProgress_correctCornerRadiusForTablet() {
         initializeVarsForTablet()
 
-        params.setProgress(
-            /* fullscreenProgress= */ 1.0f,
-            /* parentScale= */ 1.0f,
-            /* taskViewScale= */ 1.0f
-        )
+        params.setProgress(fullscreenProgress = 1.0f, parentScale = 1f, taskViewScale = 1f)
 
         val expectedRadius = QuickStepContract.getWindowCornerRadius(context)
-        assertThat(params.currentDrawnCornerRadius).isEqualTo(expectedRadius)
+        assertThat(params.currentCornerRadius).isEqualTo(expectedRadius)
     }
 
     @Test
     fun setStartProgress_correctCornerRadiusForPhone() {
         initializeVarsForPhone()
 
-        params.setProgress(
-            /* fullscreenProgress= */ 0f,
-            /* parentScale= */ 1.0f,
-            /* taskViewScale= */ 1.0f
-        )
+        params.setProgress(fullscreenProgress = 0f, parentScale = 1f, taskViewScale = 1f)
 
         val expectedRadius = TaskCornerRadius.get(context)
-        assertThat(params.currentDrawnCornerRadius).isEqualTo(expectedRadius)
+        assertThat(params.currentCornerRadius).isEqualTo(expectedRadius)
     }
 
     @Test
     fun setFullProgress_correctCornerRadiusForPhone() {
         initializeVarsForPhone()
 
-        params.setProgress(
-            /* fullscreenProgress= */ 1.0f,
-            /* parentScale= */ 1.0f,
-            /* taskViewScale= */ 1.0f
-        )
+        params.setProgress(fullscreenProgress = 1.0f, parentScale = 1f, taskViewScale = 1f)
 
         val expectedRadius = QuickStepContract.getWindowCornerRadius(context)
-        assertThat(params.currentDrawnCornerRadius).isEqualTo(expectedRadius)
+        assertThat(params.currentCornerRadius).isEqualTo(expectedRadius)
     }
 
     @Test
     fun setStartProgress_correctCornerRadiusForMultiDisplay() {
-        val display1Context = context
-        val display2Context = mock(Context::class.java)
-        val spyParams = spy(params)
+        val display1Context = mock<Context>()
+        val display2Context = mock<Context>()
+        val display1TaskRadius = TASK_CORNER_RADIUS + 1
+        val display2TaskRadius = TASK_CORNER_RADIUS + 2
 
-        val display1TaskRadius = TaskCornerRadius.get(display1Context)
-        val display1WindowRadius = QuickStepContract.getWindowCornerRadius(display1Context)
-        val display2TaskRadius = display1TaskRadius * 2 + 1 // Arbitrarily different.
-        val display2WindowRadius = display1WindowRadius * 2 + 1 // Arbitrarily different.
-        doReturn(display2TaskRadius).`when`(spyParams).computeTaskCornerRadius(display2Context)
-        doReturn(display2WindowRadius).`when`(spyParams).computeWindowCornerRadius(display2Context)
+        val params =
+            FullscreenDrawParams(
+                context,
+                taskCornerRadiusProvider = { context ->
+                    when (context) {
+                        display1Context -> display1TaskRadius
+                        display2Context -> display2TaskRadius
+                        else -> TASK_CORNER_RADIUS
+                    }
+                },
+                windowCornerRadiusProvider = { 0f },
+            )
 
-        spyParams.updateCornerRadius(display1Context)
-        spyParams.setProgress(
-            /* fullscreenProgress= */ 0f,
-            /* parentScale= */ 1.0f,
-            /* taskViewScale= */ 1.0f
-        )
-        assertThat(spyParams.currentDrawnCornerRadius).isEqualTo(display1TaskRadius)
+        params.setProgress(fullscreenProgress = 0f, parentScale = 1f, taskViewScale = 1f)
+        assertThat(params.currentCornerRadius).isEqualTo(TASK_CORNER_RADIUS)
 
-        spyParams.updateCornerRadius(display2Context)
-        spyParams.setProgress(
-            /* fullscreenProgress= */ 0f,
-            /* parentScale= */ 1.0f,
-            /* taskViewScale= */ 1.0f
-        )
-        assertThat(spyParams.currentDrawnCornerRadius).isEqualTo(display2TaskRadius)
+        params.updateCornerRadius(display1Context)
+        params.setProgress(fullscreenProgress = 0f, parentScale = 1f, taskViewScale = 1f)
+        assertThat(params.currentCornerRadius).isEqualTo(display1TaskRadius)
+
+        params.updateCornerRadius(display2Context)
+        params.setProgress(fullscreenProgress = 0f, parentScale = 1f, taskViewScale = 1f)
+        assertThat(params.currentCornerRadius).isEqualTo(display2TaskRadius)
     }
 
     @Test
     fun setFullProgress_correctCornerRadiusForMultiDisplay() {
-        val display1Context = context
-        val display2Context = mock(Context::class.java)
-        val spyParams = spy(params)
+        val display1Context = mock<Context>()
+        val display2Context = mock<Context>()
+        val display1WindowRadius = WINDOW_CORNER_RADIUS + 1
+        val display2WindowRadius = WINDOW_CORNER_RADIUS + 2
 
-        val display1TaskRadius = TaskCornerRadius.get(display1Context)
-        val display1WindowRadius = QuickStepContract.getWindowCornerRadius(display1Context)
-        val display2TaskRadius = display1TaskRadius * 2 + 1 // Arbitrarily different.
-        val display2WindowRadius = display1WindowRadius * 2 + 1 // Arbitrarily different.
-        doReturn(display2TaskRadius).`when`(spyParams).computeTaskCornerRadius(display2Context)
-        doReturn(display2WindowRadius).`when`(spyParams).computeWindowCornerRadius(display2Context)
+        val params =
+            FullscreenDrawParams(
+                context,
+                taskCornerRadiusProvider = { 0f },
+                windowCornerRadiusProvider = { context ->
+                    when (context) {
+                        display1Context -> display1WindowRadius
+                        display2Context -> display2WindowRadius
+                        else -> WINDOW_CORNER_RADIUS
+                    }
+                },
+            )
 
-        spyParams.updateCornerRadius(display1Context)
-        spyParams.setProgress(
-            /* fullscreenProgress= */ 1.0f,
-            /* parentScale= */ 1.0f,
-            /* taskViewScale= */ 1.0f
-        )
-        assertThat(spyParams.currentDrawnCornerRadius).isEqualTo(display1WindowRadius)
+        params.setProgress(fullscreenProgress = 1f, parentScale = 1f, taskViewScale = 1f)
+        assertThat(params.currentCornerRadius).isEqualTo(WINDOW_CORNER_RADIUS)
 
-        spyParams.updateCornerRadius(display2Context)
-        spyParams.setProgress(
-            /* fullscreenProgress= */ 1.0f,
-            /* parentScale= */ 1.0f,
-            /* taskViewScale= */ 1.0f,
-        )
-        assertThat(spyParams.currentDrawnCornerRadius).isEqualTo(display2WindowRadius)
+        params.updateCornerRadius(display1Context)
+        params.setProgress(fullscreenProgress = 1f, parentScale = 1f, taskViewScale = 1f)
+        assertThat(params.currentCornerRadius).isEqualTo(display1WindowRadius)
+
+        params.updateCornerRadius(display2Context)
+        params.setProgress(fullscreenProgress = 1f, parentScale = 1f, taskViewScale = 1f)
+        assertThat(params.currentCornerRadius).isEqualTo(display2WindowRadius)
+    }
+
+    companion object {
+        const val TASK_CORNER_RADIUS = 56f
+        const val WINDOW_CORNER_RADIUS = 32f
     }
 }
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..a777bd4
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelImplTest.kt
@@ -0,0 +1,294 @@
+/*
+ * 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.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 val recentsViewData = RecentsViewData()
+    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,
+            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 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/multivalentTests/src/com/android/quickstep/util/TaskViewSimulatorTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskViewSimulatorTest.java
index 72cfd92..fa81680 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskViewSimulatorTest.java
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskViewSimulatorTest.java
@@ -202,7 +202,7 @@
                 mDeviceProfile.updateInsets(mLauncherInsets);
 
                 TaskViewSimulator tvs = new TaskViewSimulator(helper.sandboxContext,
-                        FallbackActivityInterface.INSTANCE);
+                        FallbackActivityInterface.INSTANCE, false, 0);
                 tvs.setDp(mDeviceProfile);
 
                 int launcherRotation = info.rotation;
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/InputConsumerUtilsTest.java b/quickstep/tests/src/com/android/quickstep/InputConsumerUtilsTest.java
new file mode 100644
index 0000000..5dc6932
--- /dev/null
+++ b/quickstep/tests/src/com/android/quickstep/InputConsumerUtilsTest.java
@@ -0,0 +1,593 @@
+/*
+ * 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 androidx.test.core.app.ApplicationProvider.getApplicationContext;
+
+import static com.android.quickstep.InputConsumerUtils.newBaseConsumer;
+import static com.android.quickstep.InputConsumerUtils.newConsumer;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Looper;
+import android.view.Choreographer;
+import android.view.MotionEvent;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.anim.AnimatedFloat;
+import com.android.launcher3.taskbar.TaskbarActivityContext;
+import com.android.launcher3.taskbar.TaskbarManager;
+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.BubbleBarViewController;
+import com.android.launcher3.taskbar.bubbles.BubbleControllers;
+import com.android.launcher3.taskbar.bubbles.BubbleCreator;
+import com.android.launcher3.taskbar.bubbles.BubbleDismissController;
+import com.android.launcher3.taskbar.bubbles.BubbleDragController;
+import com.android.launcher3.taskbar.bubbles.BubblePinController;
+import com.android.launcher3.taskbar.bubbles.BubbleStashedHandleViewController;
+import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController;
+import com.android.launcher3.util.LockedUserState;
+import com.android.launcher3.util.MainThreadInitializedObject;
+import com.android.launcher3.views.BaseDragLayer;
+import com.android.quickstep.fallback.window.RecentsWindowManager;
+import com.android.quickstep.inputconsumers.AccessibilityInputConsumer;
+import com.android.quickstep.inputconsumers.BubbleBarInputConsumer;
+import com.android.quickstep.inputconsumers.DeviceLockedInputConsumer;
+import com.android.quickstep.inputconsumers.NavHandleLongPressInputConsumer;
+import com.android.quickstep.inputconsumers.OneHandedModeInputConsumer;
+import com.android.quickstep.inputconsumers.OtherActivityInputConsumer;
+import com.android.quickstep.inputconsumers.OverviewInputConsumer;
+import com.android.quickstep.inputconsumers.OverviewWithoutFocusInputConsumer;
+import com.android.quickstep.inputconsumers.ProgressDelegateInputConsumer;
+import com.android.quickstep.inputconsumers.ResetGestureInputConsumer;
+import com.android.quickstep.inputconsumers.ScreenPinnedInputConsumer;
+import com.android.quickstep.inputconsumers.SysUiOverlayInputConsumer;
+import com.android.quickstep.inputconsumers.TrackpadStatusBarInputConsumer;
+import com.android.quickstep.util.ActiveGestureLog;
+import com.android.quickstep.util.NavBarPosition;
+import com.android.quickstep.views.RecentsViewContainer;
+import com.android.systemui.shared.system.InputChannelCompat;
+import com.android.systemui.shared.system.InputMonitorCompat;
+
+import org.junit.After;
+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 java.util.Optional;
+import java.util.function.Function;
+
+import javax.inject.Provider;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class InputConsumerUtilsTest {
+
+    @NonNull private final MainThreadInitializedObject.SandboxContext mContext =
+            new MainThreadInitializedObject.SandboxContext(getApplicationContext());
+    @NonNull private final TaskAnimationManager mTaskAnimationManager = new TaskAnimationManager(
+            mContext, mock(RecentsWindowManager.class));
+    @NonNull private final InputMonitorCompat mInputMonitorCompat = new InputMonitorCompat("", 0);
+
+    private InputChannelCompat.InputEventReceiver mInputEventReceiver;
+    @Nullable private ResetGestureInputConsumer mResetGestureInputConsumer;
+    @NonNull private Function<GestureState, AnimatedFloat> mSwipeUpProxyProvider = (state) -> null;
+
+    @NonNull @Mock private TaskbarActivityContext mTaskbarActivityContext;
+    @NonNull @Mock private OverviewComponentObserver mOverviewComponentObserver;
+    @NonNull @Mock private RecentsAnimationDeviceState mDeviceState;
+    @NonNull @Mock private AbsSwipeUpHandler.Factory mSwipeUpHandlerFactory;
+    @NonNull @Mock private TaskbarManager mTaskbarManager;
+    @NonNull @Mock private OverviewCommandHelper mOverviewCommandHelper;
+    @NonNull @Mock private GestureState mPreviousGestureState;
+    @NonNull @Mock private GestureState mCurrentGestureState;
+    @NonNull @Mock private LockedUserState mLockedUserState;
+    @NonNull @Mock private TopTaskTracker.CachedTaskInfo mRunningTask;
+    @NonNull @Mock private BaseContainerInterface<?, ?> mContainerInterface;
+    @NonNull @Mock private BaseDragLayer<?> mBaseDragLayer;
+
+    @Rule
+    public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+
+    @Before
+    public void setupMainThreadInitializedObjects() {
+        mContext.putObject(LockedUserState.INSTANCE, mLockedUserState);
+    }
+
+    @Before
+    public void setUpInputEventReceiver() {
+        runOnMainSync(() ->
+                mInputEventReceiver = mInputMonitorCompat.getInputReceiver(
+                        Looper.getMainLooper(),
+                        Choreographer.getInstance(),
+                        event -> {}));
+    }
+
+    @Before
+    public void setUpTaskbarActivityContext() {
+        NavHandle navHandle = mock(NavHandle.class);
+
+        when(navHandle.canNavHandleBeLongPressed()).thenReturn(true);
+
+        when(mTaskbarActivityContext.getDeviceProfile()).thenReturn(new DeviceProfile());
+        when(mTaskbarActivityContext.getNavHandle()).thenReturn(navHandle);
+    }
+
+    @Before
+    public void setUpTaskbarManager() {
+        when(mTaskbarManager.getCurrentActivityContext()).thenReturn(mTaskbarActivityContext);
+    }
+
+    @Before
+    public void setUpResetGestureInputConsumer() {
+        mResetGestureInputConsumer = new ResetGestureInputConsumer(
+                mTaskAnimationManager, mTaskbarManager::getCurrentActivityContext);
+    }
+
+    @Before
+    public void setupLockedUserState() {
+        when(mLockedUserState.isUserUnlocked()).thenReturn(true);
+    }
+
+    @Before
+    public void setupGestureStates() {
+        when(mCurrentGestureState.getRunningTask()).thenReturn(mRunningTask);
+        doReturn(mContainerInterface).when(mCurrentGestureState).getContainerInterface();
+    }
+
+    @Before
+    public void setUpContainerInterface() {
+        RecentsViewContainer recentsViewContainer = mock(RecentsViewContainer.class);
+
+        when(recentsViewContainer.getDragLayer()).thenReturn(mBaseDragLayer);
+        when(recentsViewContainer.getRootView()).thenReturn(mBaseDragLayer);
+        when(recentsViewContainer.asContext()).thenReturn(mContext);
+
+        doReturn(recentsViewContainer).when(mContainerInterface).getCreatedContainer();
+    }
+
+    @Before
+    public void setupBaseDragLayer() {
+        when(mBaseDragLayer.hasWindowFocus()).thenReturn(true);
+    }
+
+    @Before
+    public void setupDeviceState() {
+        when(mDeviceState.canStartTrackpadGesture()).thenReturn(true);
+        when(mDeviceState.canStartSystemGesture()).thenReturn(true);
+        when(mDeviceState.isFullyGesturalNavMode()).thenReturn(true);
+        when(mDeviceState.getNavBarPosition()).thenReturn(mock(NavBarPosition.class));
+        when(mDeviceState.getRotationTouchHelper()).thenReturn(mock(RotationTouchHelper.class));
+    }
+
+    @After
+    public void cleanUp() {
+        mInputMonitorCompat.dispose();
+        mInputEventReceiver.dispose();
+    }
+
+    @Test
+    public void testNewBaseConsumer_onKeyguard_returnsDeviceLockedInputConsumer() {
+        when(mDeviceState.isKeyguardShowingOccluded()).thenReturn(true);
+
+        assertCorrectInputConsumer(
+                this::createBaseInputConsumer,
+                DeviceLockedInputConsumer.class,
+                InputConsumer.TYPE_DEVICE_LOCKED);
+    }
+
+    @Test
+    public void testNewBaseConsumer_onLiveTileModeWithNoContainer_returnsDefaultInputConsumer() {
+        when(mContainerInterface.isInLiveTileMode()).thenReturn(true);
+        when(mContainerInterface.getCreatedContainer()).thenReturn(null);
+
+        assertEqualsDefaultInputConsumer(this::createBaseInputConsumer);
+    }
+
+    @Test
+    public void testNewBaseConsumer_onLiveTileMode_returnsOverviewInputConsumer() {
+        when(mContainerInterface.isInLiveTileMode()).thenReturn(true);
+
+        assertCorrectInputConsumer(
+                this::createBaseInputConsumer,
+                OverviewInputConsumer.class,
+                InputConsumer.TYPE_OVERVIEW);
+    }
+
+    @Test
+    public void testNewBaseConsumer_withNoRunningTask_returnsDefaultInputConsumer() {
+        when(mCurrentGestureState.getRunningTask()).thenReturn(null);
+
+        assertEqualsDefaultInputConsumer(this::createBaseInputConsumer);
+    }
+
+    @Test
+    public void testNewBaseConsumer_prevGestureAnimatingToLauncher_returnsOverviewInputConsumer() {
+        when(mPreviousGestureState.isRunningAnimationToLauncher()).thenReturn(true);
+
+        assertCorrectInputConsumer(
+                this::createBaseInputConsumer,
+                OverviewInputConsumer.class,
+                InputConsumer.TYPE_OVERVIEW);
+    }
+
+    @Test
+    public void testNewBaseConsumer_predictiveBackToHomeInProgress_returnsOverviewInputConsumer() {
+        when(mDeviceState.isPredictiveBackToHomeInProgress()).thenReturn(true);
+
+        assertCorrectInputConsumer(
+                this::createBaseInputConsumer,
+                OverviewInputConsumer.class,
+                InputConsumer.TYPE_OVERVIEW);
+    }
+
+    @Test
+    public void testNewBaseConsumer_resumedThroughShellTransition_returnsOverviewInputConsumer() {
+        when(mContainerInterface.isResumed()).thenReturn(true);
+
+        assertCorrectInputConsumer(
+                this::createBaseInputConsumer,
+                OverviewInputConsumer.class,
+                InputConsumer.TYPE_OVERVIEW);
+    }
+
+    @Test
+    public void testNewBaseConsumer_shellNoWindowFocus_returnsOverviewWithoutFocusInputConsumer() {
+        when(mContainerInterface.isResumed()).thenReturn(true);
+        when(mBaseDragLayer.hasWindowFocus()).thenReturn(false);
+
+        assertCorrectInputConsumer(
+                this::createBaseInputConsumer,
+                OverviewWithoutFocusInputConsumer.class,
+                InputConsumer.TYPE_OVERVIEW_WITHOUT_FOCUS);
+    }
+
+    @Test
+    public void testNewBaseConsumer_forceOverviewInputConsumer_returnsOverviewInputConsumer() {
+        when(mContainerInterface.isResumed()).thenReturn(true);
+        when(mRunningTask.isRootChooseActivity()).thenReturn(true);
+
+        assertCorrectInputConsumer(
+                this::createBaseInputConsumer,
+                OverviewInputConsumer.class,
+                InputConsumer.TYPE_OVERVIEW);
+    }
+
+    @Test
+    public void testNewBaseConsumer_launcherChildActivityResumed_returnsDefaultInputConsumer() {
+        when(mRunningTask.isHomeTask()).thenReturn(true);
+        when(mOverviewComponentObserver.isHomeAndOverviewSame()).thenReturn(true);
+
+        assertEqualsDefaultInputConsumer(this::createBaseInputConsumer);
+    }
+
+    @Test
+    public void testNewBaseConsumer_onGestureBlockedTask_returnsDefaultInputConsumer() {
+        when(mDeviceState.isGestureBlockedTask(any())).thenReturn(true);
+
+        assertEqualsDefaultInputConsumer(this::createBaseInputConsumer);
+    }
+
+    @Test
+    public void testNewBaseConsumer_containsOtherActivityInputConsumer() {
+        // OtherActivityInputConsumer needs to be initialized on the main thread because of
+        // MotionPauseDetector.mForcePauseTimeout
+        assertCorrectInputConsumer(
+                this::createBaseInputConsumer,
+                OtherActivityInputConsumer.class,
+                InputConsumer.TYPE_OTHER_ACTIVITY);
+    }
+
+    @Test
+    public void testNewConsumer_containsOtherActivityInputConsumer() {
+        assertCorrectInputConsumer(
+                this::createInputConsumer,
+                NavHandleLongPressInputConsumer.class,
+                OtherActivityInputConsumer.class,
+                InputConsumer.TYPE_OTHER_ACTIVITY | InputConsumer.TYPE_NAV_HANDLE_LONG_PRESS);
+    }
+
+    @Test
+    public void testNewConsumer_eventCanTriggerAssistantAction_containsAssistantInputConsumer() {
+        when(mDeviceState.canTriggerAssistantAction(any())).thenReturn(true);
+
+        assertCorrectInputConsumer(
+                this::createInputConsumer,
+                NavHandleLongPressInputConsumer.class,
+                OtherActivityInputConsumer.class,
+                InputConsumer.TYPE_OTHER_ACTIVITY
+                        | InputConsumer.TYPE_NAV_HANDLE_LONG_PRESS
+                        | InputConsumer.TYPE_ASSISTANT);
+    }
+
+    @Test
+    public void testNewConsumer_taskbarIsPresent_containsTaskbarUnstashInputConsumer() {
+        DeviceProfile deviceProfile = new DeviceProfile();
+        deviceProfile.isTaskbarPresent = true;
+        when(mTaskbarActivityContext.getDeviceProfile()).thenReturn(deviceProfile);
+
+        assertCorrectInputConsumer(
+                this::createInputConsumer,
+                NavHandleLongPressInputConsumer.class,
+                OtherActivityInputConsumer.class,
+                InputConsumer.TYPE_OTHER_ACTIVITY
+                        | InputConsumer.TYPE_TASKBAR_STASH
+                        | InputConsumer.TYPE_NAV_HANDLE_LONG_PRESS
+                        | InputConsumer.TYPE_CURSOR_HOVER);
+    }
+
+    @Test
+    public void testNewConsumer_whileSystemUiDialogShowing_returnsSysUiOverlayInputConsumer() {
+        when(mDeviceState.isSystemUiDialogShowing()).thenReturn(true);
+
+        assertCorrectInputConsumer(
+                this::createInputConsumer,
+                SysUiOverlayInputConsumer.class,
+                InputConsumer.TYPE_SYSUI_OVERLAY);
+    }
+
+    @Test
+    public void testNewConsumer_onTrackpadGesture_returnsTrackpadStatusBarInputConsumer() {
+        when(mCurrentGestureState.isTrackpadGesture()).thenReturn(true);
+
+        assertCorrectInputConsumer(
+                this::createInputConsumer,
+                TrackpadStatusBarInputConsumer.class,
+                OtherActivityInputConsumer.class,
+                InputConsumer.TYPE_OTHER_ACTIVITY
+                        | InputConsumer.TYPE_NAV_HANDLE_LONG_PRESS
+                        | InputConsumer.TYPE_STATUS_BAR);
+    }
+
+    @Test
+    public void testNewConsumer_whileScreenPinningActive_returnsScreenPinnedInputConsumer() {
+        when(mDeviceState.isScreenPinningActive()).thenReturn(true);
+
+        assertCorrectInputConsumer(
+                this::createInputConsumer,
+                ScreenPinnedInputConsumer.class,
+                InputConsumer.TYPE_SCREEN_PINNED);
+    }
+
+    @Test
+    public void testNewConsumer_canTriggerOneHandedAction_returnsOneHandedModeInputConsumer() {
+        when(mDeviceState.canTriggerOneHandedAction(any())).thenReturn(true);
+
+        assertCorrectInputConsumer(
+                this::createInputConsumer,
+                OneHandedModeInputConsumer.class,
+                OtherActivityInputConsumer.class,
+                InputConsumer.TYPE_OTHER_ACTIVITY
+                        | InputConsumer.TYPE_NAV_HANDLE_LONG_PRESS
+                        | InputConsumer.TYPE_ONE_HANDED);
+    }
+
+    @Test
+    public void testNewConsumer_accessibilityMenuAvailable_returnsAccessibilityInputConsumer() {
+        when(mDeviceState.isAccessibilityMenuAvailable()).thenReturn(true);
+
+        assertCorrectInputConsumer(
+                this::createInputConsumer,
+                AccessibilityInputConsumer.class,
+                OtherActivityInputConsumer.class,
+                InputConsumer.TYPE_OTHER_ACTIVITY
+                        | InputConsumer.TYPE_NAV_HANDLE_LONG_PRESS
+                        | InputConsumer.TYPE_ACCESSIBILITY);
+    }
+
+    @Test
+    public void testNewConsumer_onStashedBubbleBar_returnsBubbleBarInputConsumer() {
+        BubbleControllers bubbleControllers = createBubbleControllers(/* isStashed= */ true);
+
+        when(mTaskbarActivityContext.isBubbleBarEnabled()).thenReturn(true);
+        when(mTaskbarActivityContext.getBubbleControllers()).thenReturn(bubbleControllers);
+
+        assertCorrectInputConsumer(
+                this::createInputConsumer,
+                BubbleBarInputConsumer.class,
+                InputConsumer.TYPE_BUBBLE_BAR);
+    }
+
+    @Test
+    public void testNewConsumer_onVisibleBubbleBar_returnsBubbleBarInputConsumer() {
+        BubbleControllers bubbleControllers = createBubbleControllers(/* isStashed= */ false);
+
+        when(mTaskbarActivityContext.isBubbleBarEnabled()).thenReturn(true);
+        when(mTaskbarActivityContext.getBubbleControllers()).thenReturn(bubbleControllers);
+
+        assertCorrectInputConsumer(
+                this::createInputConsumer,
+                BubbleBarInputConsumer.class,
+                InputConsumer.TYPE_BUBBLE_BAR);
+    }
+
+    @Test
+    public void testNewConsumer_withSwipeUpProxyProvider_returnsProgressDelegateInputConsumer() {
+        mSwipeUpProxyProvider = (state) -> new AnimatedFloat();
+
+        assertCorrectInputConsumer(
+                this::createInputConsumer,
+                ProgressDelegateInputConsumer.class,
+                InputConsumer.TYPE_PROGRESS_DELEGATE);
+    }
+
+    @Test
+    public void testNewConsumer_onLockedState_returnsDeviceLockedInputConsumer() {
+        when(mLockedUserState.isUserUnlocked()).thenReturn(false);
+
+        assertCorrectInputConsumer(
+                this::createInputConsumer,
+                DeviceLockedInputConsumer.class,
+                InputConsumer.TYPE_DEVICE_LOCKED);
+    }
+
+    @Test
+    public void testNewConsumer_cannotStartSysGestureOnLockedState_returnsDefaultInputConsumer() {
+        when(mLockedUserState.isUserUnlocked()).thenReturn(false);
+        when(mDeviceState.canStartSystemGesture()).thenReturn(false);
+
+        assertEqualsDefaultInputConsumer(this::createInputConsumer);
+    }
+
+    @Test
+    public void testNewConsumer_cannotStartTrackGestureOnLockedState_returnsDefaultInputConsumer() {
+        when(mLockedUserState.isUserUnlocked()).thenReturn(false);
+        when(mCurrentGestureState.isTrackpadGesture()).thenReturn(true);
+        when(mDeviceState.canStartTrackpadGesture()).thenReturn(false);
+
+        assertEqualsDefaultInputConsumer(this::createInputConsumer);
+    }
+
+    private InputConsumer createInputConsumer() {
+        MotionEvent event = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 0, 0);
+        InputConsumer inputConsumer = newConsumer(
+                mContext,
+                mContext,
+                mResetGestureInputConsumer,
+                mOverviewComponentObserver,
+                mDeviceState,
+                mPreviousGestureState,
+                mCurrentGestureState,
+                mTaskAnimationManager,
+                mInputMonitorCompat,
+                mSwipeUpHandlerFactory,
+                otherActivityInputConsumer -> {},
+                mInputEventReceiver,
+                mTaskbarManager,
+                mSwipeUpProxyProvider,
+                mOverviewCommandHelper,
+                event);
+
+        event.recycle();
+
+        return inputConsumer;
+    }
+
+    private InputConsumer createBaseInputConsumer() {
+        MotionEvent event = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 0, 0);
+        InputConsumer inputConsumer = newBaseConsumer(
+                mContext,
+                mResetGestureInputConsumer,
+                mOverviewComponentObserver,
+                mDeviceState,
+                mPreviousGestureState,
+                mCurrentGestureState,
+                mTaskAnimationManager,
+                mInputMonitorCompat,
+                mSwipeUpHandlerFactory,
+                otherActivityInputConsumer -> {},
+                mInputEventReceiver,
+                event,
+                ActiveGestureLog.CompoundString.NO_OP);
+
+        event.recycle();
+
+        return inputConsumer;
+    }
+
+    private void assertEqualsDefaultInputConsumer(
+            @NonNull Provider<InputConsumer> inputConsumerProvider) {
+        assertCorrectInputConsumer(
+                inputConsumerProvider,
+                ResetGestureInputConsumer.class,
+                InputConsumer.TYPE_RESET_GESTURE);
+
+        mResetGestureInputConsumer = null;
+
+        runOnMainSync(() -> assertThat(inputConsumerProvider.get()).isEqualTo(InputConsumer.NO_OP));
+    }
+
+    private static void assertCorrectInputConsumer(
+            @NonNull Provider<InputConsumer> inputConsumerProvider,
+            @NonNull Class<? extends InputConsumer> expectedOutputConsumer,
+            int expectedType) {
+        assertCorrectInputConsumer(
+                inputConsumerProvider,
+                expectedOutputConsumer,
+                expectedOutputConsumer,
+                expectedType);
+    }
+
+    private static void assertCorrectInputConsumer(
+            @NonNull Provider<InputConsumer> inputConsumerProvider,
+            @NonNull Class<? extends InputConsumer> expectedOutputConsumer,
+            @NonNull Class<? extends InputConsumer> expectedActiveConsumer,
+            int expectedType) {
+        runOnMainSync(() -> {
+            InputConsumer inputConsumer = inputConsumerProvider.get();
+
+            assertThat(inputConsumer).isInstanceOf(expectedOutputConsumer);
+            assertThat(inputConsumer.getActiveConsumerInHierarchy())
+                    .isInstanceOf(expectedActiveConsumer);
+            assertThat(inputConsumer.getType()).isEqualTo(expectedType);
+        });
+    }
+
+    private static void runOnMainSync(@NonNull Runnable runnable) {
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(runnable);
+    }
+
+    private static BubbleControllers createBubbleControllers(boolean isStashed) {
+        BubbleBarController bubbleBarController = mock(BubbleBarController.class);
+        BubbleBarViewController bubbleBarViewController = mock(BubbleBarViewController.class);
+        BubbleStashController bubbleStashController = mock(BubbleStashController.class);
+        BubbleStashedHandleViewController bubbleStashedHandleViewController =
+                mock(BubbleStashedHandleViewController.class);
+        BubbleDragController bubbleDragController = mock(BubbleDragController.class);
+        BubbleDismissController bubbleDismissController = mock(BubbleDismissController.class);
+        BubbleBarPinController bubbleBarPinController = mock(BubbleBarPinController.class);
+        BubblePinController bubblePinController = mock(BubblePinController.class);
+        BubbleBarSwipeController bubbleBarSwipeController = mock(BubbleBarSwipeController.class);
+        BubbleCreator bubbleCreator = mock(BubbleCreator.class);
+        BubbleControllers bubbleControllers = new BubbleControllers(
+                bubbleBarController,
+                bubbleBarViewController,
+                bubbleStashController,
+                Optional.of(bubbleStashedHandleViewController),
+                bubbleDragController,
+                bubbleDismissController,
+                bubbleBarPinController,
+                bubblePinController,
+                Optional.of(bubbleBarSwipeController),
+                bubbleCreator);
+
+        when(bubbleBarViewController.hasBubbles()).thenReturn(true);
+        when(bubbleStashController.isStashed()).thenReturn(isStashed);
+        when(bubbleStashedHandleViewController.isEventOverHandle(any())).thenReturn(true);
+        when(bubbleBarViewController.isBubbleBarVisible()).thenReturn(!isStashed);
+        when(bubbleBarViewController.isEventOverBubbleBar(any())).thenReturn(true);
+
+        return bubbleControllers;
+    }
+}
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/OrientationTouchTransformerTest.java b/quickstep/tests/src/com/android/quickstep/OrientationTouchTransformerTest.java
index f5d082d..ff0ad53 100644
--- a/quickstep/tests/src/com/android/quickstep/OrientationTouchTransformerTest.java
+++ b/quickstep/tests/src/com/android/quickstep/OrientationTouchTransformerTest.java
@@ -90,7 +90,7 @@
         float landscapeRegionY =
                 generateTouchRegionHeight(NORMAL_SCREEN_SIZE, Surface.ROTATION_90) + 1;
 
-        mTouchTransformer.createOrAddTouchRegion(mInfo);
+        mTouchTransformer.createOrAddTouchRegion(mInfo, "test");
         tapAndAssertTrue(100, portraitRegionY,
                 event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY()));
         tapAndAssertFalse(100, landscapeRegionY,
@@ -102,7 +102,8 @@
 
         // Override region
         mTouchTransformer
-            .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90));
+            .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90),
+                    "test");
         tapAndAssertFalse(100, portraitRegionY,
                 event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY()));
         tapAndAssertTrue(100, landscapeRegionY,
@@ -113,7 +114,7 @@
                 event -> mTouchTransformer.touchInAssistantRegion(event));
 
         // Override region again
-        mTouchTransformer.createOrAddTouchRegion(mInfo);
+        mTouchTransformer.createOrAddTouchRegion(mInfo, "test");
         tapAndAssertTrue(100, portraitRegionY,
                 event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY()));
         tapAndAssertFalse(100, landscapeRegionY,
@@ -132,7 +133,8 @@
                 generateTouchRegionHeight(NORMAL_SCREEN_SIZE, Surface.ROTATION_90) + 1;
 
         mTouchTransformer
-            .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90));
+            .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90),
+                    "test");
         tapAndAssertFalse(100, portraitRegionY,
                 event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY()));
         tapAndAssertTrue(100, landscapeRegionY,
@@ -144,7 +146,7 @@
         // We have to add 0 rotation second so that gets set as the current rotation, otherwise
         // matrix transform will fail (tests only work in Portrait at the moment)
         mTouchTransformer.enableMultipleRegions(true, mInfo);
-        mTouchTransformer.createOrAddTouchRegion(mInfo);
+        mTouchTransformer.createOrAddTouchRegion(mInfo, "test");
 
         tapAndAssertTrue(100, portraitRegionY,
                 event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY()));
@@ -165,8 +167,9 @@
 
         mTouchTransformer.enableMultipleRegions(true, mInfo);
         mTouchTransformer
-            .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90));
-        mTouchTransformer.createOrAddTouchRegion(mInfo);
+            .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90),
+                    "test");
+        mTouchTransformer.createOrAddTouchRegion(mInfo, "test");
         tapAndAssertTrue(0, portraitRegionY,
                 event -> mTouchTransformer.touchInAssistantRegion(event));
         tapAndAssertFalse(0, landscapeRegionY,
@@ -181,9 +184,10 @@
                 generateTouchRegionHeight(NORMAL_SCREEN_SIZE, Surface.ROTATION_90) + 1;
 
         mTouchTransformer.enableMultipleRegions(true, mInfo);
-        mTouchTransformer.createOrAddTouchRegion(mInfo);
+        mTouchTransformer.createOrAddTouchRegion(mInfo, "test");
         mTouchTransformer
-            .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90));
+            .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90),
+                    "test");
         mTouchTransformer.enableMultipleRegions(false, mInfo);
         tapAndAssertTrue(0, portraitRegionY,
                 event -> mTouchTransformer.touchInAssistantRegion(event));
@@ -213,14 +217,14 @@
 
     @Test
     public void applyTransform_taskNotFrozen_notInRegion() {
-        mTouchTransformer.createOrAddTouchRegion(mInfo);
+        mTouchTransformer.createOrAddTouchRegion(mInfo, "test");
         tapAndAssertFalse(100, 100,
                 event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY()));
     }
 
     @Test
     public void applyTransform_taskFrozen_noRotate_outOfRegion() {
-        mTouchTransformer.createOrAddTouchRegion(mInfo);
+        mTouchTransformer.createOrAddTouchRegion(mInfo, "test");
         mTouchTransformer.enableMultipleRegions(true, mInfo);
         tapAndAssertFalse(100, 100,
                 event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY()));
@@ -228,7 +232,7 @@
 
     @Test
     public void applyTransform_taskFrozen_noRotate_inRegion() {
-        mTouchTransformer.createOrAddTouchRegion(mInfo);
+        mTouchTransformer.createOrAddTouchRegion(mInfo, "test");
         mTouchTransformer.enableMultipleRegions(true, mInfo);
         float y = generateTouchRegionHeight(NORMAL_SCREEN_SIZE, Surface.ROTATION_0) + 1;
         tapAndAssertTrue(100, y,
@@ -237,7 +241,7 @@
 
     @Test
     public void applyTransform_taskNotFrozen_noRotate_inDefaultRegion() {
-        mTouchTransformer.createOrAddTouchRegion(mInfo);
+        mTouchTransformer.createOrAddTouchRegion(mInfo, "test");
         float y = generateTouchRegionHeight(NORMAL_SCREEN_SIZE, Surface.ROTATION_0) + 1;
         tapAndAssertTrue(100, y,
                 event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY()));
@@ -246,7 +250,8 @@
     @Test
     public void applyTransform_taskNotFrozen_90Rotate_inRegion() {
         mTouchTransformer
-            .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90));
+            .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90),
+                    "test");
         float y = generateTouchRegionHeight(NORMAL_SCREEN_SIZE, Surface.ROTATION_90) + 1;
         tapAndAssertTrue(100, y,
                 event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY()));
@@ -254,10 +259,11 @@
 
     @Test
     public void applyTransform_taskNotFrozen_90Rotate_withTwoRegions() {
-        mTouchTransformer.createOrAddTouchRegion(mInfo);
+        mTouchTransformer.createOrAddTouchRegion(mInfo, "test");
         mTouchTransformer.enableMultipleRegions(true, mInfo);
         mTouchTransformer
-            .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90));
+            .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90),
+                    "test");
         // Landscape point
         float y1 = generateTouchRegionHeight(NORMAL_SCREEN_SIZE, Surface.ROTATION_90) + 1;
         MotionEvent inRegion1_down = generateMotionEvent(MotionEvent.ACTION_DOWN, 10, y1);
@@ -278,10 +284,11 @@
     @Test
     public void applyTransform_90Rotate_inRotatedRegion() {
         // Create regions for both 0 Rotation and 90 Rotation
-        mTouchTransformer.createOrAddTouchRegion(mInfo);
+        mTouchTransformer.createOrAddTouchRegion(mInfo, "test");
         mTouchTransformer.enableMultipleRegions(true, mInfo);
         mTouchTransformer
-                .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90));
+                .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90),
+                        "test");
         // Portrait point in landscape orientation axis
         float x1 = generateTouchRegionHeight(NORMAL_SCREEN_SIZE, Surface.ROTATION_0);
         // bottom of screen, from landscape perspective right side of screen
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..6560632
--- /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_vertical_inset"
+            android:end="@dimen/all_apps_tabs_focus_horizontal_inset"
+            android:start="@dimen/all_apps_tabs_focus_horizontal_inset"
+            android:top="@dimen/all_apps_tabs_focus_vertical_inset">
+        <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..ce7b334
--- /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_vertical_inset"
+         android:end="@dimen/all_apps_tabs_focus_horizontal_inset"
+         android:start="@dimen/all_apps_tabs_focus_horizontal_inset"
+         android:top="@dimen/all_apps_tabs_focus_vertical_inset">
+        <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/add_item_confirmation_activity.xml b/res/layout/add_item_confirmation_activity.xml
index d113a38..2bb2eb3 100644
--- a/res/layout/add_item_confirmation_activity.xml
+++ b/res/layout/add_item_confirmation_activity.xml
@@ -71,7 +71,8 @@
                 android:id="@+id/widget_preview_scroll_view"
                 android:layout_width="match_parent"
                 android:layout_height="0dp"
-                android:layout_marginVertical="16dp"
+                android:layout_margin="16dp"
+                android:background="@drawable/widgets_surface_background"
                 android:layout_weight="1">
 
                 <include
diff --git a/res/layout/all_apps_personal_work_tabs.xml b/res/layout/all_apps_personal_work_tabs.xml
index e04b207..b6a8ed8 100644
--- a/res/layout/all_apps_personal_work_tabs.xml
+++ b/res/layout/all_apps_personal_work_tabs.xml
@@ -21,8 +21,6 @@
     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: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_full_sheet_paged_view.xml b/res/layout/widgets_full_sheet_paged_view.xml
index 622f0d6..7c57726 100644
--- a/res/layout/widgets_full_sheet_paged_view.xml
+++ b/res/layout/widgets_full_sheet_paged_view.xml
@@ -81,6 +81,7 @@
             android:layout_marginTop="8dp"
             android:layout_marginBottom="8dp"
             android:background="@drawable/widgets_surface_background"
+            android:clipToOutline="true"
             android:orientation="vertical"
             android:layout_marginHorizontal="@dimen/widget_list_horizontal_margin"
             android:visibility="gone">
diff --git a/res/layout/widgets_full_sheet_recyclerview.xml b/res/layout/widgets_full_sheet_recyclerview.xml
index 5427732..1ce1c55 100644
--- a/res/layout/widgets_full_sheet_recyclerview.xml
+++ b/res/layout/widgets_full_sheet_recyclerview.xml
@@ -64,6 +64,7 @@
             android:layout_marginTop="8dp"
             android:layout_marginBottom="8dp"
             android:background="@drawable/widgets_surface_background"
+            android:clipToOutline="true"
             android:orientation="vertical"
             android:visibility="gone">
             <include layout="@layout/widget_recommendations" />
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..cf090ad 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
@@ -143,6 +133,7 @@
                                 android:layout_width="match_parent"
                                 android:layout_height="match_parent"
                                 android:background="@drawable/widgets_surface_background"
+                                android:clipToOutline="true"
                                 android:orientation="vertical"
                                 android:visibility="gone">
                                 <include layout="@layout/widget_recommendations" />
diff --git a/res/layout/widgets_two_pane_sheet_paged_view.xml b/res/layout/widgets_two_pane_sheet_paged_view.xml
index 71c77b5..0528d3b 100644
--- a/res/layout/widgets_two_pane_sheet_paged_view.xml
+++ b/res/layout/widgets_two_pane_sheet_paged_view.xml
@@ -15,7 +15,7 @@
 <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"
@@ -23,40 +23,17 @@
         android:layout_gravity="start"
         android:clipChildren="false"
         android:clipToPadding="false"
-        android:layout_alignParentStart="true">
-        <!-- 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>
-
+        android:layout_alignParentStart="true"
+        android:orientation="vertical">
         <!-- SearchAndRecommendationsView without the tab layout as well -->
         <!-- Note: the horizontal padding matches with the WidgetPagedView -->
-        <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:elevation="1dp"
+            android:background="?attr/widgetPickerPrimarySurfaceColor"
             android:paddingHorizontal="@dimen/widget_list_horizontal_margin_two_pane"
             android:orientation="vertical">
 
@@ -67,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"
@@ -98,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">
@@ -140,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 e5c1b61..0886f50 100644
--- a/res/values-af/strings.xml
+++ b/res/values-af/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"Wys almal"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"Wys alle legstukke"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"Wys tans alle legstukke"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"Landskapmodus"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"Stel foon op landskapmodus"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"Skedule van werkapps"</string>
     <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 53dc4ba..576fefc 100644
--- a/res/values-am/strings.xml
+++ b/res/values-am/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"ሁሉንም አሳይ"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"ሁሉንም ምግብሮች አሳይ"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"ሁሉንም ምግብሮች በማሳየት ላይ"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"የመሬት አቀማመጥ ሁኔታ"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"ስልክን ወደ የመሬት አቀማመጥ ሁኔታ ያቀናብሩ"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"የሥራ መተግበሪያዎች መርሐግብር"</string>
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"አጣራ"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"አልተሳካም፦ <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"የግል ቦታ"</string>
diff --git a/res/values-ar/strings.xml b/res/values-ar/strings.xml
index b15d525..b902aca 100644
--- a/res/values-ar/strings.xml
+++ b/res/values-ar/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"عرض الكل"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"عرض كل التطبيقات المصغّرة"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"جارٍ عرض كل التطبيقات المصغّرة"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"الوضع الأفقي"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"ضبط الهاتف على الوضع الأفقي"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"الجدول الزمني لتطبيقات العمل"</string>
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"فلتر"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"تعذَّر <xliff:g id="WHAT">%1$s</xliff:g>."</string>
     <string name="private_space_label" msgid="2359721649407947001">"مساحة خاصة"</string>
diff --git a/res/values-as/strings.xml b/res/values-as/strings.xml
index d8783a6..128c846 100644
--- a/res/values-as/strings.xml
+++ b/res/values-as/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"আটাইবোৰ দেখুৱাওক"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"আটাইবোৰ ৱিজেট দেখুৱাওক"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"আটাইবোৰ ৱিজেট দেখুৱাই থকা হৈছে"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"লেণ্ডস্কেইপ ম’ড"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"ফ’নটো লেণ্ডস্কেইপ ম’ডলৈ ছেট কৰক"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"কাম সম্পৰ্কীয় এপৰ সময়সূচী"</string>
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"ফিল্টাৰ"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"বিফল: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"প্ৰাইভেট স্পে\'চ"</string>
diff --git a/res/values-az/strings.xml b/res/values-az/strings.xml
index 5b86fac..96447b4 100644
--- a/res/values-az/strings.xml
+++ b/res/values-az/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"Hamısını göstər"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"Bütün vidcetləri göstərin"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"Bütün vidcetlər göstərilir"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"Landşaft rejimi"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"Telefonu landşaft rejiminə ayarlayın"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"İş tətbiqləri cədvəli"</string>
     <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 002c800..5408aaf 100644
--- a/res/values-b+sr+Latn/strings.xml
+++ b/res/values-b+sr+Latn/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"Prikaži sve"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"Prikažite sve vidžete"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"Prikazuju se svi vidžeti"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"Vodoravni režim"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"Podesite telefon na vodoravni režim"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"Raspored za poslovne aplikacije"</string>
     <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 0984f32..cf5e7f8 100644
--- a/res/values-be/strings.xml
+++ b/res/values-be/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"Паказаць усе"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"Паказаць усе віджэты"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"Паказаны ўсе віджэты"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"Альбомная арыентацыя"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"Перавядзіце тэлефон у альбомную арыентацыю"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"Расклад працоўных праграм"</string>
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Фільтр"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Не ўдалося: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Прыватная прастора"</string>
diff --git a/res/values-bg/strings.xml b/res/values-bg/strings.xml
index b321b42..fd4c983 100644
--- a/res/values-bg/strings.xml
+++ b/res/values-bg/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"Вижте всички"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"Показване на всички приспособления"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"Показват се всички приспособления"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"Хоризонтален режим"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"Поставете телефона в хоризонтален режим"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"График за служебните приложения"</string>
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Филтър"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Неуспешно: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Частно пространство"</string>
diff --git a/res/values-bn/strings.xml b/res/values-bn/strings.xml
index 0722e84..56461a3 100644
--- a/res/values-bn/strings.xml
+++ b/res/values-bn/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"সব দেখুন"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"সব উইজেট দেখুন"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"সব উইজেট দেখানো হচ্ছে"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"ভূদৃশ্য মোড"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"ভূদৃশ্য মোডে ফোন সেট করুন"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"অফিসের অ্যাপের শিডিউল"</string>
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"ফিল্টার"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"কাজটি করা যায়নি: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"ব্যক্তিগত স্পেস"</string>
diff --git a/res/values-bs/strings.xml b/res/values-bs/strings.xml
index 2b168f6..803c6c0 100644
--- a/res/values-bs/strings.xml
+++ b/res/values-bs/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"Prikaži sve"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"Prikaz svih vidžeta"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"Prikazivanje svih vidžeta"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"Vodoravni način"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"Postavite telefon u vodoravni način"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"Raspored poslovnih aplikacija"</string>
     <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 6d30ec1..0414bb2 100644
--- a/res/values-ca/strings.xml
+++ b/res/values-ca/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"Mostra-ho tot"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"Mostra tots els widgets"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"S\'estan mostrant tots els widgets"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"Mode horitzontal"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"Posa el telèfon en mode horitzontal"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"Programació de les aplicacions de treball"</string>
     <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 732343d..aa50a42 100644
--- a/res/values-cs/strings.xml
+++ b/res/values-cs/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"Zobrazit vše"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"Zobrazit všechny widgety"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"Zobrazují se všechny widgety"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"Režim na šířku"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"Nastavit telefon do režimu na šířku"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"Plán pracovních aplikací"</string>
     <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 9211e76..0410763 100644
--- a/res/values-da/strings.xml
+++ b/res/values-da/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"Vis alle"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"Vis alle widgets"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"Viser alle widgets"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"Liggende format"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"Indstil telefonen til liggende format"</string>
     <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>
@@ -142,7 +149,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>
@@ -174,7 +182,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"Tidsplan for arbejdsapps"</string>
     <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 c90cf84..cb63ff0 100644
--- a/res/values-de/strings.xml
+++ b/res/values-de/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"Alle anzeigen"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"Alle Widgets anzeigen"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"Es werden alle Widgets angezeigt"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"Querformat"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"Smartphone auf Querformat einstellen"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"Zeitplan für geschäftliche Apps"</string>
     <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 cd1a1e0..79d9f19 100644
--- a/res/values-el/strings.xml
+++ b/res/values-el/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"Εμφάνιση όλων"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"Εμφάνιση συνόλου γραφικών στοιχείων"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"Εμφάνιση όλων των γραφικών στοιχείων"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"Οριζόντιος προσανατολισμός"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"Ορισμός τηλεφώνου σε οριζόντιο προσανατολισμό"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"Πρόγραμμα εφαρμογών εργασιών"</string>
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Φίλτρο"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Αποτυχία: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Ιδιωτικός χώρος"</string>
diff --git a/res/values-en-rAU/strings.xml b/res/values-en-rAU/strings.xml
index f7b04a3..a58ace8 100644
--- a/res/values-en-rAU/strings.xml
+++ b/res/values-en-rAU/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,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>
@@ -124,6 +129,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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <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-rCA/strings.xml b/res/values-en-rCA/strings.xml
index 8feccb0..e2d072b 100644
--- a/res/values-en-rCA/strings.xml
+++ b/res/values-en-rCA/strings.xml
@@ -32,6 +32,7 @@
     <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="manage_windows_option_taskbar" msgid="2294109489960654212">"Manage Windows"</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>
@@ -69,6 +70,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>
@@ -124,6 +128,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>
@@ -142,7 +148,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>
@@ -187,6 +194,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 f7b04a3..a58ace8 100644
--- a/res/values-en-rGB/strings.xml
+++ b/res/values-en-rGB/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,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>
@@ -124,6 +129,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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <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-rIN/strings.xml b/res/values-en-rIN/strings.xml
index f7b04a3..a58ace8 100644
--- a/res/values-en-rIN/strings.xml
+++ b/res/values-en-rIN/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,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>
@@ -124,6 +129,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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <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-es-rUS/strings.xml b/res/values-es-rUS/strings.xml
index 0125ae5..b0b06b0 100644
--- a/res/values-es-rUS/strings.xml
+++ b/res/values-es-rUS/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"Mostrar todos"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"Mostrar todos los widgets"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"Mostrando todos los widgets"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"Modo horizontal"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"Establecer el teléfono en modo horizontal"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"Programa de las apps de trabajo"</string>
     <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 ddcee65..0062b2f 100644
--- a/res/values-es/strings.xml
+++ b/res/values-es/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"Mostrar todo"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"Mostrar todos los widgets"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"Mostrando todos los widgets"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"Modo de vista horizontal"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"Pon el teléfono en modo de vista horizontal"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"Horario de aplicaciones de trabajo"</string>
     <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 289b9d9..5eccca9 100644
--- a/res/values-et/strings.xml
+++ b/res/values-et/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"Kuva kõik"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"Kuva kõik vidinad"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"Kõik vidinad on kuvatud"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"Horisontaalrežiim"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"Sea telefon horisontaalrežiimi"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"Töörakenduste ajakava"</string>
     <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 045ae12..8d0499c 100644
--- a/res/values-eu/strings.xml
+++ b/res/values-eu/strings.xml
@@ -32,9 +32,11 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"Erakutsi guztiak"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"Erakutsi widget guztiak"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"Widget guztiak erakusten"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"Ikuspegi horizontala"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"Ezarri telefonoa ikuspegi horizontalean"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"Laneko aplikazioen programazioa"</string>
     <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 7c6e373..18a2038 100644
--- a/res/values-fa/strings.xml
+++ b/res/values-fa/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"نمایش همه"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"نمایش همه ابزاره‌ها"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"درحال نمایش دادن همه ابزاره‌ها"</string>
     <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 +107,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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"حالت افقی"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"تنظیم تلفن روی حالت افقی"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"برنامه زمانی برنامه‌های کاری"</string>
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"فیلتر"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"ناموفق بود: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"فضای خصوصی"</string>
diff --git a/res/values-fi/strings.xml b/res/values-fi/strings.xml
index c61c85a..f04fa62 100644
--- a/res/values-fi/strings.xml
+++ b/res/values-fi/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"Näytä kaikki"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"Näytä kaikki widgetit"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"Näytetään kaikki widgetit"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"Vaakasuunta"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"Aseta puhelin vaakasuuntaan"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"Työsovellusten aikataulu"</string>
     <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 cae77dc..6f26dae 100644
--- a/res/values-fr-rCA/strings.xml
+++ b/res/values-fr-rCA/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"Tout afficher"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"Afficher tous les widgets"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"Tous les widgets affichés"</string>
     <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"Touchez pour modifier les paramètres du widget"</string>
     <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"Modifier les paramètres du widget"</string>
     <string name="all_apps_search_bar_hint" msgid="1390553134053255246">"Rechercher dans les applis"</string>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"Mode paysage"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"Configurer le téléphone en mode paysage"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,12 +195,13 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"Horaire des applis professionnelles"</string>
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Filtrer"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Échec : <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <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 6cbc921..bd1f23c 100644
--- a/res/values-fr/strings.xml
+++ b/res/values-fr/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"Tout afficher"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"Afficher tous les widgets"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"Afficher tous les widgets"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"Mode Paysage"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"Placez le téléphone en mode Paysage"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"Planifier l\'activation des applis pros"</string>
     <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 e6d6442..9eae629 100644
--- a/res/values-gl/strings.xml
+++ b/res/values-gl/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"Mostrar todo"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"Mostrar todos os widgets"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"Mostrando todos os widgets"</string>
     <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>
@@ -120,10 +125,12 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"Modo horizontal"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"Pon o teléfono no modo horizontal"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"Horario das aplicacións do traballo"</string>
     <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 aca3054..84f4ce6 100644
--- a/res/values-gu/strings.xml
+++ b/res/values-gu/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"બધા બતાવો"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"બધા વિજેટ બતાવો"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"બધા વિજેટ બતાવી રહ્યાં છીએ"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"લૅન્ડસ્કેપ મોડ"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"ફોનને લૅન્ડસ્કેપ મોડમાં સેટ કરો"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"ઑફિસ માટેની ઍપનું શેડ્યૂલ"</string>
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"ફિલ્ટર કરો"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"નિષ્ફળ: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"ખાનગી સ્પેસ"</string>
diff --git a/res/values-hi/strings.xml b/res/values-hi/strings.xml
index 00726ea..110e3af 100644
--- a/res/values-hi/strings.xml
+++ b/res/values-hi/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"सभी दिखाएं"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"सभी विजेट दिखाएं"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"सभी विजेट दिखाए जा रहे हैं"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"लैंडस्केप मोड"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"फ़ोन को लैंडस्केप मोड में सेट करें"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"वर्क ऐप्लिकेशन के लिए शेड्यूल"</string>
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"फ़िल्टर"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"पूरा नहीं हुआ: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"प्राइवेट स्पेस"</string>
diff --git a/res/values-hr/strings.xml b/res/values-hr/strings.xml
index a9fd14e..06a8d06 100644
--- a/res/values-hr/strings.xml
+++ b/res/values-hr/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"Prikaži sve"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"Prikaži sve widgete"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"Prikazuju se svi widgeti"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"Pejzažni način"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"Postavljanje telefona u pejzažni način"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"Raspored za poslovne aplikacije"</string>
     <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 a0c089e..6228678 100644
--- a/res/values-hu/strings.xml
+++ b/res/values-hu/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"Az összes megjelenítése"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"Minden modul mutatása"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"Összes modul megjelenítése…"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"Fekvő tájolás"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"Állítsa a telefont fekvő tájolásúra"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"Munkahelyi alkalmazások ütemezése"</string>
     <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 ba2edb1..010d259 100644
--- a/res/values-hy/strings.xml
+++ b/res/values-hy/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"Բոլորը"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"Ցույց տալ բոլոր վիջեթները"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"Բոլոր վիջեթները ցուցադրված են"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"Հորիզոնական"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"Հեռախոսն օգտագործել հորիզոնական ռեժիմում"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,9 +195,10 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"Աշխատանքային հավելվածների ժամանակացույց"</string>
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Զտեք"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Չհաջողվեց կատարել գործողությունը (<xliff:g id="WHAT">%1$s</xliff:g>)"</string>
-    <string name="private_space_label" msgid="2359721649407947001">"Անձնական տարածք"</string>
+    <string name="private_space_label" msgid="2359721649407947001">"Մասնավոր տարածք"</string>
     <string name="private_space_secondary_label" msgid="9203933341714508907">"Հպեք կարգավորելու կամ բացելու համար"</string>
     <string name="ps_container_title" msgid="4391796149519594205">"Մասնավոր"</string>
     <string name="ps_container_settings" msgid="6059734123353320479">"Անձնական տարածքի կարգավորումներ"</string>
diff --git a/res/values-in/strings.xml b/res/values-in/strings.xml
index 40f2e57..027a111 100644
--- a/res/values-in/strings.xml
+++ b/res/values-in/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"Tampilkan semua"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"Tampilkan semua widget"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"Menampilkan semua widget"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"Mode lanskap"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"Setel ponsel ke mode lanskap"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"Jadwal aplikasi kerja"</string>
     <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 0014317..c2f8f5a 100644
--- a/res/values-is/strings.xml
+++ b/res/values-is/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"Sýna allt"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"Sýna allar græjur"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"Sýnir allar græjur"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"Langsnið"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"Stilla síma á langsnið"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"Áætlun vinnuforrita"</string>
     <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 731f839..34d2f57 100644
--- a/res/values-it/strings.xml
+++ b/res/values-it/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"Mostra tutto"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"Mostra tutti i widget"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"Visualizzazione di tutti i widget"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"Modalità Orizzontale"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"Imposta lo smartphone in modalità Orizzontale"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,12 +195,13 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"Programmazione app di lavoro"</string>
     <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 82eb5f8..4eb3e96 100644
--- a/res/values-iw/strings.xml
+++ b/res/values-iw/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"הצגת הכול"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"הצגת כל הווידג\'טים"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"כל הווידג\'טים מוצגים"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"פריסה לרוחב"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"העברת הטלפון לפריסה לרוחב"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"לוח זמנים להשהיית אפליקציות לעבודה"</string>
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"סינון"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"הפעולה נכשלה: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"מרחב פרטי"</string>
diff --git a/res/values-ja/strings.xml b/res/values-ja/strings.xml
index fc89041..e11a22c 100644
--- a/res/values-ja/strings.xml
+++ b/res/values-ja/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"すべて表示"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"すべてのウィジェットを表示"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"すべてのウィジェットを表示しています"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"横表示"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"スマートフォンを横表示にしてください"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"仕事用アプリのスケジュール"</string>
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"フィルタ"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"失敗: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"プライベート スペース"</string>
diff --git a/res/values-ka/strings.xml b/res/values-ka/strings.xml
index f099bcd..c702c62 100644
--- a/res/values-ka/strings.xml
+++ b/res/values-ka/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"ყველას ჩვენება"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"ყველა ვიჯეტის ჩვენება"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"ნაჩვენებია ყველა ვიჯეტი"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"პეიზაჟის რეჟიმი"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"ტელეფონის დაყენება პეიზაჟის რეჟიმში"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,11 +195,12 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"სამსახურის აპების განრიგი"</string>
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"ფილტრი"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"ვერ მოხერხდა: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"პირადი სივრცე"</string>
     <string name="private_space_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 ebaacc9..605a602 100644
--- a/res/values-kk/strings.xml
+++ b/res/values-kk/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"Барлығын көру"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"Барлық виджетті көрсету"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"Барлық виджет көрсетіліп тұр."</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"Альбом режимі"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"Телефонды альбом режиміне қою"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"Жұмыс қолданбаларының кестесі"</string>
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Сүзгі"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Қате шықты: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Құпия кеңістік"</string>
diff --git a/res/values-km/strings.xml b/res/values-km/strings.xml
index b05eeb0..f6d0f37 100644
--- a/res/values-km/strings.xml
+++ b/res/values-km/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"បង្ហាញ​ទាំងអស់"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"បង្ហាញធាតុ​ក្រាហ្វិកទាំងអស់"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"កំពុងបង្ហាញធាតុ​ក្រាហ្វិកទាំងអស់"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"ផ្ដេក"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"កំណត់ទូរសព្ទទៅផ្ដេក"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"កាលវិភាគកម្មវិធី​ការងារ"</string>
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"តម្រង"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"បានបរាជ័យ៖ <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"បន្ទប់​ឯកជន"</string>
diff --git a/res/values-kn/strings.xml b/res/values-kn/strings.xml
index 91530b5..bc9f293 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>
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"ಎಲ್ಲಾ ತೋರಿಸಿ"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"ಎಲ್ಲಾ ವಿಜೆಟ್‌ಗಳನ್ನು ತೋರಿಸಿ"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"ಎಲ್ಲಾ ವಿಜೆಟ್‌ಗಳನ್ನು ತೋರಿಸಲಾಗುತ್ತಿದೆ"</string>
     <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>
@@ -103,7 +108,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>
@@ -120,10 +125,12 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"ಲ್ಯಾಂಡ್‌ಸ್ಕೇಪ್ ಮೋಡ್"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"ಫೋನ್ ಅನ್ನು ಲ್ಯಾಂಡ್‌ಸ್ಕೇಪ್ ಮೋಡ್‌ಗೆ ಸೆಟ್ ಮಾಡಿ"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"ಕೆಲಸಕ್ಕೆ ಸಂಬಂಧಿಸಿದ ಆ್ಯಪ್‌ಗಳ ವೇಳಾಪಟ್ಟಿ"</string>
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"ಫಿಲ್ಟರ್‌"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"ವಿಫಲವಾಗಿದೆ: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"ಖಾಸಗಿ ಸ್ಪೇಸ್"</string>
diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml
index 0e7ef7b..7748a32 100644
--- a/res/values-ko/strings.xml
+++ b/res/values-ko/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"모두 표시"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"모든 위젯 표시"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"모든 위젯 표시"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"가로 모드"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"휴대전화를 가로 모드로 설정"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"직장 앱 일정"</string>
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"필터"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"실패: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"비공개 스페이스"</string>
diff --git a/res/values-ky/strings.xml b/res/values-ky/strings.xml
index 753d2dd..d3cf7e4 100644
--- a/res/values-ky/strings.xml
+++ b/res/values-ky/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"Баарын көрсөтүү"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"Виджеттин баарын көрсөтүү"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"Бардык виджеттерди көрсөтүү"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"Туурасынан"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"Телефонду туурасынан коюңуз"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"Жумуш колдонмолорунун графиги"</string>
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Чыпкалоо"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Аткарылган жок: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Жеке мейкиндик"</string>
diff --git a/res/values-lo/strings.xml b/res/values-lo/strings.xml
index 9ddf0b3..10611c1 100644
--- a/res/values-lo/strings.xml
+++ b/res/values-lo/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"ສະແດງທັງໝົດ"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"ສະແດງວິດເຈັດທັງໝົດ"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"ກໍາລັງສະແດງວິດເຈັດທັງໝົດ"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"ໂໝດແນວນອນ"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"ຕັ້ງຄ່າໂທລະສັບເປັນໂໝດແນວນອນ"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"ກຳນົດເວລາຂອງແອັບບ່ອນເຮັດວຽກ"</string>
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"ກັ່ນຕອງ"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"ບໍ່ສຳເລັດ: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"ພື້ນທີ່ສ່ວນຕົວ"</string>
diff --git a/res/values-lt/strings.xml b/res/values-lt/strings.xml
index 6879862..c6c379a 100644
--- a/res/values-lt/strings.xml
+++ b/res/values-lt/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"Rodyti viską"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"Rodyti visus valdiklius"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"Rodomi visi valdikliai"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"Gulsčiojo ekrano režimas"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"Nustatykite telefoną į gulsčiojo ekrano režimą"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"Darbo programų tvarkaraštis"</string>
     <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 8166bce..0dc7445 100644
--- a/res/values-lv/strings.xml
+++ b/res/values-lv/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"Rādīt visus"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"Rādīt visus logrīkus"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"Tiek rādīti visi logrīki"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"Ainavas režīms"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"Iestatīt tālrunī ainavas režīmu"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"Darba lietotņu grafiks"</string>
     <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 0511c3f..c5518bc 100644
--- a/res/values-mk/strings.xml
+++ b/res/values-mk/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"Прикажи ги сите"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"Прикажи ги сите виџети"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"Се прикажуваат сите виџети"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"Хоризонтален режим"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"Поставете го телефонот во „Хоризонтален режим“"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"Распоред на работните апликации"</string>
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Филтер"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Не успеа: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Приватен простор"</string>
diff --git a/res/values-ml/strings.xml b/res/values-ml/strings.xml
index b9e68e0..756c065 100644
--- a/res/values-ml/strings.xml
+++ b/res/values-ml/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"എല്ലാം കാണിക്കൂ"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"എല്ലാ വിജറ്റും കാണിക്കുക"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"എല്ലാ വിജറ്റുകളും കാണിക്കുന്നു"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"ലാൻഡ്‌സ്‌കേപ്പ് മോഡ്"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"ഫോൺ ലാൻഡ്‌സ്‌കേപ്പ് മോഡിലേക്ക് സജ്ജീകരിക്കുക"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"ഔദ്യോഗിക ആപ്പുകൾക്കുള്ള ഷെഡ്യൂൾ"</string>
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"ഫിൽട്ടർ ചെയ്യുക"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"പരാജയപ്പെട്ടു: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"സ്വകാര്യ സ്പേസ്"</string>
diff --git a/res/values-mn/strings.xml b/res/values-mn/strings.xml
index 28a1438..aad461b 100644
--- a/res/values-mn/strings.xml
+++ b/res/values-mn/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"Бүгдийг харуул"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"Бүх виджетийг харуулах"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"Бүх виджетийг харуулж байна"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"Хэвтээ горим"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"Утсыг хэвтээ горимд тохируулах"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"Ажлын аппуудын хуваарь"</string>
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Шүүлтүүр"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Амжилтгүй болсон: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Хувийн орон зай"</string>
diff --git a/res/values-mr/strings.xml b/res/values-mr/strings.xml
index 0190f1f..a089669 100644
--- a/res/values-mr/strings.xml
+++ b/res/values-mr/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"सर्व दाखवा"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"सर्व विजेट दाखवा"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"सर्व विजेट दाखवत आहे"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"लँडस्केप मोड"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"फोन लँडस्केप मोडमध्ये सेट करा"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"Work apps साठी शेड्यूल"</string>
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"फिल्टर"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"हे करता आले नाही: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"खाजगी स्पेस"</string>
diff --git a/res/values-ms/strings.xml b/res/values-ms/strings.xml
index 5ddf8a2..79a8c43 100644
--- a/res/values-ms/strings.xml
+++ b/res/values-ms/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"Tunjukkan semua"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"Tunjukkan semua widget"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"Menunjukkan semua widget"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"Mod landskap"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"Tetapkan telefon kepada mod landskap"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,9 +195,10 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"Jadual apl kerja"</string>
     <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 948f2eb..340d7a8 100644
--- a/res/values-my/strings.xml
+++ b/res/values-my/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"အားလုံးပြပါ"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"ဝိဂျက်အားလုံး ပြပါ"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"ဝိဂျက်အားလုံးကို ပြထားသည်"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"အလျားလိုက်"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"ဖုန်းကို အလျားလိုက်သို့ သတ်မှတ်နိုင်သည်"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"အလုပ်သုံးအက်ပ်များ အချိန်ဇယား"</string>
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"စစ်ထုတ်ရန်"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"မအောင်မြင်ပါ− <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"သီးသန့်ချတ်ခန်း"</string>
diff --git a/res/values-nb/strings.xml b/res/values-nb/strings.xml
index 0dc6160..004e100 100644
--- a/res/values-nb/strings.xml
+++ b/res/values-nb/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"Vis alle"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"Vis alle moduler"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"Viser alle moduler"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"Liggende retning"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"Plasser telefonen i liggende retning"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"Tidsplan for jobbapper"</string>
     <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 4fae68b..0940743 100644
--- a/res/values-ne/strings.xml
+++ b/res/values-ne/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"सबै देखाउनुहोस्"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"सबै विजेटहरू देखाउनुहोस्"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"सबै विजेटहरू देखाइँदै छन्"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"ल्यान्डस्केप मोड"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"फोनमा ल्यान्डस्केप मोड अन गर्नुहोस्"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"कामसम्बन्धी एपहरूको समयतालिका"</string>
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"फिल्टर"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"कार्य पूरा गर्न सकिएन: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"निजी स्पेस"</string>
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-night/styles.xml b/res/values-night/styles.xml
index 06f0eee..a891e39 100644
--- a/res/values-night/styles.xml
+++ b/res/values-night/styles.xml
@@ -38,16 +38,16 @@
         <item name="materialColorSecondaryFixedDim">@color/system_secondary_fixed_dim</item>
         <item name="materialColorOnErrorContainer">@color/system_on_error_container_dark</item>
         <item name="materialColorOnSecondaryFixed">@color/system_on_secondary_fixed</item>
-        <item name="materialColorOnSurfaceInverse">@color/system_on_surface_light</item>
+        <item name="materialColorInverseOnSurface">@color/system_on_surface_light</item>
         <item name="materialColorTertiaryFixedDim">@color/system_tertiary_fixed_dim</item>
         <item name="materialColorOnTertiaryFixed">@color/system_on_tertiary_fixed</item>
         <item name="materialColorPrimaryFixedDim">@color/system_primary_fixed_dim</item>
         <item name="materialColorSecondaryContainer">@color/system_secondary_container_dark</item>
         <item name="materialColorErrorContainer">@color/system_error_container_dark</item>
         <item name="materialColorOnPrimaryFixed">@color/system_on_primary_fixed</item>
-        <item name="materialColorPrimaryInverse">@color/system_primary_light</item>
+        <item name="materialColorInversePrimary">@color/system_primary_light</item>
         <item name="materialColorSecondaryFixed">@color/system_secondary_fixed</item>
-        <item name="materialColorSurfaceInverse">@color/system_surface_light</item>
+        <item name="materialColorInverseSurface">@color/system_surface_light</item>
         <item name="materialColorSurfaceVariant">@color/system_surface_variant_dark</item>
         <item name="materialColorTertiaryContainer">@color/system_tertiary_container_dark</item>
         <item name="materialColorTertiaryFixed">@color/system_tertiary_fixed</item>
diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml
index 35f8d65..f6d3e0f 100644
--- a/res/values-nl/strings.xml
+++ b/res/values-nl/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"Alles tonen"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"Alle widgets tonen"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"Alle widgets worden getoond"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"Liggende modus"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"Telefoon instellen op liggende modus"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"Planning voor werk-apps"</string>
     <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 abc0787..7b48ad0 100644
--- a/res/values-or/strings.xml
+++ b/res/values-or/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"ସବୁ ଦେଖାନ୍ତୁ"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"ସମସ୍ତ ୱିଜେଟ ଦେଖାନ୍ତୁ"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"ସମସ୍ତ ୱିଜେଟ ଦେଖାଉଛି"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"ଲେଣ୍ଡସ୍କେପ ମୋଡ"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"ଫୋନକୁ ଲେଣ୍ଡସ୍କେପ ମୋଡରେ ସେଟ କରନ୍ତୁ"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"ୱାର୍କ ଆପ୍ସ ସିଡୁଲ"</string>
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"ଫିଲ୍ଟର୍"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"ବିଫଳ ହୋଇଛି: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"ପ୍ରାଇଭେଟ ସ୍ପେସ"</string>
diff --git a/res/values-pa/strings.xml b/res/values-pa/strings.xml
index 715f61b..6ea5848 100644
--- a/res/values-pa/strings.xml
+++ b/res/values-pa/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"ਸਭ ਦਿਖਾਓ"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"ਸਭ ਵਿਜੇਟ ਦਿਖਾਓ"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"ਸਭ ਵਿਜੇਟ ਦਿਖਾਏ ਜਾ ਰਹੇ ਹਨ"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"ਲੈਂਡਸਕੇਪ ਮੋਡ"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"ਫ਼ੋਨ ਨੂੰ ਲੈਂਡਸਕੇਪ ਮੋਡ ਵਿੱਚ ਸੈੱਟ ਕਰੋ"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"ਕੰਮ ਸੰਬੰਧੀ ਐਪਾਂ ਦੀ ਸਮਾਂ-ਸੂਚੀ"</string>
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"ਫਿਲਟਰ"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"ਇਹ ਕਾਰਵਾਈ ਅਸਫਲ ਹੋਈ: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"ਪ੍ਰਾਈਵੇਟ ਸਪੇਸ"</string>
diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml
index 47f8a51..bde1d2f 100644
--- a/res/values-pl/strings.xml
+++ b/res/values-pl/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"Pokaż wszystko"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"Pokaż wszystkie widżety"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"Wyświetlam wszystkie widżety"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"Tryb poziomy"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"Przestaw telefon w tryb poziomy"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"Harmonogram aplikacji służbowych"</string>
     <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 d2b214e..1fadfdd 100644
--- a/res/values-pt-rPT/strings.xml
+++ b/res/values-pt-rPT/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"Mostrar tudo"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"Mostrar todos os widgets"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"A mostrar todos os widgets"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"Modo horizontal"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"Defina o telemóvel para o modo horizontal"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"Horário das apps de trabalho"</string>
     <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 f864b3e..e2cf070 100644
--- a/res/values-pt/strings.xml
+++ b/res/values-pt/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"Mostrar tudo"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"Mostrar todos os widgets"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"Mostrando todos os widgets"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"Modo paisagem"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"Definir o smartphone para o modo paisagem"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"Programação de apps de trabalho"</string>
     <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 30e33dc..71d16cb 100644
--- a/res/values-ro/strings.xml
+++ b/res/values-ro/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"Afișează tot"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"Afișează toate widgeturile"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"Se afișează toate widgeturile"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"Modul Peisaj"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"Setează telefonul în modul Peisaj"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"Programul aplicațiilor pentru lucru"</string>
     <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 126e3ad..10af874 100644
--- a/res/values-ru/strings.xml
+++ b/res/values-ru/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"Показать все"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"Показать все виджеты"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"Показаны все виджеты"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"Горизонтальный режим"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"Перевести телефон в горизонтальный режим"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"Расписание рабочих приложений"</string>
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Фильтр"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Не удалось выполнить действие (<xliff:g id="WHAT">%1$s</xliff:g>)."</string>
     <string name="private_space_label" msgid="2359721649407947001">"Частное пространство"</string>
diff --git a/res/values-si/strings.xml b/res/values-si/strings.xml
index 328f2f5..44fc1da 100644
--- a/res/values-si/strings.xml
+++ b/res/values-si/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"සියල්ල පෙන්වන්න"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"සියලු ම විජට් පෙන්වන්න"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"සියලුම විජට් පෙන්වමින්"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"භූ දර්ශන ආකාරය"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"දුරකථනය භූ දර්ශන ආකාරයට සකසන්න"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"කාර්යාල යෙදුම් කාල සටහන"</string>
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"පෙරහන"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"අසාර්ථකයි: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"පෞද්ගලික ඉඩ"</string>
diff --git a/res/values-sk/strings.xml b/res/values-sk/strings.xml
index 88cb1b1..8eb4e38 100644
--- a/res/values-sk/strings.xml
+++ b/res/values-sk/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"Zobraziť všetko"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"Zobraziť všetky miniaplikácie"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"Zobrazujú sa všetky miniaplikácie"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"Režim na šírku"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"Nastavte v telefóne režim na šírku"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"Plán pre pracovné aplikácie"</string>
     <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 37a02e4..9c2221b 100644
--- a/res/values-sl/strings.xml
+++ b/res/values-sl/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"Pokaži vse"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"Prikaz vseh pripomočkov"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"Prikazani so vsi pripomočki"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"Ležeči način"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"Telefon preklopite v ležeči način"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"Razpored delovnih aplikacij"</string>
     <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 5be991a..ced2c8c 100644
--- a/res/values-sq/strings.xml
+++ b/res/values-sq/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"Shfaq të gjitha"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"Shfaq të gjitha miniaplikacionet"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"Po shfaqen të gjitha miniaplikacionet"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"Modaliteti horizontal"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"Vendose telefonin në modalitetin horizontal"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"Orari për aplikacionet e punës"</string>
     <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 afaeb61..b4cc0e5 100644
--- a/res/values-sr/strings.xml
+++ b/res/values-sr/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"Прикажи све"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"Прикажите све виџете"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"Приказују се сви виџети"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"Водоравни режим"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"Подесите телефон на водоравни режим"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"Распоред за пословне апликације"</string>
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Филтер"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Није успело: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Приватни простор"</string>
diff --git a/res/values-sv/strings.xml b/res/values-sv/strings.xml
index 547f60e..52b8108 100644
--- a/res/values-sv/strings.xml
+++ b/res/values-sv/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"Visa alla"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"Visa alla widgetar"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"Visar alla widgetar"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"Liggande"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"Ställ in telefonen på liggande läge"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"Schema för jobbappar"</string>
     <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 71f8d8b..5acef91 100644
--- a/res/values-sw/strings.xml
+++ b/res/values-sw/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"Onyesha zote"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"Onyesha wijeti zote"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"Inaonyesha wijeti zote"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"Mkao wa mlalo"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"Weka simu katika mkao wa mlalo"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"Ratiba ya programu za kazini"</string>
     <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 e87e2eb..ea84963 100644
--- a/res/values-ta/strings.xml
+++ b/res/values-ta/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"எல்லாம் காட்டு"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"அனைத்து விட்ஜெட்களையும் காட்டும்"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"அனைத்து விட்ஜெட்களையும் காட்டுகிறது"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"லேண்ட்ஸ்கேப் பயன்முறை"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"மொபைலை லேண்ட்ஸ்கேப் பயன்முறையில் அமையுங்கள்"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"பணி ஆப்ஸுக்கான திட்ட அட்டவணை"</string>
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"வடிப்பான்"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"தோல்வி: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"தனிப்பட்ட சேமிப்பிடம்"</string>
diff --git a/res/values-te/strings.xml b/res/values-te/strings.xml
index 75dc7f0..f445d77 100644
--- a/res/values-te/strings.xml
+++ b/res/values-te/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"అన్నీ చూడండి"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"అన్ని విడ్జెట్‌లను చూపండి"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"అన్ని విడ్జెట్‌లు చూపబడుతున్నాయి"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"ల్యాండ్‌స్కేప్ మోడ్"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"ఫోన్‌ను ల్యాండ్‌స్కేప్ మోడ్‌కు సెట్ చేయండి"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"వర్క్ యాప్‌ల షెడ్యూల్"</string>
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"ఫిల్టర్ చేయి"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"విఫలమైంది: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"ప్రైవేట్ స్పేస్"</string>
diff --git a/res/values-th/strings.xml b/res/values-th/strings.xml
index 50a0e52..6acf390 100644
--- a/res/values-th/strings.xml
+++ b/res/values-th/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"แสดงทั้งหมด"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"แสดงวิดเจ็ตทั้งหมด"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"กำลังแสดงวิดเจ็ตทั้งหมด"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"โหมดแนวนอน"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"ตั้งค่าโทรศัพท์เป็นโหมดแนวนอน"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"กำหนดเวลาของแอปงาน"</string>
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"ตัวกรอง"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"ไม่สำเร็จ: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"พื้นที่ส่วนตัว"</string>
diff --git a/res/values-tl/strings.xml b/res/values-tl/strings.xml
index 86ad330..0cef73a 100644
--- a/res/values-tl/strings.xml
+++ b/res/values-tl/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"Ipakita lahat"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"Ipakita ang lahat ng widget"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"Ipinapakita ang lahat ng widget"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"Landscape mode"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"Itakda ang telepono sa landscape mode"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"Iskedyul ng mga app para sa trabaho"</string>
     <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 9955bbf..8dbcc49 100644
--- a/res/values-tr/strings.xml
+++ b/res/values-tr/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"Tümünü göster"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"Tüm widget\'ları göster"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"Tüm widget\'lar gösteriliyor"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"Yatay mod"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"Telefonu yatay moda ayarlayın"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"İş uygulamaları programı"</string>
     <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 e777262..069277d 100644
--- a/res/values-uk/strings.xml
+++ b/res/values-uk/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"Показати всі"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"Показати всі віджети"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"Показано всі віджети"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"Альбомна орієнтація"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"Змінити орієнтацію екрана телефона на альбомну"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"Розклад призупинення робочих додатків"</string>
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"Фільтр"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"Не вдалося <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"Приватний простір"</string>
diff --git a/res/values-ur/strings.xml b/res/values-ur/strings.xml
index 9fb85b2..3cbfd1b 100644
--- a/res/values-ur/strings.xml
+++ b/res/values-ur/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"سبھی دکھائیں"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"سبھی ویجیٹس دکھائیں"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"سبھی ویجیٹس دکھائے جا رہے ہیں"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"لینڈ اسکیپ وضع"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"فون کو لینڈ اسکیپ وضع میں سیٹ کریں"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"ورک ایپس کا شیڈول"</string>
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"فلٹر"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"ناکام ہو گيا: <xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"نجی اسپیس"</string>
diff --git a/res/values-uz/strings.xml b/res/values-uz/strings.xml
index 83cabc9..2b77321 100644
--- a/res/values-uz/strings.xml
+++ b/res/values-uz/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"Hammasi"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"Barcha vidjetlar"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"Barcha vidjetlar chiqarilgan"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"Yotiq rejim"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"Telefonni yotiq rejimga oʻtkazish"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"Ishga oid ilovalar jadvali"</string>
     <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 d67f661..ae49356 100644
--- a/res/values-vi/strings.xml
+++ b/res/values-vi/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"Hiện tất cả"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"Hiện tất cả tiện ích"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"Đang hiện tất cả tiện ích"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"Chế độ ngang"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"Đặt điện thoại ở chế độ ngang"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"Lịch biểu cho ứng dụng công việc"</string>
     <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 1097e57..69d92f8 100644
--- a/res/values-zh-rCN/strings.xml
+++ b/res/values-zh-rCN/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"全部显示"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"显示所有微件"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"正在显示所有微件"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"横屏模式"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"将手机设为横屏模式"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"工作应用时间表"</string>
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"过滤器"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"失败:<xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"私密空间"</string>
diff --git a/res/values-zh-rHK/strings.xml b/res/values-zh-rHK/strings.xml
index 6471a9a..3589167 100644
--- a/res/values-zh-rHK/strings.xml
+++ b/res/values-zh-rHK/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"顯示全部"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"顯示所有小工具"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"顯示所有小工具"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"水平模式"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"將手機設定為水平模式"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"工作應用程式時間表"</string>
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"篩選器"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"操作失敗:<xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"私人空間"</string>
diff --git a/res/values-zh-rTW/strings.xml b/res/values-zh-rTW/strings.xml
index 0a9ffa2..3f16360 100644
--- a/res/values-zh-rTW/strings.xml
+++ b/res/values-zh-rTW/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"全部顯示"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"顯示所有小工具"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"現已顯示所有小工具"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"橫向模式"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"將手機設為橫向模式"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"工作應用程式時間表"</string>
     <string name="developer_options_filter_hint" msgid="5896817443635989056">"篩選器"</string>
     <string name="remote_action_failed" msgid="1383965239183576790">"失敗:<xliff:g id="WHAT">%1$s</xliff:g>"</string>
     <string name="private_space_label" msgid="2359721649407947001">"私人空間"</string>
diff --git a/res/values-zu/strings.xml b/res/values-zu/strings.xml
index 59c99c4..63fd982 100644
--- a/res/values-zu/strings.xml
+++ b/res/values-zu/strings.xml
@@ -32,6 +32,8 @@
     <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>
+    <!-- no translation found for manage_windows_option_taskbar (2294109489960654212) -->
+    <skip />
     <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>
@@ -69,6 +71,9 @@
     <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>
+    <string name="widgets_list_expand_button_label" msgid="7912016136574932622">"Bonisa konke"</string>
+    <string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"Bonisa wonke amawijethi"</string>
+    <string name="widgets_list_expanded" msgid="7374857868788557730">"Ibonisa wonke amawijethi"</string>
     <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>
@@ -124,6 +129,8 @@
     <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>
+    <string name="landscape_mode_title" msgid="5138814555934843926">"Imodi yokuvundla"</string>
+    <string name="landscape_mode_desc" msgid="7372569859592816793">"Setha ifoni kumodi yokuvundla"</string>
     <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>
@@ -142,7 +149,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>
@@ -187,6 +195,7 @@
     <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>
+    <string name="work_scheduler_button_content_description" msgid="917340740986764967">"Ishejuli yama-app omsebenzi"</string>
     <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..535d61f 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -58,16 +58,16 @@
     <attr name="materialColorSecondaryFixedDim" format="color" />
     <attr name="materialColorOnErrorContainer" format="color" />
     <attr name="materialColorOnSecondaryFixed" format="color" />
-    <attr name="materialColorOnSurfaceInverse" format="color" />
+    <attr name="materialColorInverseOnSurface" format="color" />
     <attr name="materialColorTertiaryFixedDim" format="color" />
     <attr name="materialColorOnTertiaryFixed" format="color" />
     <attr name="materialColorPrimaryFixedDim" format="color" />
     <attr name="materialColorSecondaryContainer" format="color" />
     <attr name="materialColorErrorContainer" format="color" />
     <attr name="materialColorOnPrimaryFixed" format="color" />
-    <attr name="materialColorPrimaryInverse" format="color" />
+    <attr name="materialColorInversePrimary" format="color" />
     <attr name="materialColorSecondaryFixed" format="color" />
-    <attr name="materialColorSurfaceInverse" format="color" />
+    <attr name="materialColorInverseSurface" format="color" />
     <attr name="materialColorSurfaceVariant" format="color" />
     <attr name="materialColorTertiaryContainer" format="color" />
     <attr name="materialColorTertiaryFixed" format="color" />
@@ -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="GridDimension">
+        <attr name="minDeviceWidthPx" format="float"/>
+        <attr name="minDeviceHeightPx" format="float"/>
+        <attr name="numGridDimension" 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..326ee06 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -125,7 +125,10 @@
     <dimen name="all_apps_work_profile_tab_footer_top_padding">16dp</dimen>
     <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_focus_horizontal_inset">5dp</dimen>
+    <dimen name="all_apps_tabs_focus_vertical_inset">6dp</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 +154,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 +228,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 +440,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>
@@ -552,4 +568,8 @@
 
     <!-- WindowManagerProxy -->
     <dimen name="max_width_and_height_of_small_display_cutout">136px</dimen>
+
+    <!-- App Title Pill -->
+    <dimen name="app_title_pill_horizontal_padding">4dp</dimen>
+    <dimen name="app_title_pill_round_rect_padding">2dp</dimen>
 </resources>
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..f7069a6 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -47,6 +47,8 @@
 
     <!-- Title for an option to open a new window for a given app   -->
     <string name="new_window_option_taskbar">New Window</string>
+    <!-- Title for an option to manage open windows for a given app   -->
+    <string name="manage_windows_option_taskbar">Manage Windows</string>
 
     <!-- App pairs -->
     <string name="save_app_pair">Save app pair</string>
@@ -157,6 +159,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 +319,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 +369,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 -->
@@ -443,7 +458,7 @@
     <string name="widget_resized">Widget resized to width <xliff:g id="number" example="2">%1$s</xliff:g> height <xliff:g id="number" example="1">%2$s</xliff:g></string>
 
     <!-- Accessibility action to show quick actions menu for an icon. [CHAR_LIMIT=30] -->
-    <string name="action_deep_shortcut">Shortcuts</string>
+    <string name="action_deep_shortcut">Shortcut Menu</string>
 
     <!-- Accessibility action to dismiss a notification in the shortcuts menu for an icon. [CHAR_LIMIT=30] -->
     <string name="action_dismiss_notification">Dismiss</string>
@@ -465,6 +480,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 +500,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..1c70d6c 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -41,16 +41,16 @@
         <item name="materialColorSecondaryFixedDim">@color/system_secondary_fixed_dim</item>
         <item name="materialColorOnErrorContainer">@color/system_on_error_container_light</item>
         <item name="materialColorOnSecondaryFixed">@color/system_on_secondary_fixed</item>
-        <item name="materialColorOnSurfaceInverse">@color/system_on_surface_dark</item>
+        <item name="materialColorInverseOnSurface">@color/system_on_surface_dark</item>
         <item name="materialColorTertiaryFixedDim">@color/system_tertiary_fixed_dim</item>
         <item name="materialColorOnTertiaryFixed">@color/system_on_tertiary_fixed</item>
         <item name="materialColorPrimaryFixedDim">@color/system_primary_fixed_dim</item>
         <item name="materialColorSecondaryContainer">@color/system_secondary_container_light</item>
         <item name="materialColorErrorContainer">@color/system_error_container_light</item>
         <item name="materialColorOnPrimaryFixed">@color/system_on_primary_fixed</item>
-        <item name="materialColorPrimaryInverse">@color/system_primary_dark</item>
+        <item name="materialColorInversePrimary">@color/system_primary_dark</item>
         <item name="materialColorSecondaryFixed">@color/system_secondary_fixed</item>
-        <item name="materialColorSurfaceInverse">@color/system_surface_dark</item>
+        <item name="materialColorInverseSurface">@color/system_surface_dark</item>
         <item name="materialColorSurfaceVariant">@color/system_surface_variant_light</item>
         <item name="materialColorTertiaryContainer">@color/system_tertiary_container_light</item>
         <item name="materialColorTertiaryFixed">@color/system_tertiary_fixed</item>
@@ -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..ed2ab81 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,66 @@
         }
     }
 
+    /** 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);
+        CharSequence text = getText();
+
+        int mAppTitleHorizontalPadding = getResources().getDimensionPixelSize(
+                R.dimen.app_title_pill_horizontal_padding);
+        int mRoundRectPadding = getResources().getDimensionPixelSize(
+                R.dimen.app_title_pill_round_rect_padding);
+
+        float titleLength = (getPaint().measureText(text, 0, text.length())
+                + (mAppTitleHorizontalPadding + mRoundRectPadding) * 2);
+        titleLength = Math.min(titleLength, tmpRect.width());
+        appTitleBounds = new RectF((tmpRect.width() - titleLength) / 2.f - getCompoundPaddingLeft(),
+                0, (tmpRect.width() + titleLength) / 2.f + getCompoundPaddingRight(),
+                (int) Math.ceil(fm.bottom - fm.top));
+        appTitleBounds.inset(mRoundRectPadding * 2, 0);
+
+
+        if (mIcon != null) {
+            Rect iconBounds = new Rect();
+            getIconBounds(iconBounds);
+            int textStart = iconBounds.bottom + getCompoundDrawablePadding();
+            appTitleBounds.offset(0, textStart);
+        }
+
+        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
@@ -804,6 +862,16 @@
             setPadding(getPaddingLeft(), (height - cellHeightPx) / 2, getPaddingRight(),
                     getPaddingBottom());
         }
+        if (shouldDrawAppContrastTile()) {
+            int mAppTitleHorizontalPadding = getResources().getDimensionPixelSize(
+                    R.dimen.app_title_pill_horizontal_padding);
+            int mRoundRectPadding = getResources().getDimensionPixelSize(
+                    R.dimen.app_title_pill_round_rect_padding);
+
+            setPadding(mAppTitleHorizontalPadding + mRoundRectPadding, getPaddingTop(),
+                    mAppTitleHorizontalPadding + mRoundRectPadding,
+                    getPaddingBottom());
+        }
         // Only apply two line for all_apps and device search only if necessary.
         if (shouldUseTwoLine() && (mLastOriginalText != null)) {
             int allowedVerticalSpace = height - getPaddingTop() - getPaddingBottom()
@@ -887,7 +955,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 +974,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 +1209,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 +1224,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 +1285,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 +1302,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 +1324,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 +1339,7 @@
             }
 
             mDisableRelayout = false;
-            mEnableIconUpdateAnimation = false;
+            mHighResUpdateInProgress = false;
         }
     }
 
@@ -1265,7 +1351,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/ButtonDropTarget.java b/src/com/android/launcher3/ButtonDropTarget.java
index 3d715e5..18619f5 100644
--- a/src/com/android/launcher3/ButtonDropTarget.java
+++ b/src/com/android/launcher3/ButtonDropTarget.java
@@ -41,8 +41,11 @@
 import com.android.launcher3.dragndrop.DragOptions;
 import com.android.launcher3.dragndrop.DragView;
 import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.util.MSDLPlayerWrapper;
 import com.android.launcher3.views.ActivityContext;
 
+import com.google.android.msdl.data.model.MSDLToken;
+
 /**
  * Implements a DropTarget.
  */
@@ -62,6 +65,7 @@
     protected final ActivityContext mActivityContext;
     protected final DropTargetHandler mDropTargetHandler;
     protected DropTargetBar mDropTargetBar;
+    private MSDLPlayerWrapper mMSDLPlayerWrapper;
 
     /** Whether this drop target is active for the current drag */
     protected boolean mActive;
@@ -94,6 +98,7 @@
         super(context, attrs, defStyle);
         mActivityContext = ActivityContext.lookupContext(context);
         mDropTargetHandler = mActivityContext.getDropTargetHandler();
+        mMSDLPlayerWrapper = MSDLPlayerWrapper.INSTANCE.get(context);
 
         Resources resources = getResources();
         mDragDistanceThreshold = resources.getDimensionPixelSize(R.dimen.drag_distanceThreshold);
@@ -142,6 +147,10 @@
 
     @Override
     public final void onDragEnter(DragObject d) {
+        // Perform Haptic feedback
+        if (Flags.msdlFeedback()) {
+            mMSDLPlayerWrapper.playToken(MSDLToken.SWIPE_THRESHOLD_INDICATOR);
+        }
         if (!mAccessibleDrag && !mTextVisible) {
             // Show tooltip
             hideTooltip();
@@ -429,6 +438,11 @@
         return textHeight + getPaddingTop() + getPaddingBottom() >= availableHeight;
     }
 
+    @VisibleForTesting
+    public void setMSDLPlayerWrapper(MSDLPlayerWrapper wrapper) {
+        mMSDLPlayerWrapper = wrapper;
+    }
+
     /**
      * Reduce the size of the text until it fits the measured width or reaches a minimum.
      *
diff --git a/src/com/android/launcher3/DeleteDropTarget.java b/src/com/android/launcher3/DeleteDropTarget.java
index 58789fd..425f277 100644
--- a/src/com/android/launcher3/DeleteDropTarget.java
+++ b/src/com/android/launcher3/DeleteDropTarget.java
@@ -130,7 +130,6 @@
     public void completeDrop(DragObject d) {
         ItemInfo item = d.dragInfo;
         if (canRemove(item)) {
-            onAccessibilityDrop(null, item);
             mDropTargetHandler.onDeleteComplete(item);
         }
     }
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index 483f5f8..09225e7 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
@@ -1804,7 +1833,8 @@
                         workspacePageIndicatorHeight - mWorkspacePageIndicatorOverlapWorkspace;
             }
             int paddingTop = workspaceTopPadding + (mIsScalableGrid ? 0 : edgeMarginPx);
-            int paddingSide = desiredWorkspaceHorizontalMarginPx;
+            // On isFixedLandscapeMode on phones we already have padding because of the camera hole
+            int paddingSide = inv.isFixedLandscapeMode ? 0 : desiredWorkspaceHorizontalMarginPx;
 
             padding.set(paddingSide, paddingTop, paddingSide, paddingBottom);
         }
@@ -1830,19 +1860,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 +1877,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 +1922,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 +1941,11 @@
             }
             startSpacing += getAdditionalQsbSpace();
 
+            if (inv.isFixedLandscapeMode) {
+                endSpacing += mInsets.right;
+                startSpacing +=  mInsets.left;
+            }
+
             hotseatBarPadding.top = hotseatBarTopPadding;
             hotseatBarPadding.bottom = hotseatBarBottomPadding;
             boolean isRtl = Utilities.isRtl(context.getResources());
@@ -1998,6 +2055,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 +2283,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 +2401,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 +2541,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/DropTargetHandler.kt b/src/com/android/launcher3/DropTargetHandler.kt
index f1029b1..4d3fe52 100644
--- a/src/com/android/launcher3/DropTargetHandler.kt
+++ b/src/com/android/launcher3/DropTargetHandler.kt
@@ -65,6 +65,7 @@
     }
 
     fun onDeleteComplete(item: ItemInfo) {
+        removeItemAndStripEmptyScreens(null /* view */, item)
         var pageItem: ItemInfo = item
         if (item.container <= 0) {
             val v = mLauncher.workspace.getHomescreenIconByItemId(item.container)
@@ -90,11 +91,7 @@
     }
 
     fun onAccessibilityDelete(view: View?, item: ItemInfo, announcement: CharSequence) {
-        // Remove the item from launcher and the db, we can ignore the containerInfo in this call
-        // because we already remove the drag view from the folder (if the drag originated from
-        // a folder) in Folder.beginDrag()
-        mLauncher.removeItem(view, item, true /* deleteFromDb */, "removed by accessibility drop")
-        mLauncher.workspace.stripEmptyScreens()
+        removeItemAndStripEmptyScreens(view, item)
         mLauncher.dragLayer.announceForAccessibility(announcement)
     }
 
@@ -105,4 +102,12 @@
     fun onClick(buttonDropTarget: ButtonDropTarget) {
         mLauncher.accessibilityDelegate.handleAccessibleDrop(buttonDropTarget, null, null)
     }
+
+    private fun removeItemAndStripEmptyScreens(view: View?, item: ItemInfo) {
+        // Remove the item from launcher and the db, we can ignore the containerInfo in this call
+        // because we already remove the drag view from the folder (if the drag originated from
+        // a folder) in Folder.beginDrag()
+        mLauncher.removeItem(view, item, true /* deleteFromDb */, "removed by accessibility drop")
+        mLauncher.workspace.stripEmptyScreens()
+    }
 }
diff --git a/src/com/android/launcher3/Hotseat.java b/src/com/android/launcher3/Hotseat.java
index 024dde4..b2ccba4 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,20 @@
     public void adjustForBubbleBar(boolean isBubbleBarVisible) {
         DeviceProfile dp = mActivity.getDeviceProfile();
         float adjustedBorderSpace = dp.getHotseatAdjustedBorderSpaceForBubbleBar(getContext());
-        if (Float.compare(adjustedBorderSpace, 0f) == 0) {
-            return;
-        }
-
+        boolean shouldAdjustHotseat = isBubbleBarVisible
+                && 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 (shouldAdjustHotseat) {
+            icons.setTranslationProvider(
+                    cellX -> dp.getHotseatAdjustedTranslation(getContext(), cellX));
         } else {
             icons.setTranslationProvider(null);
         }
-
+        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 = shouldAdjustHotseat ? dp.getHotseatAdjustedTranslation(getContext(), i) : 0;
             if (child instanceof Reorderable) {
                 MultiTranslateDelegate mtd = ((Reorderable) child).getTranslateDelegate();
                 animatorSet.play(
@@ -187,8 +211,8 @@
         }
         if (mQsb instanceof HorizontalInsettableView horizontalInsettableQsb) {
             final float currentInsetFraction = horizontalInsettableQsb.getHorizontalInsets();
-            final float targetInsetFraction =
-                    isBubbleBarVisible ? (float) dp.iconSizePx / dp.hotseatQsbWidth : 0;
+            final float targetInsetFraction = shouldAdjustHotseat
+                    ? (float) dp.iconSizePx / dp.hotseatQsbWidth : 0;
             ValueAnimator qsbAnimator =
                     ValueAnimator.ofFloat(currentInsetFraction, targetInsetFraction);
             qsbAnimator.addUpdateListener(animation -> {
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index 54aea38..e1d84be 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 GridDimension specs. Then calls findBestRowCount to get the
+     * correct row count for this GridOption.
+     *
+     * @return the result of {@link #findBestRowCount(List, Info)}.
+     */
+    public static GridDimension getRowCount(ResourceHelper resourceHelper, Context context,
+            Info displayInfo) {
+        ArrayList<GridDimension> 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)
+                        && "GridDimension".equals(parser.getName())) {
+                    rowCounts.add(new GridDimension(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 GridDimension to
+     * determine that. If no best row count is found, return -1.
+     */
+    public static GridDimension findBestRowCount(List<GridDimension> 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);
+            }
+        }
+
+        GridDimension selectedRow = null;
+        for (GridDimension item: list) {
+            if (minWidthPx >= item.mMinDeviceWidthPx && minHeightPx >= item.mMinDeviceHeightPx) {
+                if (selectedRow == null || selectedRow.mNumGridDimension < item.mNumGridDimension) {
+                    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);
+                GridDimension numR = getRowCount(resourceHelper, context, displayInfo);
+                numRows = numR.mNumGridDimension;
+                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 GridDimension {
+        final int mNumGridDimension;
+        final float mMinDeviceWidthPx;
+        final float mMinDeviceHeightPx;
+        final String mDbFile;
+        final int mDefaultLayoutId;
+        final int mDemoModeLayoutId;
+
+
+        GridDimension(Context context, AttributeSet attrs) {
+            TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.GridDimension);
+
+            mNumGridDimension = (int) a.getFloat(R.styleable.GridDimension_numGridDimension, 0);
+            mMinDeviceWidthPx = a.getFloat(R.styleable.GridDimension_minDeviceWidthPx, 0);
+            mMinDeviceHeightPx = a.getFloat(R.styleable.GridDimension_minDeviceHeightPx, 0);
+            mDbFile = a.getString(R.styleable.GridDimension_dbFile);
+            mDefaultLayoutId = a.getResourceId(
+                    R.styleable.GridDimension_defaultLayoutId, 0);
+            mDemoModeLayoutId = a.getResourceId(
+                    R.styleable.GridDimension_demoModeLayoutId, mDefaultLayoutId);
+
+            a.recycle();
+        }
     }
 
     @VisibleForTesting
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index b0ec9b0..8981024 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -224,15 +224,16 @@
 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;
 import com.android.launcher3.util.KeyboardShortcutsDelegate;
 import com.android.launcher3.util.LockedUserState;
+import com.android.launcher3.util.MSDLPlayerWrapper;
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.util.PendingRequestArgs;
 import com.android.launcher3.util.PluginManagerWrapper;
@@ -279,6 +280,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 +294,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 +415,7 @@
 
     private final List<BackPressHandler> mBackPressedHandlers = new ArrayList<>();
     private boolean mIsColdStartupAfterReboot;
+    private boolean mForceConfigUpdate;
 
     private boolean mIsNaturalScrollingEnabled;
 
@@ -534,6 +538,7 @@
 
         mPopupDataProvider = new PopupDataProvider(this::updateNotificationDots);
         mWidgetPickerDataProvider = new WidgetPickerDataProvider();
+        PillColorProvider.getInstance(mWorkspace.getContext()).registerObserver();
 
         boolean internalStateHandled = ACTIVITY_TRACKER.handleCreate(this);
         if (internalStateHandled) {
@@ -756,10 +761,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 +774,25 @@
             mModel.rebindCallbacks();
             updateDisallowBack();
         } finally {
+            mForceConfigUpdate = false;
             Trace.endSection();
         }
     }
 
+    private void updateFixedLandscape() {
+        if (!com.android.launcher3.Flags.oneGridSpecs()) {
+            return;
+        }
+        // When the flag oneGridSpecs is on we want to disable ALLOW_ROTATION which is replaced
+        // by FIXED_LANDSCAPE_MODE, ALLOW_ROTATION will only be used on Tablets afterwards.
+        if (getDeviceProfile().isPhone || getDeviceProfile().isTwoPanels) {
+            LauncherPrefs.get(this).put(LauncherPrefs.ALLOW_ROTATION, false);
+        }
+        getRotationHelper().setFixedLandscape(
+                Objects.requireNonNull(mDeviceProfile.inv).isFixedLandscapeMode
+        );
+    }
+
     public void onAssistantVisibilityChanged(float visibility) {
         mHotseat.getQsb().setAlpha(1f - visibility);
     }
@@ -802,6 +821,7 @@
                     mDeviceProfile.numShownHotseatIcons);
         }
         mModelWriter = mModel.getWriter(true, mCellPosMapper, this);
+        updateFixedLandscape();
         return true;
     }
 
@@ -1342,7 +1362,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 +1797,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 +1820,7 @@
         // changes while launcher is still loading.
         getRootView().getViewTreeObserver().removeOnPreDrawListener(mOnInitialBindListener);
         mOverlayManager.onActivityDestroyed();
+        PillColorProvider.getInstance(mWorkspace.getContext()).unregisterObserver();
     }
 
     public LauncherAccessibilityDelegate getAccessibilityDelegate() {
@@ -1872,13 +1894,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 +2647,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
@@ -2718,6 +2746,7 @@
         mModel.dumpState(prefix, fd, writer, args);
         mOverlayManager.dump(prefix, writer);
         ACTIVITY_TRACKER.dump(prefix, writer);
+        MSDLPlayerWrapper.INSTANCE.get(getApplicationContext()).dump(prefix, writer);
     }
 
     /**
@@ -3146,6 +3175,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..01d0a74 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);
         });
 
@@ -198,7 +198,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 +267,7 @@
     }
 
     private class IconObserver
-            implements IconProvider.IconChangeListener, OnSharedPreferenceChangeListener {
+            implements IconProvider.IconChangeListener, LauncherPrefChangeListener {
 
         @Override
         public void onAppIconChanged(String packageName, UserHandle user) {
@@ -288,7 +289,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..a5b8168 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_8_BY_3_DB = "launcher_8_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_8_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..f4d3146 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;
@@ -304,10 +303,11 @@
                     .setData(Uri.fromParts("package", cn.getPackageName(), cn.getClassName()))
                     .putExtra(Intent.EXTRA_USER, info.user);
             context.startActivity(i);
-            FileLog.d(TAG, "start uninstall activity " + cn.getPackageName());
+            FileLog.d(TAG, "start uninstall activity from drop target " + cn.getPackageName());
             return cn;
         } catch (URISyntaxException e) {
-            Log.e(TAG, "Failed to parse intent to start uninstall activity for item=" + info);
+            Log.e(TAG, "Failed to parse intent to start drop target uninstall activity for"
+                    + " item=" + info);
             return null;
         }
     }
@@ -342,9 +342,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/SessionCommitReceiver.java b/src/com/android/launcher3/SessionCommitReceiver.java
index 6168e41..ea5eb8f 100644
--- a/src/com/android/launcher3/SessionCommitReceiver.java
+++ b/src/com/android/launcher3/SessionCommitReceiver.java
@@ -73,8 +73,9 @@
                 || alreadyAddedPromiseIcon) {
             FileLog.d(LOG,
                     String.format(Locale.ENGLISH,
-                            "Removing PromiseIcon for package: %s, install reason: %d,"
-                            + " alreadyAddedPromiseIcon: %s",
+                            "Removing unneeded PromiseIcon for package: %s"
+                                    + ", install reason: %d,"
+                                    + " alreadyAddedPromiseIcon: %s",
                     info.getAppPackageName(),
                     info.getInstallReason(),
                     alreadyAddedPromiseIcon
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..c938482 100644
--- a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
@@ -28,6 +28,7 @@
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.ScrollableLayoutManager.PREDICTIVE_BACK_MIN_SCALE;
 import static com.android.launcher3.views.RecyclerViewFastScroller.FastScrollerLocation.ALL_APPS_SCROLLER;
+import static com.android.window.flags.Flags.predictiveBackThreeButtonNav;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -47,7 +48,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 +96,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 +116,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 +154,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 +180,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 +265,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 +285,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 +344,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 +475,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 +705,7 @@
             post(() -> mAH.get(AdapterHolder.WORK).applyPadding());
 
         } else {
-            mWorkManager.detachWorkModeSwitch();
+            mWorkManager.detachWorkUtilityViews();
             mViewPager = null;
         }
 
@@ -740,6 +723,8 @@
     }
 
     void setupHeader() {
+        mAdditionalHeaderRows.forEach(row -> mHeader.onPluginDisconnected(row));
+
         mHeader.setVisibility(View.VISIBLE);
         boolean tabsHidden = !mUsingTabs;
         mHeader.setup(
@@ -757,6 +742,7 @@
                 adapterHolder.mRecyclerView.scrollToTop();
             }
         });
+        mAdditionalHeaderRows.forEach(row -> mHeader.onPluginConnected(row, mActivityContext));
 
         removeCustomRules(mHeader);
         if (isSearchBarFloating()) {
@@ -998,18 +984,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 +1128,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;
@@ -1193,8 +1174,10 @@
         super.dispatchDraw(canvas);
 
         if (mNavBarScrimHeight > 0) {
-            canvas.drawRect(0, getHeight() - mNavBarScrimHeight, getWidth(), getHeight(),
-                    mNavBarScrimPaint);
+            float left = (getWidth() - getWidth() / getScaleX()) / 2;
+            float top = getHeight() / 2f + (getHeight() / 2f - mNavBarScrimHeight) / getScaleY();
+            canvas.drawRect(left, top, getWidth() / getScaleX(),
+                    top + mNavBarScrimHeight / getScaleY(), mNavBarScrimPaint);
         }
     }
 
@@ -1253,8 +1236,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();
     }
@@ -1360,6 +1343,17 @@
         invalidateHeader();
     }
 
+    @Override
+    public void setScaleY(float scaleY) {
+        super.setScaleY(scaleY);
+        if (predictiveBackThreeButtonNav() && mNavBarScrimHeight > 0) {
+            // Call invalidate to prevent navbar scrim from scaling. The navbar scrim is drawn
+            // directly onto the canvas. To prevent it from being scaled with the canvas, there's a
+            // counter scale applied in dispatchDraw.
+            invalidate(20, getHeight() - mNavBarScrimHeight, getWidth(), getHeight());
+        }
+    }
+
     /**
      * Set {@link Animator.AnimatorListener} on {@link mAllAppsTransitionController} to observe
      * animation of backing out of all apps search view to all apps view.
@@ -1394,7 +1388,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 +1571,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..609edd2 100644
--- a/src/com/android/launcher3/allapps/PrivateProfileManager.java
+++ b/src/com/android/launcher3/allapps/PrivateProfileManager.java
@@ -45,6 +45,7 @@
 import android.animation.ValueAnimator;
 import android.content.Context;
 import android.content.Intent;
+import android.os.Trace;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.util.Log;
@@ -66,6 +67,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;
@@ -219,7 +221,8 @@
      * when animation is not running.
      */
     public void reset() {
-        // Ensure the state of the header views is what it should be before animating.
+        Trace.beginSection("PrivateProfileManager#reset");
+        // Ensure the state of the header view is what it should be before animating.
         updateView();
         getMainRecyclerView().setChildAttachedConsumer(null);
         int previousState = getCurrentState();
@@ -238,6 +241,7 @@
             executeLock();
         }
         addPrivateSpaceDecorator(updatedState);
+        Trace.endSection();
     }
 
     /** Returns whether or not Private Space Settings Page is available. */
@@ -292,31 +296,12 @@
         }
     }
 
-    @Override
     public void setQuietMode(boolean enable) {
-        UI_HELPER_EXECUTOR.post(() ->
-                mUserCache.getUserProfiles()
-                        .stream()
-                        .filter(getUserMatcher())
-                        .findFirst()
-                        .ifPresent(userHandle -> setQuietModeSafely(enable, userHandle)));
+        setQuietMode(enable, mAllApps.mActivityContext);
         mReadyToAnimate = true;
     }
 
     /**
-     * Sets Quiet Mode for Private Profile.
-     * If {@link SecurityException} is thrown, prompts the user to set this launcher as HOME app.
-     */
-    private void setQuietModeSafely(boolean enable, UserHandle userHandle) {
-        try {
-            mUserManager.requestQuietModeEnabled(enable, userHandle);
-        } catch (SecurityException ex) {
-            ApiWrapper.INSTANCE.get(mAllApps.mActivityContext)
-                    .assignDefaultHomeRole(mAllApps.mActivityContext);
-        }
-    }
-
-    /**
      * Expand the private space after the app list has been added and updated from
      * {@link AlphabeticalAppsList#onAppsUpdated()}
      */
@@ -330,7 +315,9 @@
 
     /** Collapses the private space before the app list has been updated. */
     void executeLock() {
+        Trace.beginSection("PrivateProfileManager#executeLock");
         MAIN_EXECUTOR.execute(() -> updatePrivateStateAnimator(false));
+        Trace.endSection();
     }
 
     void setAnimationRunning(boolean isAnimationRunning) {
@@ -377,6 +364,7 @@
         if (mPSHeader == null) {
             return;
         }
+        Trace.beginSection("PrivateProfileManager#updateView");
         Log.d(TAG, "bindPrivateSpaceHeaderViewElements: " + "Updating view with state: "
                 + getCurrentState());
         mPSHeader.setAlpha(1);
@@ -434,6 +422,8 @@
                 lockPill.setVisibility(GONE);
             }
         }
+        mPSHeader.invalidate();
+        Trace.endSection();
     }
 
     /** Sets the enablement of the profile when header or button is clicked. */
@@ -473,7 +463,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 +690,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();
@@ -835,6 +828,7 @@
         ActivityAllAppsContainerView<?>.AdapterHolder mainAdapterHolder = mAllApps.mAH.get(MAIN);
         List<BaseAllAppsAdapter.AdapterItem> adapterItems =
                 mainAdapterHolder.mAppsList.getAdapterItems();
+        Trace.beginSection("PrivateProfileManager#expandPrivateSpace");
         if (Flags.enablePrivateSpace() && Flags.privateSpaceAnimation()
                 && mAllApps.isPersonalTab()) {
             // Animate the text and settings icon.
@@ -844,6 +838,7 @@
                     getPsHeaderHeight(), deviceProfile.allAppsCellHeightPx);
             updatePrivateStateAnimator(true);
         }
+        Trace.endSection();
     }
 
     private void exitSearchAndExpand() {
diff --git a/src/com/android/launcher3/allapps/UserProfileManager.java b/src/com/android/launcher3/allapps/UserProfileManager.java
index 93b6b29..765c29c 100644
--- a/src/com/android/launcher3/allapps/UserProfileManager.java
+++ b/src/com/android/launcher3/allapps/UserProfileManager.java
@@ -18,6 +18,7 @@
 
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 
+import android.content.Context;
 import android.os.UserHandle;
 import android.os.UserManager;
 
@@ -26,6 +27,7 @@
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.pm.UserCache;
+import com.android.launcher3.util.ApiWrapper;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -69,14 +71,26 @@
     }
 
     /** Sets quiet mode as enabled/disabled for the profile type. */
-    protected void setQuietMode(boolean enabled) {
+    protected void setQuietMode(boolean enabled, Context context) {
         UI_HELPER_EXECUTOR.post(() ->
                 mUserCache.getUserProfiles()
                         .stream()
                         .filter(getUserMatcher())
                         .findFirst()
                         .ifPresent(userHandle ->
-                                mUserManager.requestQuietModeEnabled(enabled, userHandle)));
+                                setQuietModeSafely(enabled, userHandle, context)));
+    }
+
+    /**
+     * Sets Quiet Mode for Private Profile.
+     * If {@link SecurityException} is thrown, prompts the user to set this launcher as HOME app.
+     */
+    private void setQuietModeSafely(boolean enable, UserHandle userHandle, Context context) {
+        try {
+            mUserManager.requestQuietModeEnabled(enable, userHandle);
+        } catch (SecurityException ex) {
+            ApiWrapper.INSTANCE.get(context).assignDefaultHomeRole(context);
+        }
     }
 
     /** Sets current state for the profile type. */
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..6ebab5a 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(
@@ -74,20 +74,20 @@
      */
     public void setWorkProfileEnabled(boolean enabled) {
         updateCurrentState(STATE_TRANSITION);
-        setQuietMode(!enabled);
+        setQuietMode(!enabled, mAllApps.mActivityContext);
     }
 
     @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..fe11ee2 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();
         }
@@ -144,6 +150,7 @@
         mCallback.clearSearchResult();
         mInput.reset();
         mInput.clearFocus();
+        mInput.hideKeyboard();
         mQuery = null;
     }
 
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/dagger/LauncherComponentProvider.kt b/src/com/android/launcher3/dagger/LauncherComponentProvider.kt
new file mode 100644
index 0000000..5015e54
--- /dev/null
+++ b/src/com/android/launcher3/dagger/LauncherComponentProvider.kt
@@ -0,0 +1,57 @@
+/*
+ * 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.dagger
+
+import android.content.Context
+import android.view.LayoutInflater
+import com.android.launcher3.LauncherApplication
+
+/**
+ * Utility class to extract LauncherAppComponent from a context.
+ *
+ * If the context doesn't provide LauncherAppComponent by default, it creates a new one and
+ * associate it with that context
+ */
+object LauncherComponentProvider {
+
+    @JvmStatic
+    fun get(c: Context): LauncherAppComponent {
+        val app = c.applicationContext
+        if (app is LauncherApplication) return app.appComponent
+
+        val inflater = LayoutInflater.from(app)
+        val existingFilter = inflater.filter
+        if (existingFilter is Holder) return existingFilter.component
+
+        // Create a new component
+        return Holder(
+                DaggerLauncherAppComponent.builder().appContext(app).build()
+                    as LauncherAppComponent,
+                existingFilter,
+            )
+            .apply { inflater.filter = this }
+            .component
+    }
+
+    private data class Holder(
+        val component: LauncherAppComponent,
+        private val filter: LayoutInflater.Filter?,
+    ) : LayoutInflater.Filter {
+
+        override fun onLoadClass(clazz: Class<*>?) = filter?.onLoadClass(clazz) ?: true
+    }
+}
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 27ec838..7367f2e 100644
--- a/src/com/android/launcher3/graphics/GridCustomizationsProvider.java
+++ b/src/com/android/launcher3/graphics/GridCustomizationsProvider.java
@@ -36,18 +36,24 @@
 import android.text.TextUtils;
 import android.util.Log;
 
+import androidx.annotation.NonNull;
+
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.InvariantDeviceProfile.GridOption;
 import com.android.launcher3.LauncherAppState;
 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;
@@ -55,30 +61,45 @@
 /**
  * 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";
@@ -94,7 +115,9 @@
     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;
 
     // Set of all active previews used to track duplicate memory allocations
     private final Set<PreviewLifecycleObserver> mActivePreviews =
@@ -108,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)
@@ -159,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
@@ -216,20 +275,30 @@
     }
 
     @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) {
+        Context context = getContext();
+        if (context == null) {
+            return null;
+        }
         RunnableList lifeCycleTracker = new RunnableList();
         try {
             PreviewSurfaceRenderer renderer = new PreviewSurfaceRenderer(
@@ -267,7 +336,9 @@
         public final PreviewSurfaceRenderer renderer;
         public boolean destroyed = false;
 
-        PreviewLifecycleObserver(RunnableList lifeCycleTracker, PreviewSurfaceRenderer renderer) {
+        PreviewLifecycleObserver(
+                RunnableList lifeCycleTracker,
+                PreviewSurfaceRenderer renderer) {
             this.lifeCycleTracker = lifeCycleTracker;
             this.renderer = renderer;
             lifeCycleTracker.add(() -> destroyed = true);
@@ -283,12 +354,28 @@
                 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:
                     // Unknown command, destroy lifecycle
                     Log.d(TAG, "Unknown preview command: " + message.what + ", destroying preview");
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 1b23d75..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,6 +98,8 @@
     private final int mDisplayId;
     private final Display mDisplay;
     private final WallpaperColors mWallpaperColors;
+    private SparseIntArray mPreviewColorOverride;
+    @Nullable private Boolean mDarkMode;
     private final RunnableList mLifeCycleTracker;
 
     private final SurfaceControlViewHost mSurfaceControlViewHost;
@@ -110,6 +119,9 @@
             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);
@@ -217,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
@@ -258,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() {
@@ -300,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
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/DatabaseHelper.java b/src/com/android/launcher3/model/DatabaseHelper.java
index 8368256..ed4f492 100644
--- a/src/com/android/launcher3/model/DatabaseHelper.java
+++ b/src/com/android/launcher3/model/DatabaseHelper.java
@@ -56,6 +56,7 @@
 import java.net.URISyntaxException;
 import java.util.Arrays;
 import java.util.Locale;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.ToLongFunction;
 import java.util.stream.Collectors;
 
@@ -79,8 +80,8 @@
     private final Context mContext;
     private final ToLongFunction<UserHandle> mUserSerialProvider;
     private final Runnable mOnEmptyDbCreateCallback;
+    private final AtomicInteger mMaxItemId = new AtomicInteger(-1);
 
-    private int mMaxItemId = -1;
     public boolean mHotseatRestoreTableExists;
 
     /**
@@ -97,21 +98,19 @@
     protected void initIds() {
         // In the case where neither onCreate nor onUpgrade gets called, we read the maxId from
         // the DB here
-        if (mMaxItemId == -1) {
-            mMaxItemId = initializeMaxItemId(getWritableDatabase());
-        }
+        mMaxItemId.compareAndSet(-1, initializeMaxItemId(getWritableDatabase()));
     }
 
     @Override
     public void onCreate(SQLiteDatabase db) {
         if (LOGD) Log.d(TAG, "creating new launcher database");
 
-        mMaxItemId = 1;
+        mMaxItemId.set(1);
 
         addTableToDb(db, getDefaultUserSerial(), false /* optional */);
 
         // Fresh and clean launcher DB.
-        mMaxItemId = initializeMaxItemId(db);
+        mMaxItemId.set(initializeMaxItemId(db));
         mOnEmptyDbCreateCallback.run();
     }
 
@@ -451,11 +450,10 @@
     // after that point
     @Override
     public int generateNewItemId() {
-        if (mMaxItemId < 0) {
+        if (mMaxItemId.get() < 0) {
             throw new RuntimeException("Error: max item id was not initialized");
         }
-        mMaxItemId += 1;
-        return mMaxItemId;
+        return mMaxItemId.incrementAndGet();
     }
 
     /**
@@ -484,7 +482,7 @@
 
     public void checkId(ContentValues values) {
         int id = values.getAsInteger(Favorites._ID);
-        mMaxItemId = Math.max(id, mMaxItemId);
+        mMaxItemId.accumulateAndGet(id, Math::max);
     }
 
     private int initializeMaxItemId(SQLiteDatabase db) {
@@ -508,7 +506,7 @@
         int count = loader.loadLayout(db);
 
         // Ensure that the max ids are initialized
-        mMaxItemId = initializeMaxItemId(db);
+        mMaxItemId.set(initializeMaxItemId(db));
         return count;
     }
 
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..bfa00bd 100644
--- a/src/com/android/launcher3/model/GridSizeMigrationUtil.java
+++ b/src/com/android/launcher3/model/GridSizeMigrationDBController.java
@@ -17,17 +17,18 @@
 package com.android.launcher3.model;
 
 import static com.android.launcher3.Flags.enableSmartspaceRemovalToggle;
+import static com.android.launcher3.Flags.oneGridSpecs;
 import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME;
 import static com.android.launcher3.LauncherSettings.Favorites.TMP_TABLE;
 import static com.android.launcher3.Utilities.SHOULD_SHOW_FIRST_PAGE_WIDGET;
 import static com.android.launcher3.model.LoaderTask.SMARTSPACE_ON_HOME_SCREEN;
 import static com.android.launcher3.provider.LauncherDbUtils.copyTable;
 import static com.android.launcher3.provider.LauncherDbUtils.dropTable;
+import static com.android.launcher3.provider.LauncherDbUtils.shiftTableByXCells;
 
 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 +39,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 +57,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 +64,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 +80,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,25 +119,33 @@
             @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);
+
+            if (oneGridSpecs()) {
+                DbReader destReader = new DbReader(
+                        target.getWritableDatabase(), TABLE_NAME, context);
+                boolean shouldShiftCells = shouldShiftCells(destReader, srcDeviceState.getRows());
+                if (shouldShiftCells) {
+                    shiftTableByXCells(
+                            target.getWritableDatabase(),
+                            (destDeviceState.getRows() - srcDeviceState.getRows()),
+                            TABLE_NAME);
+                }
+            }
+
+            // Save current configuration, so that the migration does not run again.
             destDeviceState.writeToPrefs(context);
             return true;
         }
@@ -196,7 +206,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 +299,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 +338,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 +348,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 +394,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 +420,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 +443,31 @@
         }
     }
 
-    @VisibleForTesting
+    private static boolean shouldShiftCells(final DbReader destReader, final int srcGridRowCount) {
+        List<DbEntry> workspaceItems = destReader.loadAllWorkspaceEntries();
+        int firstPageItemsRowPosSum = workspaceItems.stream()
+                .filter(entry -> entry.screenId == 0)
+                .mapToInt(entry -> entry.cellY).sum();
+        int firstPageWorkspaceItemsCount = (int) workspaceItems.stream()
+                .filter(entry -> entry.screenId == 0).count();
+        if (firstPageWorkspaceItemsCount == 0) {
+            return false;
+        }
+        float srcGridMidPoint = srcGridRowCount / 2f;
+        float firstPageItemPosAvg = (float) firstPageItemsRowPosSum / firstPageWorkspaceItemsCount;
+        return (firstPageItemPosAvg >= srcGridMidPoint);
+    }
+
+
+    @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 +552,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 +671,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..75fd31e
--- /dev/null
+++ b/src/com/android/launcher3/model/GridSizeMigrationLogic.kt
@@ -0,0 +1,551 @@
+/*
+ * 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.Flags.oneGridSpecs
+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.LauncherSettings.Favorites.TABLE_NAME
+import com.android.launcher3.LauncherSettings.Favorites.TMP_TABLE
+import com.android.launcher3.Utilities
+import com.android.launcher3.config.FeatureFlags
+import com.android.launcher3.model.GridSizeMigrationDBController.DbReader
+import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction
+import com.android.launcher3.provider.LauncherDbUtils.copyTable
+import com.android.launcher3.provider.LauncherDbUtils.dropTable
+import com.android.launcher3.provider.LauncherDbUtils.shiftTableByXCells
+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\n srcDeviceState: " +
+                "$srcDeviceState\ndestDeviceState: $destDeviceState\nisDestNewDb: $isDestNewDb",
+        )
+
+        // 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)) {
+            Log.d(TAG, "Migrating to strictly taller grid")
+            copyTable(source, TABLE_NAME, target.writableDatabase, TABLE_NAME, context)
+            if (oneGridSpecs()) {
+                val destReader = DbReader(target.writableDatabase, TABLE_NAME, context)
+                val shouldShiftCells = shouldShiftCells(destReader, srcDeviceState.rows)
+                if (shouldShiftCells) {
+                    shiftTableByXCells(
+                        target.writableDatabase,
+                        (destDeviceState.rows - srcDeviceState.rows),
+                        TABLE_NAME,
+                    )
+                }
+            }
+            // Save current configuration, so that the migration does not run again.
+            destDeviceState.writeToPrefs(context)
+            return
+        }
+
+        copyTable(source, TABLE_NAME, target.writableDatabase, TMP_TABLE, context)
+
+        val migrationStartTime = System.currentTimeMillis()
+        try {
+            SQLiteTransaction(target.writableDatabase).use { t ->
+                val srcReader = DbReader(t.db, TMP_TABLE, context)
+                val destReader = DbReader(t.db, 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)
+
+                dropTable(t.db, 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)
+        }
+    }
+
+    private fun shouldShiftCells(destReader: DbReader, srcGridRowCount: Int): Boolean {
+        val workspaceItems = destReader.loadAllWorkspaceEntries()
+        val firstPageItemsRowPosSum =
+            workspaceItems.sumOf { entry -> if (entry.screenId == 0) entry.cellY else 0 }
+        val firstPageWorkspaceItemsCount = workspaceItems.count { entry -> entry.screenId == 0 }
+        if (firstPageWorkspaceItemsCount == 0) {
+            return false
+        }
+        val srcGridMidPoint = srcGridRowCount / 2f
+        val firstPageItemPosAvg = firstPageItemsRowPosSum / firstPageWorkspaceItemsCount.toFloat()
+        return (firstPageItemPosAvg >= srcGridMidPoint)
+    }
+
+    /** 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..83eace8 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() {
@@ -238,7 +246,7 @@
         TraceHelper.INSTANCE.beginSection(TAG);
         LoaderMemoryLogger memoryLogger = new LoaderMemoryLogger();
         mIsRestoreFromBackup =
-                (Boolean) LauncherPrefs.get(mApp.getContext()).get(IS_FIRST_LOAD_AFTER_RESTORE);
+                LauncherPrefs.get(mApp.getContext()).get(IS_FIRST_LOAD_AFTER_RESTORE);
         LauncherRestoreEventLogger restoreEventLogger = null;
         if (enableLauncherBrMetricsFixed()) {
             restoreEventLogger = LauncherRestoreEventLogger.Companion
@@ -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.
@@ -258,21 +266,21 @@
                 sanitizeFolders(mItemsDeleted);
                 sanitizeAppPairs();
                 sanitizeWidgetsShortcutsAndPackages();
-                logASplit("sanitizeData");
+                logASplit("sanitizeData finished");
             }
 
             verifyNotStopped();
             mLauncherBinder.bindWorkspace(true /* incrementBindId */, /* isBindSync= */ false);
-            logASplit("bindWorkspace");
+            logASplit("bindWorkspace finished");
 
             mModelDelegate.workspaceLoadComplete();
             // Notify the installer packages of packages with active installs on the first screen.
             sendFirstScreenActiveInstallsBroadcast();
-            logASplit("sendFirstScreenBroadcast");
+            logASplit("sendFirstScreenBroadcast finished");
 
             // Take a break
             waitForIdle();
-            logASplit("step 1 complete");
+            logASplit("step 1 loading workspace complete");
             verifyNotStopped();
 
             // second step
@@ -283,16 +291,11 @@
             } finally {
                 Trace.endSection();
             }
-            logASplit("loadAllApps");
+            logASplit("loadAllApps finished");
 
-            if (FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) {
-                mModelDelegate.loadAndBindAllAppsItems(mUserManagerState,
-                        mLauncherBinder.mCallbacksList, mShortcutKeyToPinnedShortcuts);
-                logASplit("allAppsDelegateItems");
-            }
             verifyNotStopped();
             mLauncherBinder.bindAllApps();
-            logASplit("bindAllApps");
+            logASplit("bindAllApps finished");
 
             verifyNotStopped();
             IconCacheUpdateHandler updateHandler = mIconCache.getUpdateHandler();
@@ -300,44 +303,53 @@
             updateHandler.updateIcons(allActivityList,
                     LauncherActivityCachingLogic.INSTANCE,
                     mApp.getModel()::onPackageIconsUpdated);
-            logASplit("update icon cache");
+            logASplit("update AllApps icon cache finished");
 
             verifyNotStopped();
-            logASplit("save shortcuts in icon cache");
-            updateHandler.updateIcons(allShortcuts, new ShortcutCachingLogic(),
+            logASplit("saving all shortcuts in icon cache");
+            updateHandler.updateIcons(allShortcuts, CacheableShortcutCachingLogic.INSTANCE,
                     mApp.getModel()::onPackageIconsUpdated);
 
             // Take a break
             waitForIdle();
-            logASplit("step 2 complete");
+            logASplit("step 2 loading AllApps complete");
             verifyNotStopped();
 
             // third step
             List<ShortcutInfo> allDeepShortcuts = loadDeepShortcuts();
-            logASplit("loadDeepShortcuts");
+            logASplit("loadDeepShortcuts finished");
 
             verifyNotStopped();
             mLauncherBinder.bindDeepShortcuts();
-            logASplit("bindDeepShortcuts");
+            logASplit("bindDeepShortcuts finished");
 
             verifyNotStopped();
-            logASplit("save deep shortcuts in icon cache");
-            updateHandler.updateIcons(allDeepShortcuts,
-                    new ShortcutCachingLogic(), (pkgs, user) -> { });
+            logASplit("saving deep shortcuts in icon cache");
+            updateHandler.updateIcons(
+                    convertShortcutsToCacheableShortcuts(allDeepShortcuts, allActivityList),
+                    CacheableShortcutCachingLogic.INSTANCE,
+                    (pkgs, user) -> { });
 
             // Take a break
             waitForIdle();
-            logASplit("step 3 complete");
+            logASplit("step 3 loading all shortcuts complete");
             verifyNotStopped();
 
             // fourth step
-            List<ComponentWithLabelAndIcon> allWidgetsList =
-                    mBgDataModel.widgetsModel.update(mApp, null);
-            logASplit("load widgets");
+            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 finished");
 
             verifyNotStopped();
             mLauncherBinder.bindWidgets();
-            logASplit("bindWidgets");
+            logASplit("bindWidgets finished");
             verifyNotStopped();
             LauncherPrefs prefs = LauncherPrefs.get(mApp.getContext());
 
@@ -345,7 +357,7 @@
                 mLauncherBinder.bindSmartspaceWidget();
                 // Turn off pref.
                 prefs.putSync(SHOULD_SHOW_SMARTSPACE.to(false));
-                logASplit("bindSmartspaceWidget");
+                logASplit("bindSmartspaceWidget finished");
                 verifyNotStopped();
             } else if (!enableSmartspaceAsAWidget() && WIDGET_ON_FIRST_SCREEN
                     && !prefs.get(LauncherPrefs.SHOULD_SHOW_SMARTSPACE)) {
@@ -353,16 +365,10 @@
                 prefs.putSync(SHOULD_SHOW_SMARTSPACE.to(true));
             }
 
-            if (FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) {
-                mModelDelegate.loadAndBindOtherItems(mLauncherBinder.mCallbacksList);
-                logASplit("otherDelegateItems");
-                verifyNotStopped();
-            }
-
+            logASplit("saving all widgets in icon cache");
             updateHandler.updateIcons(allWidgetsList,
-                    new CachedObjectCachingLogic(mApp.getContext()),
+                    CachedObjectCachingLogic.INSTANCE,
                     mApp.getModel()::onWidgetLabelsUpdated);
-            logASplit("save widgets in icon cache");
 
             // fifth step
             loadFolderNames();
@@ -397,7 +403,7 @@
     }
 
     protected void loadWorkspace(
-            List<ShortcutInfo> allDeepShortcuts,
+            List<CacheableShortcutInfo> allDeepShortcuts,
             String selection,
             LoaderMemoryLogger memoryLogger,
             @Nullable LauncherRestoreEventLogger restoreEventLogger
@@ -408,22 +414,15 @@
         } finally {
             Trace.endSection();
         }
-        logASplit("loadWorkspace");
+        logASplit("loadWorkspace finished");
 
-        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,8 +431,16 @@
         final WidgetInflater widgetInflater = new WidgetInflater(context);
 
         ModelDbController dbController = mApp.getModel().getModelDbController();
-        dbController.tryMigrateDB(restoreEventLogger);
-        Log.d(TAG, "loadWorkspace: loading default favorites");
+        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 if necessary");
         dbController.loadDefaultFavoritesIfNecessary();
 
         synchronized (mBgDataModel) {
@@ -446,7 +453,7 @@
                 mInstallingPkgsCached = installingPkgs;
             }
             installingPkgs.forEach(mApp.getIconCache()::updateSessionCache);
-            FileLog.d(TAG, "loadWorkspace: Packages with active install sessions: "
+            FileLog.d(TAG, "loadWorkspace: Packages with active install/update sessions: "
                     + installingPkgs.keySet().stream().map(info -> info.mPackageName).toList());
 
             mFirstScreenBroadcast = new FirstScreenBroadcast(installingPkgs);
@@ -471,22 +478,24 @@
                         widgetInflater, mPmHelper, iconRequestInfos, unlockedUsers,
                         allDeepShortcuts);
 
-                while (!mStopped && c.moveToNext()) {
-                    itemProcessor.processItem();
+                if (mStopped) {
+                    Log.w(TAG, "loadWorkspaceImpl: Loader stopped, skipping item processing");
+                } else {
+                    while (!mStopped && c.moveToNext()) {
+                        itemProcessor.processItem();
+                    }
                 }
                 tryLoadWorkspaceIconsInBulk(iconRequestInfos);
             } finally {
                 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..7ba2dad 100644
--- a/src/com/android/launcher3/model/ModelLauncherCallbacks.kt
+++ b/src/com/android/launcher3/model/ModelLauncherCallbacks.kt
@@ -17,10 +17,12 @@
 package com.android.launcher3.model
 
 import android.content.pm.LauncherApps
+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 +30,9 @@
 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.util.PackageUserKey
 import java.util.function.Consumer
 
 /**
@@ -35,7 +40,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 +54,7 @@
     override fun onPackageLoadingProgressChanged(
         packageName: String,
         user: UserHandle,
-        progress: Float
+        progress: Float,
     ) {
         taskExecutor.accept(PackageIncrementalDownloadUpdatedTask(packageName, user, progress))
     }
@@ -62,7 +67,7 @@
     override fun onPackagesAvailable(
         vararg packageNames: String,
         user: UserHandle,
-        replacing: Boolean
+        replacing: Boolean,
     ) {
         taskExecutor.accept(PackageUpdatedTask(OP_UPDATE, user, *packageNames))
     }
@@ -74,7 +79,7 @@
     override fun onPackagesUnavailable(
         packageNames: Array<String>,
         user: UserHandle,
-        replacing: Boolean
+        replacing: Boolean,
     ) {
         if (!replacing) {
             taskExecutor.accept(PackageUpdatedTask(OP_UNAVAILABLE, user, *packageNames))
@@ -88,7 +93,7 @@
     override fun onShortcutsChanged(
         packageName: String,
         shortcuts: MutableList<ShortcutInfo>,
-        user: UserHandle
+        user: UserHandle,
     ) {
         taskExecutor.accept(ShortcutsChangedTask(packageName, shortcuts, user, true))
     }
@@ -98,6 +103,37 @@
         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()
+            }
+        }
+    }
+
     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/InstallSessionTracker.java b/src/com/android/launcher3/pm/InstallSessionTracker.java
index 856c294..b9c928c 100644
--- a/src/com/android/launcher3/pm/InstallSessionTracker.java
+++ b/src/com/android/launcher3/pm/InstallSessionTracker.java
@@ -33,7 +33,6 @@
 import androidx.annotation.WorkerThread;
 
 import com.android.launcher3.Flags;
-import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.util.PackageUserKey;
 
 import java.lang.ref.WeakReference;
@@ -79,7 +78,7 @@
         }
         SessionInfo sessionInfo = pushSessionDisplayToLauncher(sessionId, helper, callback);
         if (sessionInfo != null) {
-            FileLog.d(TAG, "onCreated: Install session created for"
+            Log.d(TAG, "onCreated: Install session created for"
                     + " appPackageName=" + sessionInfo.getAppPackageName()
                     + ", sessionId=" + sessionInfo.getSessionId()
                     + ", appIcon=" + sessionInfo.getAppIcon()
@@ -111,7 +110,7 @@
         activeSessions.remove(sessionId);
 
         if (key != null && key.mPackageName != null) {
-            FileLog.d(TAG, "onFinished: active install session finished for"
+            Log.d(TAG, "onFinished: active install session finished for"
                     + " appPackageName=" + key.mPackageName
                     + ", sessionId=" + sessionId
                     + ", success=" + success);
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/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..6f1d0dd
--- /dev/null
+++ b/src/com/android/launcher3/provider/LauncherDbUtils.kt
@@ -0,0 +1,266 @@
+/*
+ * 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"
+                )
+            }
+        }
+    }
+
+    @JvmStatic
+    fun shiftTableByXCells(db: SQLiteDatabase, x: Int, toTable: String) {
+        db.run { execSQL("UPDATE $toTable SET cellY = cellY + $x") }
+    }
+
+    /**
+     * 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/recyclerview/AllAppsRecyclerViewPool.kt b/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt
index 82229f8..e4c50f0 100644
--- a/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt
+++ b/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt
@@ -18,18 +18,23 @@
 
 import android.content.Context
 import android.util.Log
+import android.view.InflateException
+import androidx.annotation.VisibleForTesting
+import androidx.annotation.VisibleForTesting.Companion.PROTECTED
 import androidx.recyclerview.widget.RecyclerView
 import androidx.recyclerview.widget.RecyclerView.RecycledViewPool
 import androidx.recyclerview.widget.RecyclerView.ViewHolder
 import com.android.launcher3.BubbleTextView
 import com.android.launcher3.BuildConfig
 import com.android.launcher3.allapps.BaseAllAppsAdapter
+import com.android.launcher3.config.FeatureFlags
 import com.android.launcher3.util.CancellableTask
 import com.android.launcher3.util.Executors.MAIN_EXECUTOR
 import com.android.launcher3.util.Executors.VIEW_PREINFLATION_EXECUTOR
 import com.android.launcher3.util.Themes
 import com.android.launcher3.views.ActivityContext
 import com.android.launcher3.views.ActivityContext.ActivityContextDelegate
+import java.lang.IllegalStateException
 
 const val PREINFLATE_ICONS_ROW_COUNT = 4
 const val EXTRA_ICONS_COUNT = 2
@@ -39,10 +44,11 @@
  * [RecyclerView]. The view inflation will happen on background thread and inflated [ViewHolder]s
  * will be added to [RecycledViewPool] on main thread.
  */
-class AllAppsRecyclerViewPool<T> : RecycledViewPool() {
+class AllAppsRecyclerViewPool<T> : RecycledViewPool() where T : Context, T : ActivityContext {
 
     var hasWorkProfile = false
-    private var mCancellableTask: CancellableTask<List<ViewHolder>>? = null
+    @VisibleForTesting(otherwise = PROTECTED)
+    var mCancellableTask: CancellableTask<List<ViewHolder>>? = null
 
     companion object {
         private const val TAG = "AllAppsRecyclerViewPool"
@@ -53,7 +59,7 @@
     /**
      * Preinflate app icons. If all apps RV cannot be scrolled down, we don't need to preinflate.
      */
-    fun <T> preInflateAllAppsViewHolders(context: T) where T : Context, T : ActivityContext {
+    fun preInflateAllAppsViewHolders(context: T) {
         val appsView = context.appsView ?: return
         val activeRv: RecyclerView = appsView.activeRecyclerView ?: return
         val preInflateCount = getPreinflateCount(context)
@@ -97,36 +103,65 @@
                 override fun getLayoutManager(): RecyclerView.LayoutManager? = null
             }
 
+        preInflateAllAppsViewHolders(
+            adapter,
+            BaseAllAppsAdapter.VIEW_TYPE_ICON,
+            activeRv,
+            preInflateCount,
+        ) {
+            getPreinflateCount(context)
+        }
+    }
+
+    @VisibleForTesting(otherwise = PROTECTED)
+    fun preInflateAllAppsViewHolders(
+        adapter: RecyclerView.Adapter<*>,
+        viewType: Int,
+        activeRv: RecyclerView,
+        preInflationCount: Int,
+        preInflationCountProvider: () -> Int,
+    ) {
+        if (preInflationCount <= 0) {
+            return
+        }
         mCancellableTask?.cancel()
         var task: CancellableTask<List<ViewHolder>>? = null
         task =
             CancellableTask(
                 {
                     val list: ArrayList<ViewHolder> = ArrayList()
-                    for (i in 0 until preInflateCount) {
+                    for (i in 0 until preInflationCount) {
                         if (task?.canceled == true) {
                             break
                         }
                         // If activeRv's layout manager has been reset to null on main thread, skip
                         // the preinflation as we cannot generate correct LayoutParams
                         if (activeRv.layoutManager == null) {
+                            list.clear()
                             break
                         }
-                        list.add(
-                            adapter.createViewHolder(activeRv, BaseAllAppsAdapter.VIEW_TYPE_ICON)
-                        )
+                        try {
+                            list.add(adapter.createViewHolder(activeRv, viewType))
+                        } catch (e: InflateException) {
+                            list.clear()
+                            // It's still possible for UI thread to set activeRv's layout manager to
+                            // null and we should break the loop and cancel the preinflation.
+                            break
+                        }
                     }
                     list
                 },
                 MAIN_EXECUTOR,
                 { viewHolders ->
-                    for (i in 0 until minOf(viewHolders.size, getPreinflateCount(context))) {
+                    // Run preInflationCountProvider again as the needed VH might have changed
+                    val newPreInflationCount = preInflationCountProvider.invoke()
+                    for (i in 0 until minOf(viewHolders.size, newPreInflationCount)) {
                         putRecycledView(viewHolders[i])
                     }
                 },
             )
         mCancellableTask = task
-        VIEW_PREINFLATION_EXECUTOR.submit(mCancellableTask)
+        VIEW_PREINFLATION_EXECUTOR.execute(mCancellableTask)
     }
 
     /**
@@ -143,10 +178,11 @@
      * app icons plus [EXTRA_ICONS_COUNT] is the magic minimal count of app icons to preinflate to
      * suffice fast scrolling.
      *
-     * Note that we need to preinfate extra app icons in size of one all apps pages, so that opening
-     * all apps don't need to inflate app icons.
+     * Note that if [FeatureFlags.ALL_APPS_GONE_VISIBILITY] is enabled, we need to preinfate extra
+     * app icons in size of one all apps pages, so that opening all apps don't need to inflate app
+     * icons.
      */
-    fun <T> getPreinflateCount(context: T): Int where T : Context, T : ActivityContext {
+    fun getPreinflateCount(context: T): Int {
         var targetPreinflateCount =
             PREINFLATE_ICONS_ROW_COUNT * context.deviceProfile.numShownAllAppsColumns +
                 EXTRA_ICONS_COUNT
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..5068b48 100644
--- a/src/com/android/launcher3/settings/SettingsActivity.java
+++ b/src/com/android/launcher3/settings/SettingsActivity.java
@@ -22,10 +22,12 @@
 
 import static com.android.launcher3.BuildConfig.IS_DEBUG_DEVICE;
 import static com.android.launcher3.BuildConfig.IS_STUDIO_BUILD;
+import static com.android.launcher3.InvariantDeviceProfile.TYPE_MULTI_DISPLAY;
 import static com.android.launcher3.states.RotationHelper.ALLOW_ROTATION_PREFERENCE_KEY;
 
 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;
@@ -51,6 +53,7 @@
 
 import com.android.launcher3.BuildConfig;
 import com.android.launcher3.Flags;
+import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherFiles;
 import com.android.launcher3.R;
 import com.android.launcher3.states.RotationHelper;
@@ -66,6 +69,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 +241,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 +291,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 +306,32 @@
                     // 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()
+                            // adding this condition until fixing b/378972567
+                            || InvariantDeviceProfile.INSTANCE.get(getContext()).deviceType
+                            == TYPE_MULTI_DISPLAY) {
+                        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/quickstep/src/com/android/quickstep/task/viewmodel/TaskViewModel.kt b/src/com/android/launcher3/shapes/AppShape.kt
similarity index 71%
rename from quickstep/src/com/android/quickstep/task/viewmodel/TaskViewModel.kt
rename to src/com/android/launcher3/shapes/AppShape.kt
index ec75d59..68200a0 100644
--- a/quickstep/src/com/android/quickstep/task/viewmodel/TaskViewModel.kt
+++ b/src/com/android/launcher3/shapes/AppShape.kt
@@ -14,12 +14,6 @@
  * limitations under the License.
  */
 
-package com.android.quickstep.task.viewmodel
+package com.android.launcher3.shapes
 
-import androidx.lifecycle.ViewModel
-
-class TaskViewModel(private val taskViewData: TaskViewData) : ViewModel() {
-    fun updateScale(scale: Float) {
-        taskViewData.scale.value = scale
-    }
-}
+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..467a7ec 100644
--- a/src/com/android/launcher3/util/ApiWrapper.java
+++ b/src/com/android/launcher3/util/ApiWrapper.java
@@ -17,13 +17,13 @@
 package com.android.launcher3.util;
 
 import static com.android.launcher3.LauncherConstants.ActivityCodes.REQUEST_HOME_ROLE;
-import static com.android.launcher3.util.MainThreadInitializedObject.forOverride;
 
 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.ShortcutInfo;
 import android.graphics.drawable.ColorDrawable;
@@ -32,28 +32,35 @@
 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 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;
     }
 
@@ -156,8 +163,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..3201bd1
--- /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 final 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, isHomeStarted(context));
+    }
+
+    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..a245761 100644
--- a/src/com/android/launcher3/util/DaggerSingletonObject.java
+++ b/src/com/android/launcher3/util/DaggerSingletonObject.java
@@ -18,8 +18,8 @@
 
 import android.content.Context;
 
-import com.android.launcher3.LauncherApplication;
 import com.android.launcher3.dagger.LauncherAppComponent;
+import com.android.launcher3.dagger.LauncherComponentProvider;
 
 import java.util.function.Function;
 
@@ -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) {
@@ -37,8 +37,6 @@
     }
 
     public T get(Context context) {
-        LauncherAppComponent component =
-                ((LauncherApplication) context.getApplicationContext()).getAppComponent();
-        return mFunction.apply(component);
+        return mFunction.apply(LauncherComponentProvider.get(context));
     }
 }
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..8a1d923
--- /dev/null
+++ b/src/com/android/launcher3/util/MSDLPlayerWrapper.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.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 com.google.android.msdl.logging.MSDLEvent;
+
+import java.io.PrintWriter;
+import java.util.List;
+
+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);
+    }
+
+    public List<MSDLEvent> getHistory() {
+        return mMSDLPlayer.getHistory();
+    }
+
+    /** Print the latest history of MSDL tokens played */
+    public void dump(String prefix, PrintWriter writer) {
+        writer.println(prefix + mMSDLPlayer.toString());
+        writer.println(prefix + "MSDLPlayerWrapper history of latest events:");
+        List<MSDLEvent> events = getHistory();
+        for (MSDLEvent event: events) {
+            writer.println(prefix + "\t" + event);
+        }
+    }
+}
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/SplitConfigurationOptions.java b/src/com/android/launcher3/util/SplitConfigurationOptions.java
index f457e4e..44a7c6f 100644
--- a/src/com/android/launcher3/util/SplitConfigurationOptions.java
+++ b/src/com/android/launcher3/util/SplitConfigurationOptions.java
@@ -73,7 +73,27 @@
      */
     public static final int STAGE_TYPE_SIDE = 1;
 
-    @IntDef({STAGE_TYPE_UNDEFINED, STAGE_TYPE_MAIN, STAGE_TYPE_SIDE})
+    /**
+     * Position independent stage identifier for a given Stage
+     */
+    public static final int STAGE_TYPE_A = 2;
+    /**
+     * Position independent stage identifier for a given Stage
+     */
+    public static final int STAGE_TYPE_B = 3;
+    /**
+     * Position independent stage identifier for a given Stage
+     */
+    public static final int STAGE_TYPE_C = 4;
+
+    @IntDef({
+            STAGE_TYPE_UNDEFINED,
+            STAGE_TYPE_MAIN,
+            STAGE_TYPE_SIDE,
+            STAGE_TYPE_A,
+            STAGE_TYPE_B,
+            STAGE_TYPE_C
+    })
     public @interface StageType {}
     ///////////////////////////////////
 
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/BaseWidgetSheet.java b/src/com/android/launcher3/widget/BaseWidgetSheet.java
index 1c0d94c..fda5175 100644
--- a/src/com/android/launcher3/widget/BaseWidgetSheet.java
+++ b/src/com/android/launcher3/widget/BaseWidgetSheet.java
@@ -20,6 +20,7 @@
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.anim.AnimatorListeners.forSuccessCallback;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WIDGET_ADD_BUTTON_TAP;
+import static com.android.window.flags.Flags.predictiveBackThreeButtonNav;
 
 import android.content.Context;
 import android.graphics.Canvas;
@@ -128,6 +129,17 @@
     }
 
     @Override
+    public void setScaleY(float scaleY) {
+        super.setScaleY(scaleY);
+        if (predictiveBackThreeButtonNav() && mNavBarScrimHeight > 0) {
+            // Call invalidate to prevent navbar scrim from scaling. The navbar scrim is drawn
+            // directly onto the canvas. To prevent it from being scaled with the canvas, there's a
+            // counter scale applied in dispatchDraw.
+            invalidate();
+        }
+    }
+
+    @Override
     public final void onClick(View v) {
         WidgetCell wc;
         if (v instanceof WidgetCell view) {
@@ -318,8 +330,10 @@
         super.dispatchDraw(canvas);
 
         if (mNavBarScrimHeight > 0) {
-            canvas.drawRect(0, getHeight() - mNavBarScrimHeight, getWidth(), getHeight(),
-                    mNavBarScrimPaint);
+            float left = (getWidth() - getWidth() / getScaleX()) / 2;
+            float top = getHeight() / 2f + (getHeight() / 2f - mNavBarScrimHeight) / getScaleY();
+            canvas.drawRect(left, top, getWidth() / getScaleX(),
+                    top + mNavBarScrimHeight / getScaleY(), mNavBarScrimPaint);
         }
     }
 
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..2c07fd9 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.useSystemRadiusForAppWidgets;
+
 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 (useSystemRadiusForAppWidgets()) {
+            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..e4fecc5 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -63,7 +63,6 @@
         "src/com/android/launcher3/dragging/TaplDragTest.java",
         "src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java",
         "src/com/android/launcher3/ui/TaplTestsLauncher3Test.java",
-        "src/com/android/launcher3/ui/widget/TaplWidgetPickerTest.java",
         "src/com/android/launcher3/ui/workspace/TaplWorkspaceTest.java",
     ],
 }
@@ -98,6 +97,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 +112,9 @@
     asset_dirs: ["assets"],
     // TODO(b/319712088): re-enable use_resource_processor
     use_resource_processor: false,
+    static_libs: [
+        "kotlin-reflect",
+    ],
 }
 
 android_test {
@@ -143,6 +147,7 @@
     platform_apis: true,
     test_config: "Launcher3Tests.xml",
     data: [":Launcher3"],
+    plugins: ["dagger2-compiler"],
     test_suites: ["general-tests"],
 }
 
@@ -167,6 +172,7 @@
         "multivalentTests/src/**/*.java",
         "multivalentTests/src/**/*.kt",
         "src/com/android/launcher3/ui/AbstractLauncherUiTest.java",
+        "src/com/android/launcher3/ui/BaseLauncherTaplTest.java",
         "tapl/com/android/launcher3/tapl/*.java",
         "tapl/com/android/launcher3/tapl/*.kt",
     ],
@@ -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 2553cf9..68e493d 100644
--- a/tests/AndroidManifest-common.xml
+++ b/tests/AndroidManifest-common.xml
@@ -420,6 +420,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/ReorderWidgets/push_reorder_case b/tests/assets/ReorderWidgets/push_reorder_case
index 1eacfae..73b67d0 100644
--- a/tests/assets/ReorderWidgets/push_reorder_case
+++ b/tests/assets/ReorderWidgets/push_reorder_case
@@ -39,6 +39,6 @@
 board: 6x5
 xxxxxx
 bbbb--
---m---
---aaa-
---ddd-
\ No newline at end of file
+--maaa
+--ddd-
+------
\ No newline at end of file
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..c4519eb 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,7 @@
         deviceSpecFolded: DeviceSpec,
         isLandscape: Boolean = false,
         isGestureMode: Boolean = true,
-        isFolded: Boolean = false
+        isFolded: Boolean = false,
     ) {
         val (unfoldedNaturalX, unfoldedNaturalY) = deviceSpecUnfolded.naturalSize
         val unfoldedWindowsBounds =
@@ -182,7 +184,7 @@
         val perDisplayBoundsCache =
             mapOf(
                 unfoldedDisplayInfo to unfoldedWindowsBounds,
-                foldedDisplayInfo to foldedWindowsBounds
+                foldedDisplayInfo to foldedWindowsBounds,
             )
 
         if (isFolded) {
@@ -191,7 +193,7 @@
                 displayInfo = foldedDisplayInfo,
                 rotation = if (isLandscape) Surface.ROTATION_90 else Surface.ROTATION_0,
                 isGestureMode = isGestureMode,
-                densityDpi = deviceSpecFolded.densityDpi
+                densityDpi = deviceSpecFolded.densityDpi,
             )
         } else {
             initializeCommonVars(
@@ -199,7 +201,7 @@
                 displayInfo = unfoldedDisplayInfo,
                 rotation = if (isLandscape) Surface.ROTATION_0 else Surface.ROTATION_90,
                 isGestureMode = isGestureMode,
-                densityDpi = deviceSpecUnfolded.densityDpi
+                densityDpi = deviceSpecUnfolded.densityDpi,
             )
         }
     }
@@ -208,7 +210,7 @@
         deviceSpec: DeviceSpec,
         isGestureMode: Boolean,
         naturalX: Int,
-        naturalY: Int
+        naturalY: Int,
     ): List<WindowBounds> {
         val buttonsNavHeight = Utilities.dpToPx(48f, deviceSpec.densityDpi)
 
@@ -217,14 +219,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 +235,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 +266,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 +275,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 +310,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/DeleteDropTargetTest.kt b/tests/multivalentTests/src/com/android/launcher3/DeleteDropTargetTest.kt
index 46e66e4..fa368e5 100644
--- a/tests/multivalentTests/src/com/android/launcher3/DeleteDropTargetTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/DeleteDropTargetTest.kt
@@ -1,20 +1,36 @@
 package com.android.launcher3
 
 import android.content.Context
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
 import androidx.test.core.app.ApplicationProvider.getApplicationContext
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.launcher3.Utilities.*
+import com.android.launcher3.dragndrop.DragView
 import com.android.launcher3.util.ActivityContextWrapper
+import com.android.launcher3.util.MSDLPlayerWrapper
+import com.google.android.msdl.data.model.MSDLToken
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.verifyNoMoreInteractions
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class DeleteDropTargetTest {
 
+    @get:Rule val mSetFlagsRule = SetFlagsRule()
+
+    @Mock private val msdlPlayerWrapper = mock<MSDLPlayerWrapper>()
+
     private var mContext: Context = ActivityContextWrapper(getApplicationContext())
 
     // Use a non-abstract class implementation
@@ -37,4 +53,16 @@
         // A lot of space for text so the text should not be clipped
         assertThat(buttonDropTarget.isTextClippedVertically(1000)).isFalse()
     }
+
+    @Test
+    @EnableFlags(Flags.FLAG_MSDL_FEEDBACK)
+    fun onDragEnter_performsMSDLSwipeThresholdFeedback() {
+        buttonDropTarget.setMSDLPlayerWrapper(msdlPlayerWrapper)
+        val target = DropTarget.DragObject(mContext)
+        target.dragView = mock<DragView<*>>()
+        buttonDropTarget.onDragEnter(target)
+
+        verify(msdlPlayerWrapper, times(1)).playToken(eq(MSDLToken.SWIPE_THRESHOLD_INDICATOR))
+        verifyNoMoreInteractions(msdlPlayerWrapper)
+    }
 }
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/dagger/LauncherComponentProviderTest.kt b/tests/multivalentTests/src/com/android/launcher3/dagger/LauncherComponentProviderTest.kt
new file mode 100644
index 0000000..9255877
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/dagger/LauncherComponentProviderTest.kt
@@ -0,0 +1,74 @@
+/*
+ * 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.dagger
+
+import android.content.Context
+import android.content.ContextWrapper
+import android.view.ContextThemeWrapper
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.launcher3.R
+import com.android.launcher3.util.LauncherModelHelper.SandboxModelContext
+import com.android.launcher3.util.LauncherModelHelper.TEST_PACKAGE
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNotSame
+import org.junit.Assert.assertSame
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class LauncherComponentProviderTest {
+
+    val app: Context = ApplicationProvider.getApplicationContext()
+
+    @Test
+    fun `returns same component as Launcher application`() {
+        val c = SandboxModelContext()
+        assertSame(c.appComponent, LauncherComponentProvider.get(c))
+        assertNotSame(LauncherComponentProvider.get(c), LauncherComponentProvider.get(app))
+    }
+
+    @Test
+    fun `returns same component for isolated context`() {
+        val c = IsolatedContext()
+
+        // Same component is returned for multiple calls, irrespective of the wrappers
+        assertNotNull(LauncherComponentProvider.get(c))
+        assertSame(
+            LauncherComponentProvider.get(c),
+            LauncherComponentProvider.get(ContextThemeWrapper(c, R.style.LauncherTheme)),
+        )
+
+        // Different than main application
+        assertNotSame(LauncherComponentProvider.get(c), LauncherComponentProvider.get(app))
+    }
+
+    @Test
+    fun `different components for different isolated context`() {
+        val c1 = IsolatedContext()
+        val c2 = IsolatedContext()
+
+        assertNotNull(LauncherComponentProvider.get(c1))
+        assertNotNull(LauncherComponentProvider.get(c2))
+        assertNotSame(LauncherComponentProvider.get(c1), LauncherComponentProvider.get(c2))
+    }
+
+    inner class IsolatedContext : ContextWrapper(app.createPackageContext(TEST_PACKAGE, 0)) {
+
+        override fun getApplicationContext(): Context = this
+    }
+}
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..9b4bd71 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,30 @@
 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.After;
 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 {
@@ -66,6 +100,11 @@
                 new LauncherIconProvider(mContext));
     }
 
+    @After
+    public void tearDown() {
+        mIconCache.close();
+    }
+
     @Test
     public void getShortcutInfoBadge_nullComponent_overrideAllowed() throws Exception {
         String overridePackage = "com.android.settings";
@@ -112,6 +151,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/DatabaseHelperTest.kt b/tests/multivalentTests/src/com/android/launcher3/model/DatabaseHelperTest.kt
index c9ea421..09752b8 100644
--- a/tests/multivalentTests/src/com/android/launcher3/model/DatabaseHelperTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/model/DatabaseHelperTest.kt
@@ -1,5 +1,6 @@
 package com.android.launcher3.model
 
+import android.content.Context
 import android.database.sqlite.SQLiteDatabase
 import android.os.UserHandle
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -11,8 +12,10 @@
 import com.android.launcher3.pm.UserCache
 import com.android.launcher3.provider.LauncherDbUtils
 import java.util.function.ToLongFunction
+import org.junit.After
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -24,6 +27,19 @@
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class DatabaseHelperTest {
+    val context: Context = InstrumentationRegistry.getInstrumentation().targetContext
+    // v30 - 21 columns
+    lateinit var db: SQLiteDatabase
+
+    @Before
+    fun setUp() {
+        db = FactitiousDbController(context, INSERTION_SQL).inMemoryDb
+    }
+
+    @After
+    fun tearDown() {
+        db.close()
+    }
 
     /**
      * b/304687723 occurred when a return was accidentally added to a case statement in
@@ -33,13 +49,11 @@
      */
     @Test
     fun onUpgrade_to_version_32_from_30() {
-        val context = InstrumentationRegistry.getInstrumentation().targetContext
         val userSerialProvider =
             ToLongFunction<UserHandle> {
                 UserCache.INSTANCE.get(context).getSerialNumberForUser(it)
             }
         val dbHelper = DatabaseHelper(context, null, userSerialProvider) {}
-        val db = FactitiousDbController(context, INSERTION_SQL).inMemoryDb
 
         dbHelper.onUpgrade(db, 30, 32)
 
@@ -54,9 +68,6 @@
      */
     @Test
     fun after_migrating_from_db_v30_to_v32_copy_table() {
-        val context = InstrumentationRegistry.getInstrumentation().targetContext
-        val db = FactitiousDbController(context, INSERTION_SQL).inMemoryDb // v30 - 21 columns
-
         addTableToDb(db, 1, true, TMP_TABLE)
         LauncherDbUtils.copyTable(db, TABLE_NAME, db, TMP_TABLE, context)
 
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 371bac2..e8f778f 100644
--- a/tests/multivalentTests/src/com/android/launcher3/model/FolderIconLoadTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/model/FolderIconLoadTest.kt
@@ -19,19 +19,17 @@
 import androidx.test.filters.SmallTest
 import com.android.launcher3.LauncherAppState
 import com.android.launcher3.icons.BitmapInfo
+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
 
@@ -40,8 +38,6 @@
 @RunWith(AndroidJUnit4::class)
 class FolderIconLoadTest {
 
-    @get:Rule(order = 0) val modelTestRule = ModelTestRule()
-
     private lateinit var modelHelper: LauncherModelHelper
 
     private val uniqueActivities =
@@ -147,14 +143,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..eee6191 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
@@ -79,12 +82,26 @@
 
     @After
     fun tearDown() {
+        db.close()
         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 +130,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 +223,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 +273,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 +348,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 +400,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 +452,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 +519,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 +580,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 +624,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 +647,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 +691,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 +725,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 +766,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 +797,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..63359ec 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;
@@ -112,6 +109,7 @@
 
     @After
     public void tearDown() {
+        mCursor.close();
         mModelHelper.destroy();
     }
 
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..c30b730 100644
--- a/tests/multivalentTests/src/com/android/launcher3/provider/RestoreDbTaskTest.java
+++ b/tests/multivalentTests/src/com/android/launcher3/provider/RestoreDbTaskTest.java
@@ -92,6 +92,7 @@
     private Cursor mMockCursor;
     private LauncherPrefs mPrefs;
     private LauncherRestoreEventLogger mMockRestoreEventLogger;
+    private SQLiteDatabase mDb;
 
     @Before
     public void setup() {
@@ -101,63 +102,66 @@
         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);
     }
 
     @After
     public void teardown() {
+        if (mDb != null) {
+            mDb.close();
+        }
         mModelHelper.destroy();
         LauncherPrefs.get(mContext).removeSync(RESTORE_DEVICE);
     }
 
     @Test
     public void testGetProfileId() throws Exception {
-        SQLiteDatabase db = new MyModelDbController(23).getDb();
-        assertEquals(23, new RestoreDbTask().getDefaultProfileId(db));
+        mDb = new MyModelDbController(23).getDb();
+        assertEquals(23, new RestoreDbTask().getDefaultProfileId(mDb));
     }
 
     @Test
     public void testMigrateProfileId() throws Exception {
-        SQLiteDatabase db = new MyModelDbController(42).getDb();
+        mDb = new MyModelDbController(42).getDb();
         // Add some mock data
         for (int i = 0; i < 5; i++) {
             ContentValues values = new ContentValues();
             values.put(Favorites._ID, i);
             values.put(Favorites.TITLE, "item " + i);
-            db.insert(Favorites.TABLE_NAME, null, values);
+            mDb.insert(Favorites.TABLE_NAME, null, values);
         }
         // Verify item add
-        assertEquals(5, getCount(db, "select * from favorites where profileId = 42"));
+        assertEquals(5, getCount(mDb, "select * from favorites where profileId = 42"));
 
-        new RestoreDbTask().migrateProfileId(db, 42, 33);
+        new RestoreDbTask().migrateProfileId(mDb, 42, 33);
 
         // verify data migrated
-        assertEquals(0, getCount(db, "select * from favorites where profileId = 42"));
-        assertEquals(5, getCount(db, "select * from favorites where profileId = 33"));
+        assertEquals(0, getCount(mDb, "select * from favorites where profileId = 42"));
+        assertEquals(5, getCount(mDb, "select * from favorites where profileId = 33"));
     }
 
     @Test
     public void testChangeDefaultColumn() throws Exception {
-        SQLiteDatabase db = new MyModelDbController(42).getDb();
+        mDb = new MyModelDbController(42).getDb();
         // Add some mock data
         for (int i = 0; i < 5; i++) {
             ContentValues values = new ContentValues();
             values.put(Favorites._ID, i);
             values.put(Favorites.TITLE, "item " + i);
-            db.insert(Favorites.TABLE_NAME, null, values);
+            mDb.insert(Favorites.TABLE_NAME, null, values);
         }
         // Verify default column is 42
-        assertEquals(5, getCount(db, "select * from favorites where profileId = 42"));
+        assertEquals(5, getCount(mDb, "select * from favorites where profileId = 42"));
 
-        new RestoreDbTask().changeDefaultColumn(db, 33);
+        new RestoreDbTask().changeDefaultColumn(mDb, 33);
 
         // Verify default value changed
         ContentValues values = new ContentValues();
         values.put(Favorites._ID, 100);
         values.put(Favorites.TITLE, "item 100");
-        db.insert(Favorites.TABLE_NAME, null, values);
-        assertEquals(1, getCount(db, "select * from favorites where profileId = 33"));
+        mDb.insert(Favorites.TABLE_NAME, null, values);
+        assertEquals(1, getCount(mDb, "select * from favorites where profileId = 33"));
     }
 
     @Test
@@ -170,7 +174,7 @@
         long workProfileId_old = myProfileId + 3;
 
         MyModelDbController controller = new MyModelDbController(myProfileId);
-        SQLiteDatabase db = controller.getDb();
+        mDb = controller.getDb();
         BackupManager bm = spy(new BackupManager(mContext));
         doReturn(myUserHandle()).when(bm).getUserForAncestralSerialNumber(eq(myProfileId_old));
         doReturn(mWorkUser).when(bm).getUserForAncestralSerialNumber(eq(workProfileId_old));
@@ -178,16 +182,16 @@
 
         addIconsBulk(controller, 10, 1, myProfileId_old);
         addIconsBulk(controller, 6, 2, workProfileId_old);
-        assertEquals(10, getItemCountForProfile(db, myProfileId_old));
-        assertEquals(6, getItemCountForProfile(db, workProfileId_old));
+        assertEquals(10, getItemCountForProfile(mDb, myProfileId_old));
+        assertEquals(6, getItemCountForProfile(mDb, workProfileId_old));
 
         mTask.sanitizeDB(mContext, controller, controller.getDb(), bm, mMockRestoreEventLogger);
 
         // All the data has been migrated to the new user ids
-        assertEquals(0, getItemCountForProfile(db, myProfileId_old));
-        assertEquals(0, getItemCountForProfile(db, workProfileId_old));
-        assertEquals(10, getItemCountForProfile(db, myProfileId));
-        assertEquals(6, getItemCountForProfile(db, workProfileId));
+        assertEquals(0, getItemCountForProfile(mDb, myProfileId_old));
+        assertEquals(0, getItemCountForProfile(mDb, workProfileId_old));
+        assertEquals(10, getItemCountForProfile(mDb, myProfileId));
+        assertEquals(6, getItemCountForProfile(mDb, workProfileId));
     }
 
     @Test
@@ -199,7 +203,7 @@
         long workProfileId_old = myProfileId + 3;
 
         MyModelDbController controller = new MyModelDbController(myProfileId);
-        SQLiteDatabase db = controller.getDb();
+        mDb = controller.getDb();
         BackupManager bm = spy(new BackupManager(mContext));
         doReturn(myUserHandle()).when(bm).getUserForAncestralSerialNumber(eq(myProfileId_old));
         // Work profile is not migrated
@@ -207,16 +211,16 @@
 
         addIconsBulk(controller, 10, 1, myProfileId_old);
         addIconsBulk(controller, 6, 2, workProfileId_old);
-        assertEquals(10, getItemCountForProfile(db, myProfileId_old));
-        assertEquals(6, getItemCountForProfile(db, workProfileId_old));
+        assertEquals(10, getItemCountForProfile(mDb, myProfileId_old));
+        assertEquals(6, getItemCountForProfile(mDb, workProfileId_old));
 
         mTask.sanitizeDB(mContext, controller, controller.getDb(), bm, mMockRestoreEventLogger);
 
         // All the data has been migrated to the new user ids
-        assertEquals(0, getItemCountForProfile(db, myProfileId_old));
-        assertEquals(0, getItemCountForProfile(db, workProfileId_old));
-        assertEquals(10, getItemCountForProfile(db, myProfileId));
-        assertEquals(10, getCount(db, "select * from favorites"));
+        assertEquals(0, getItemCountForProfile(mDb, myProfileId_old));
+        assertEquals(0, getItemCountForProfile(mDb, workProfileId_old));
+        assertEquals(10, getItemCountForProfile(mDb, myProfileId));
+        assertEquals(10, getCount(mDb, "select * from favorites"));
     }
 
     @Test
@@ -342,24 +346,24 @@
     }
 
     private void runRemoveScreenIdGapsTest(int[] screenIds, int[] expectedScreenIds) {
-        SQLiteDatabase db = new MyModelDbController(42).getDb();
+        mDb = new MyModelDbController(42).getDb();
         // Add some mock data
         for (int i = 0; i < screenIds.length; i++) {
             ContentValues values = new ContentValues();
             values.put(Favorites._ID, i);
             values.put(Favorites.SCREEN, screenIds[i]);
             values.put(Favorites.CONTAINER, CONTAINER_DESKTOP);
-            db.insert(Favorites.TABLE_NAME, null, values);
+            mDb.insert(Favorites.TABLE_NAME, null, values);
         }
         // Verify items are added
         assertEquals(screenIds.length,
-                getCount(db, "select * from favorites where container = -100"));
+                getCount(mDb, "select * from favorites where container = -100"));
 
-        new RestoreDbTask().removeScreenIdGaps(db);
+        new RestoreDbTask().removeScreenIdGaps(mDb);
 
         // verify screenId gaps removed
         int[] resultScreenIds = new int[screenIds.length];
-        try (Cursor c = db.rawQuery(
+        try (Cursor c = mDb.rawQuery(
                 "select screen from favorites where container = -100 order by screen", null)) {
             int i = 0;
             while (c.moveToNext()) {
diff --git a/tests/multivalentTests/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPoolTest.kt b/tests/multivalentTests/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPoolTest.kt
new file mode 100644
index 0000000..3afb0b5
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPoolTest.kt
@@ -0,0 +1,120 @@
+/*
+ * 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.recyclerview
+
+import android.content.Context
+import android.view.View
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import androidx.recyclerview.widget.RecyclerView.LayoutManager
+import androidx.recyclerview.widget.RecyclerView.ViewHolder
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.util.Executors
+import com.android.launcher3.views.ActivityContext
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class AllAppsRecyclerViewPoolTest<T> where T : Context, T : ActivityContext {
+
+    private lateinit var underTest: AllAppsRecyclerViewPool<T>
+    private lateinit var adapter: RecyclerView.Adapter<*>
+
+    @Mock private lateinit var parent: RecyclerView
+    @Mock private lateinit var itemView: View
+    @Mock private lateinit var layoutManager: LayoutManager
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        underTest = spy(AllAppsRecyclerViewPool())
+        adapter =
+            object : RecyclerView.Adapter<ViewHolder>() {
+                override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
+                    object : ViewHolder(itemView) {}
+
+                override fun getItemCount() = 0
+
+                override fun onBindViewHolder(holder: ViewHolder, position: Int) {}
+            }
+        underTest.setMaxRecycledViews(VIEW_TYPE, 20)
+        `when`(parent.layoutManager).thenReturn(layoutManager)
+    }
+
+    @Test
+    fun preinflate_success() {
+        underTest.preInflateAllAppsViewHolders(adapter, VIEW_TYPE, parent, 10) { 10 }
+
+        awaitTasksCompleted()
+        assertThat(underTest.getRecycledViewCount(VIEW_TYPE)).isEqualTo(10)
+    }
+
+    @Test
+    fun preinflate_not_triggered() {
+        underTest.preInflateAllAppsViewHolders(adapter, VIEW_TYPE, parent, 0) { 0 }
+
+        awaitTasksCompleted()
+        assertThat(underTest.getRecycledViewCount(VIEW_TYPE)).isEqualTo(0)
+    }
+
+    @Test
+    fun preinflate_cancel_before_runOnMainThread() {
+        underTest.preInflateAllAppsViewHolders(adapter, VIEW_TYPE, parent, 10) { 10 }
+        assertThat(underTest.mCancellableTask!!.canceled).isFalse()
+
+        underTest.clear()
+
+        awaitTasksCompleted()
+        verify(underTest, never()).putRecycledView(any(ViewHolder::class.java))
+        assertThat(underTest.mCancellableTask!!.canceled).isTrue()
+        assertThat(underTest.getRecycledViewCount(VIEW_TYPE)).isEqualTo(0)
+    }
+
+    @Test
+    fun preinflate_cancel_after_run() {
+        underTest.preInflateAllAppsViewHolders(adapter, VIEW_TYPE, parent, 10) { 10 }
+        assertThat(underTest.mCancellableTask!!.canceled).isFalse()
+        awaitTasksCompleted()
+
+        underTest.clear()
+
+        verify(underTest, times(10)).putRecycledView(any(ViewHolder::class.java))
+        assertThat(underTest.mCancellableTask!!.canceled).isTrue()
+        assertThat(underTest.getRecycledViewCount(VIEW_TYPE)).isEqualTo(0)
+    }
+
+    private fun awaitTasksCompleted() {
+        Executors.VIEW_PREINFLATION_EXECUTOR.submit<Any> { null }.get()
+        Executors.MAIN_EXECUTOR.submit<Any> { null }.get()
+    }
+
+    companion object {
+        private const val VIEW_TYPE: Int = 4
+    }
+}
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..ac5fda2 100644
--- a/tests/multivalentTests/src/com/android/launcher3/widget/GeneratedPreviewTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/GeneratedPreviewTest.kt
@@ -26,6 +26,7 @@
 import com.android.launcher3.util.ActivityContextWrapper
 import com.android.launcher3.util.Executors
 import com.google.common.truth.Truth.assertThat
+import org.junit.After
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
@@ -38,8 +39,8 @@
     @get:Rule val checkFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()
     private val providerName =
         ComponentName(
-            "com.android.launcher3.tests",
-            "com.android.launcher3.testcomponent.AppWidgetNoConfig"
+            getInstrumentation().context.packageName,
+            "com.android.launcher3.testcomponent.AppWidgetNoConfig",
         )
     private val generatedPreviewLayout =
         getInstrumentation().context.run {
@@ -51,6 +52,7 @@
     private lateinit var helper: WidgetManagerHelper
     private lateinit var appWidgetProviderInfo: LauncherAppWidgetProviderInfo
     private lateinit var widgetItem: WidgetItem
+    private lateinit var iconCache: IconCache
 
     @Before
     fun setup() {
@@ -61,7 +63,7 @@
                     ActivityContextWrapper(
                         ContextThemeWrapper(
                             context,
-                            com.android.launcher3.R.style.WidgetContainerTheme
+                            com.android.launcher3.R.style.WidgetContainerTheme,
                         )
                     )
                 )
@@ -78,7 +80,7 @@
             object : WidgetManagerHelper(context) {
                 override fun loadGeneratedPreview(
                     info: AppWidgetProviderInfo,
-                    widgetCategory: Int
+                    widgetCategory: Int,
                 ) =
                     generatedPreview.takeIf {
                         info === appWidgetProviderInfo &&
@@ -88,17 +90,17 @@
         createWidgetItem()
     }
 
+    @After
+    fun tearDown() {
+        iconCache.close()
+    }
+
     private fun createWidgetItem() {
         Executors.MODEL_EXECUTOR.submit {
                 val idp = InvariantDeviceProfile()
-                widgetItem =
-                    WidgetItem(
-                        appWidgetProviderInfo,
-                        idp,
-                        IconCache(context, idp, null, IconProvider(context)),
-                        context,
-                        helper,
-                    )
+                if (::iconCache.isInitialized) iconCache.close()
+                iconCache = IconCache(context, idp, null, IconProvider(context))
+                widgetItem = WidgetItem(appWidgetProviderInfo, idp, iconCache, context, helper)
             }
             .get()
     }
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/RoundedCornerEnforcementTest.kt b/tests/multivalentTests/src/com/android/launcher3/widget/RoundedCornerEnforcementTest.kt
index db77702..13e23c9 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_USE_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_USE_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_USE_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..8e4db5c 100644
--- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
+++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
@@ -15,77 +15,41 @@
  */
 package com.android.launcher3.ui;
 
-import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT;
-
 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 org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
-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;
-import android.os.Debug;
 import android.os.Process;
-import android.os.RemoteException;
-import android.os.UserHandle;
-import android.os.UserManager;
-import android.platform.test.flag.junit.SetFlagsRule;
-import android.platform.test.rule.LimitDevicesRule;
 import android.system.OsConstants;
 import android.util.Log;
 
-import androidx.annotation.NonNull;
-import androidx.test.InstrumentationRegistry;
 import androidx.test.uiautomator.By;
 import androidx.test.uiautomator.BySelector;
-import androidx.test.uiautomator.UiDevice;
 import androidx.test.uiautomator.Until;
 
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.celllayout.FavoriteItemsTransaction;
-import com.android.launcher3.tapl.HomeAllApps;
-import com.android.launcher3.tapl.HomeAppIcon;
 import com.android.launcher3.tapl.LauncherInstrumentation;
 import com.android.launcher3.tapl.TestHelpers;
 import com.android.launcher3.testcomponent.TestCommandReceiver;
 import com.android.launcher3.util.LooperExecutor;
 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;
-import com.android.launcher3.util.rule.SamplerRule;
-import com.android.launcher3.util.rule.ScreenRecordRule;
 import com.android.launcher3.util.rule.ShellCommandRule;
 import com.android.launcher3.util.rule.TestIsolationRule;
-import com.android.launcher3.util.rule.TestStabilityRule;
 import com.android.launcher3.util.rule.ViewCaptureRule;
 
-import org.junit.After;
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.Rule;
 import org.junit.rules.RuleChain;
 import org.junit.rules.TestRule;
 
-import java.io.IOException;
 import java.util.Objects;
 import java.util.concurrent.Callable;
-import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
 import java.util.function.Consumer;
@@ -95,163 +59,51 @@
 /**
  * Base class for all instrumentation tests providing various utility methods.
  */
-public abstract class AbstractLauncherUiTest<LAUNCHER_TYPE extends Launcher> {
+public abstract class AbstractLauncherUiTest<LAUNCHER_TYPE extends Launcher>
+        extends BaseLauncherTaplTest {
 
-    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 boolean sDumpWasGenerated = false;
-    private static boolean sActivityLeakReported = false;
-    private static boolean sSeenKeyguard = false;
-    private static boolean sFirstTimeWaitingForWizard = true;
-
-    private static final String SYSTEMUI_PACKAGE = "com.android.systemui";
-
     protected LooperExecutor mMainThreadExecutor = MAIN_EXECUTOR;
-    protected final UiDevice mDevice = getUiDevice();
-    protected final LauncherInstrumentation mLauncher = createLauncherInstrumentation();
-
-    @NonNull
-    public static LauncherInstrumentation createLauncherInstrumentation() {
-        waitForSetupWizardDismissal(); // precondition for creating LauncherInstrumentation
-        return new LauncherInstrumentation(true);
-    }
-
-    protected Context mTargetContext;
-    protected String mTargetPackage;
-    private int mLauncherPid;
-
-    /** Detects activity leaks and throws an exception if a leak is found. */
-    public static void checkDetectedLeaks(LauncherInstrumentation launcher) {
-        checkDetectedLeaks(launcher, false);
-    }
-
-    /** Detects activity leaks and throws an exception if a leak is found. */
-    public static void checkDetectedLeaks(LauncherInstrumentation launcher,
-            boolean requireOneActiveActivityUnused) {
-        if (TestStabilityRule.isPresubmit()) return; // b/313501215
-
-        final boolean requireOneActiveActivity =
-                false; // workaround for leaks when there is an unexpected Recents activity
-
-        if (sActivityLeakReported) return;
-
-        // Check whether activity leak detector has found leaked activities.
-        Wait.atMost(() -> getActivityLeakErrorMessage(launcher, requireOneActiveActivity),
-                () -> {
-                    launcher.forceGc();
-                    return MAIN_EXECUTOR.submit(
-                            () -> launcher.noLeakedActivities(requireOneActiveActivity)).get();
-                }, DEFAULT_UI_TIMEOUT, launcher);
-    }
-
-    public static String getAppPackageName() {
-        return getInstrumentation().getContext().getPackageName();
-    }
-
-    private static String getActivityLeakErrorMessage(LauncherInstrumentation launcher,
-            boolean requireOneActiveActivity) {
-        sActivityLeakReported = true;
-        return "Activity leak detector has found leaked activities, requirining 1 activity: "
-                + requireOneActiveActivity + "; "
-                + dumpHprofData(launcher, false, requireOneActiveActivity) + ".";
-    }
-
-    private static String dumpHprofData(LauncherInstrumentation launcher, boolean intentionalLeak,
-            boolean requireOneActiveActivity) {
-        if (intentionalLeak) return "intentional leak; not generating dump";
-
-        String result;
-        if (sDumpWasGenerated) {
-            result = "dump has already been generated by another test";
-        } else {
-            try {
-                final String fileName =
-                        getInstrumentation().getTargetContext().getFilesDir().getPath()
-                                + "/ActivityLeakHeapDump.hprof";
-                if (TestHelpers.isInLauncherProcess()) {
-                    Debug.dumpHprofData(fileName);
-                } else {
-                    final UiDevice device = getUiDevice();
-                    device.executeShellCommand(
-                            "am dumpheap " + device.getLauncherPackageName() + " " + fileName);
-                }
-                Log.d(TAG, "Saved leak dump, the leak is still present: "
-                        + !launcher.noLeakedActivities(requireOneActiveActivity));
-                sDumpWasGenerated = true;
-                result = "saved memory dump as an artifact";
-            } catch (Throwable e) {
-                Log.e(TAG, "dumpHprofData failed", e);
-                result = "failed to save memory dump";
-            }
-        }
-        return result + ". Full list of activities: " + launcher.getRootedActivitiesList();
-    }
 
     protected AbstractLauncherUiTest() {
-        mLauncher.enableCheckEventsForSuccessfulGestures();
-        mLauncher.setAnomalyChecker(AbstractLauncherUiTest::verifyKeyguardInvisible);
-        try {
-            mDevice.setOrientationNatural();
-        } catch (RemoteException e) {
-            throw new RuntimeException(e);
-        }
         if (TestHelpers.isInLauncherProcess()) {
             Utilities.enableRunningInTestHarnessForTests();
             mLauncher.setSystemHealthSupplier(startTime -> TestCommandReceiver.callCommand(
                             TestCommandReceiver.GET_SYSTEM_HEALTH_MESSAGE, startTime.toString())
                     .getString("result"));
         }
-        mLauncher.enableDebugTracing();
-        // Avoid double-reporting of Launcher crashes.
-        mLauncher.setOnLauncherCrashed(() -> mLauncherPid = 0);
     }
 
-    @Rule
-    public ShellCommandRule mDisableHeadsUpNotification =
-            ShellCommandRule.disableHeadsUpNotification();
-
-    @Rule
-    public ScreenRecordRule mScreenRecordRule = new ScreenRecordRule();
-
-    @Rule
-    public SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT);
-
-    @Rule
-    public ExtendedLongPressTimeoutRule mLongPressTimeoutRule = new ExtendedLongPressTimeoutRule();
-
-    @Rule
-    public LimitDevicesRule mlimitDevicesRule = new LimitDevicesRule();
-
+    /**
+     * @deprecated call {@link #performInitialization} instead
+     */
+    @Deprecated
     public static void initialize(AbstractLauncherUiTest test) throws Exception {
-        test.reinitializeLauncherData();
-        test.mDevice.pressHome();
-        test.waitForLauncherCondition("Launcher didn't start", Objects::nonNull);
-        test.waitForState("Launcher internal state didn't switch to Home",
-                () -> LauncherState.NORMAL);
-        test.waitForResumed("Launcher internal state is still Background");
+        test.performInitialization();
+    }
+
+    @Override
+    protected void performInitialization() {
+        reinitializeLauncherData();
+        mDevice.pressHome();
         // Check that we switched to home.
-        test.mLauncher.getWorkspace();
-        AbstractLauncherUiTest.checkDetectedLeaks(test.mLauncher, true);
+        mLauncher.getWorkspace();
+
+        waitForLauncherCondition("Launcher didn't start", Objects::nonNull);
+        waitForState("Launcher internal state didn't switch to Home",
+                () -> LauncherState.NORMAL);
+        waitForResumed("Launcher internal state is still Background");
+
+        checkDetectedLeaks(mLauncher, true);
     }
 
-    protected void clearPackageData(String pkg) throws IOException, InterruptedException {
-        assertTrue("pm clear command failed",
-                mDevice.executeShellCommand("pm clear " + pkg)
-                .contains("Success"));
-        assertTrue("pm wait-for-handler command failed",
-                mDevice.executeShellCommand("pm wait-for-handler")
-                .contains("Success"));
-    }
-
+    @Override
     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))
+                .outerRule(new PortraitLandscapeRunner<>(this))
                 .around(new FailureWatcher(mLauncher, viewCaptureRule::getViewCaptureData))
                 // .around(viewCaptureRule) // b/315482167
                 .around(new TestIsolationRule(mLauncher, true));
@@ -261,160 +113,12 @@
                 : inner;
     }
 
-    @Rule
-    public TestRule mOrderSensitiveRules = RuleChain
-            .outerRule(new SamplerRule())
-            .around(new TestStabilityRule())
-            .around(getRulesInsideActivityMonitor());
-
-    public UiDevice getDevice() {
-        return mDevice;
-    }
-
-    @Before
-    public void setUp() throws Exception {
-        mLauncher.onTestStart();
-
-        final String launcherPackageName = mDevice.getLauncherPackageName();
-        try {
-            final Context context = InstrumentationRegistry.getContext();
-            final PackageManager pm = context.getPackageManager();
-            final PackageInfo launcherPackage = pm.getPackageInfo(launcherPackageName, 0);
-
-            if (!launcherPackage.versionName.equals("BuildFromAndroidStudio")) {
-                Assert.assertEquals("Launcher version doesn't match tests version",
-                        pm.getPackageInfo(context.getPackageName(), 0).getLongVersionCode(),
-                        launcherPackage.getLongVersionCode());
-            }
-        } catch (PackageManager.NameNotFoundException e) {
-            throw new RuntimeException(e);
-        }
-
-        mLauncherPid = 0;
-
-        mTargetContext = InstrumentationRegistry.getTargetContext();
-        mTargetPackage = mTargetContext.getPackageName();
-        mLauncherPid = mLauncher.getPid();
-
-        UserManager userManager = mTargetContext.getSystemService(UserManager.class);
-        if (userManager != null) {
-            for (UserHandle userHandle : userManager.getUserProfiles()) {
-                if (!userHandle.isSystem()) {
-                    mDevice.executeShellCommand(
-                            "pm remove-user --wait " + userHandle.getIdentifier());
-                }
-            }
-        }
-
-        onTestStart();
-
-        initialize(this);
-    }
-
-    /** Method that should be called when a test starts. */
-    public static void onTestStart() {
-        waitForSetupWizardDismissal();
-
-        if (TestStabilityRule.isPresubmit()) {
-            aggressivelyUnlockSysUi();
-        } else {
-            verifyKeyguardInvisible();
-        }
-    }
-
-    private static boolean hasSystemUiObject(String resId) {
-        return getUiDevice().hasObject(
-                By.res(SYSTEMUI_PACKAGE, resId));
-    }
-
-    @NonNull
-    private static UiDevice getUiDevice() {
-        return UiDevice.getInstance(getInstrumentation());
-    }
-
-    private static void aggressivelyUnlockSysUi() {
-        final UiDevice device = getUiDevice();
-        for (int i = 0; i < 10 && hasSystemUiObject("keyguard_status_view"); ++i) {
-            Log.d(TAG, "Before attempting to unlock the phone");
-            try {
-                device.executeShellCommand("input keyevent 82");
-            } catch (IOException e) {
-                throw new RuntimeException(e);
-            }
-            device.waitForIdle();
-        }
-        Assert.assertTrue("Keyguard still visible",
-                TestHelpers.wait(
-                        Until.gone(By.res(SYSTEMUI_PACKAGE, "keyguard_status_view")), 60000));
-        Log.d(TAG, "Keyguard is not visible");
-    }
-
-    /** Waits for setup wizard to go away. */
-    private static void waitForSetupWizardDismissal() {
-        if (sFirstTimeWaitingForWizard) {
-            try {
-                getUiDevice().executeShellCommand(
-                        "am force-stop com.google.android.setupwizard");
-            } catch (IOException e) {
-                throw new RuntimeException(e);
-            }
-        }
-
-        final boolean wizardDismissed = TestHelpers.wait(
-                Until.gone(By.pkg("com.google.android.setupwizard").depth(0)),
-                sFirstTimeWaitingForWizard ? 120000 : 0);
-        sFirstTimeWaitingForWizard = false;
-        Assert.assertTrue("Setup wizard is still visible", wizardDismissed);
-    }
-
-    /** Asserts that keyguard is not visible */
-    public static void verifyKeyguardInvisible() {
-        final boolean keyguardAlreadyVisible = sSeenKeyguard;
-
-        sSeenKeyguard = sSeenKeyguard
-                || !TestHelpers.wait(
-                Until.gone(By.res(SYSTEMUI_PACKAGE, "keyguard_status_view")), 60000);
-
-        Assert.assertFalse(
-                "Keyguard is visible, which is likely caused by a crash in SysUI, seeing keyguard"
-                        + " for the first time = "
-                        + !keyguardAlreadyVisible,
-                sSeenKeyguard);
-    }
-
-    @After
-    public void verifyLauncherState() {
-        try {
-            // Limits UI tests affecting tests running after them.
-            mLauncher.waitForLauncherInitialized();
-            if (mLauncherPid != 0) {
-                assertEquals("Launcher crashed, pid mismatch:",
-                        mLauncherPid, mLauncher.getPid().intValue());
-            }
-        } finally {
-            mLauncher.onTestFinish();
-        }
-    }
-
-    protected void reinitializeLauncherData() {
-        reinitializeLauncherData(false);
-    }
-
-    protected void reinitializeLauncherData(boolean clearWorkspace) {
-        if (clearWorkspace) {
-            mLauncher.clearLauncherData();
-        } else {
-            mLauncher.reinitializeLauncherData();
-        }
-        mLauncher.waitForLauncherInitialized();
-    }
-
     /**
      * Runs the callback on the UI thread and returns the result.
      */
     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 +131,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 +173,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 +182,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 +195,7 @@
             final Object fromLauncher = getFromLauncher(f);
             output[0] = fromLauncher;
             return fromLauncher != null;
-        }, timeout, mLauncher);
+        }, mLauncher);
         return (T) output[0];
     }
 
@@ -511,54 +209,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);
-    }
-
-    /**
-     * Broadcast receiver which blocks until the result is received.
-     */
-    public class BlockingBroadcastReceiver extends BroadcastReceiver {
-
-        private final CountDownLatch latch = new CountDownLatch(1);
-        private Intent mIntent;
-
-        public BlockingBroadcastReceiver(String action) {
-            mTargetContext.registerReceiver(this, new IntentFilter(action),
-                    Context.RECEIVER_EXPORTED/*UNAUDITED*/);
-        }
-
-        @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;
-        }
-
-        public Intent blockingGetExtraIntent() throws InterruptedException {
-            Intent intent = blockingGetIntent();
-            return intent == null ? null : (Intent) intent.getParcelableExtra(
-                    Intent.EXTRA_INTENT);
-        }
+        }, mLauncher, timeout);
     }
 
     public static void startAppFast(String packageName) {
@@ -614,20 +265,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 +287,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) {
@@ -663,45 +306,4 @@
 
     protected void onLauncherActivityClose(LAUNCHER_TYPE launcher) {
     }
-
-    protected HomeAppIcon createShortcutInCenterIfNotExist(String name) {
-        Point dimension = mLauncher.getWorkspace().getIconGridDimensions();
-        return createShortcutIfNotExist(name, dimension.x / 2, dimension.y / 2);
-    }
-
-    protected HomeAppIcon createShortcutIfNotExist(String name, Point cellPosition) {
-        return createShortcutIfNotExist(name, cellPosition.x, cellPosition.y);
-    }
-
-    protected HomeAppIcon createShortcutIfNotExist(String name, int cellX, int cellY) {
-        HomeAppIcon homeAppIcon = mLauncher.getWorkspace().tryGetWorkspaceAppIcon(name);
-        Log.d(ICON_MISSING, "homeAppIcon: " + homeAppIcon + " name: " + name +
-                " cell: " + cellX + ", " + cellY);
-        if (homeAppIcon == null) {
-            HomeAllApps allApps = mLauncher.getWorkspace().switchToAllApps();
-            allApps.freeze();
-            try {
-                allApps.getAppIcon(name).dragToWorkspace(cellX, cellY);
-            } finally {
-                allApps.unfreeze();
-            }
-            homeAppIcon = mLauncher.getWorkspace().getWorkspaceAppIcon(name);
-        }
-        return homeAppIcon;
-    }
-
-    protected void commitTransactionAndLoadHome(FavoriteItemsTransaction transaction) {
-        transaction.commit();
-
-        // Launch the home activity
-        UiDevice.getInstance(getInstrumentation()).pressHome();
-        mLauncher.waitForLauncherInitialized();
-    }
-
-    /** Clears all recent tasks */
-    protected void clearAllRecentTasks() {
-        if (!mLauncher.getRecentTasks().isEmpty()) {
-            mLauncher.goHome().switchToOverview().dismissAllTasks();
-        }
-    }
 }
diff --git a/tests/src/com/android/launcher3/ui/BaseLauncherTaplTest.java b/tests/src/com/android/launcher3/ui/BaseLauncherTaplTest.java
new file mode 100644
index 0000000..8449853
--- /dev/null
+++ b/tests/src/com/android/launcher3/ui/BaseLauncherTaplTest.java
@@ -0,0 +1,529 @@
+/*
+ * 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.ui;
+
+import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static com.android.launcher3.testing.shared.TestProtocol.ICON_MISSING;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.app.ActivityManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.graphics.Point;
+import android.os.Debug;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.platform.test.flag.junit.SetFlagsRule;
+import android.platform.test.rule.LimitDevicesRule;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.test.InstrumentationRegistry;
+import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.BySelector;
+import androidx.test.uiautomator.UiDevice;
+import androidx.test.uiautomator.Until;
+
+import com.android.launcher3.celllayout.FavoriteItemsTransaction;
+import com.android.launcher3.tapl.HomeAllApps;
+import com.android.launcher3.tapl.HomeAppIcon;
+import com.android.launcher3.tapl.LauncherInstrumentation;
+import com.android.launcher3.tapl.TestHelpers;
+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;
+import com.android.launcher3.util.rule.SamplerRule;
+import com.android.launcher3.util.rule.ScreenRecordRule;
+import com.android.launcher3.util.rule.ShellCommandRule;
+import com.android.launcher3.util.rule.TestIsolationRule;
+import com.android.launcher3.util.rule.TestStabilityRule;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.rules.RuleChain;
+import org.junit.rules.TestRule;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Base class for all TAPL tests in Launcher providing various utility methods.
+ */
+public abstract class BaseLauncherTaplTest {
+
+    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 = "BaseLauncherTaplTest";
+
+    private static final long BYTES_PER_MEGABYTE = 1 << 20;
+
+    private static boolean sDumpWasGenerated = false;
+    private static boolean sActivityLeakReported = false;
+    private static boolean sSeenKeyguard = false;
+    private static boolean sFirstTimeWaitingForWizard = true;
+
+    private static final String SYSTEMUI_PACKAGE = "com.android.systemui";
+
+    protected final UiDevice mDevice = getUiDevice();
+    protected final LauncherInstrumentation mLauncher = createLauncherInstrumentation();
+
+    @NonNull
+    public static LauncherInstrumentation createLauncherInstrumentation() {
+        waitForSetupWizardDismissal(); // precondition for creating LauncherInstrumentation
+        return new LauncherInstrumentation(true);
+    }
+
+    protected Context mTargetContext;
+    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);
+    }
+
+    /** Detects activity leaks and throws an exception if a leak is found. */
+    public static void checkDetectedLeaks(LauncherInstrumentation launcher,
+            boolean requireOneActiveActivityUnused) {
+        if (TestStabilityRule.isPresubmit()) return; // b/313501215
+
+        final boolean requireOneActiveActivity =
+                false; // workaround for leaks when there is an unexpected Recents activity
+
+        if (sActivityLeakReported) return;
+
+        // Check whether activity leak detector has found leaked activities.
+        Wait.atMost(() -> getActivityLeakErrorMessage(launcher, requireOneActiveActivity),
+                () -> {
+                    launcher.forceGc();
+                    return MAIN_EXECUTOR.submit(
+                            () -> launcher.noLeakedActivities(requireOneActiveActivity)).get();
+                }, launcher, DEFAULT_UI_TIMEOUT);
+    }
+
+    public static String getAppPackageName() {
+        return getInstrumentation().getContext().getPackageName();
+    }
+
+    private static String getActivityLeakErrorMessage(LauncherInstrumentation launcher,
+            boolean requireOneActiveActivity) {
+        sActivityLeakReported = true;
+        return "Activity leak detector has found leaked activities, requirining 1 activity: "
+                + requireOneActiveActivity + "; "
+                + dumpHprofData(launcher, false, requireOneActiveActivity) + ".";
+    }
+
+    private static String dumpHprofData(LauncherInstrumentation launcher, boolean intentionalLeak,
+            boolean requireOneActiveActivity) {
+        if (intentionalLeak) return "intentional leak; not generating dump";
+
+        String result;
+        if (sDumpWasGenerated) {
+            result = "dump has already been generated by another test";
+        } else {
+            try {
+                final String fileName =
+                        getInstrumentation().getTargetContext().getFilesDir().getPath()
+                                + "/ActivityLeakHeapDump.hprof";
+                if (TestHelpers.isInLauncherProcess()) {
+                    Debug.dumpHprofData(fileName);
+                } else {
+                    final UiDevice device = getUiDevice();
+                    device.executeShellCommand(
+                            "am dumpheap " + device.getLauncherPackageName() + " " + fileName);
+                }
+                Log.d(TAG, "Saved leak dump, the leak is still present: "
+                        + !launcher.noLeakedActivities(requireOneActiveActivity));
+                sDumpWasGenerated = true;
+                result = "saved memory dump as an artifact";
+            } catch (Throwable e) {
+                Log.e(TAG, "dumpHprofData failed", e);
+                result = "failed to save memory dump";
+            }
+        }
+        return result + ". Full list of activities: " + launcher.getRootedActivitiesList();
+    }
+
+    protected BaseLauncherTaplTest() {
+        mActivityManager = InstrumentationRegistry.getContext()
+                .getSystemService(ActivityManager.class);
+        mLauncher.enableCheckEventsForSuccessfulGestures();
+        mLauncher.setAnomalyChecker(BaseLauncherTaplTest::verifyKeyguardInvisible);
+        try {
+            mDevice.setOrientationNatural();
+        } catch (RemoteException e) {
+            throw new RuntimeException(e);
+        }
+        mLauncher.enableDebugTracing();
+        // Avoid double-reporting of Launcher crashes.
+        mLauncher.setOnLauncherCrashed(() -> mLauncherPid = 0);
+    }
+
+    @Rule
+    public ShellCommandRule mDisableHeadsUpNotification =
+            ShellCommandRule.disableHeadsUpNotification();
+
+    @Rule
+    public ScreenRecordRule mScreenRecordRule = new ScreenRecordRule();
+
+    @Rule
+    public SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT);
+
+    @Rule
+    public ExtendedLongPressTimeoutRule mLongPressTimeoutRule = new ExtendedLongPressTimeoutRule();
+
+    @Rule
+    public LimitDevicesRule mlimitDevicesRule = new LimitDevicesRule();
+
+    protected void performInitialization() {
+        reinitializeLauncherData();
+        mDevice.pressHome();
+        // Check that we switched to home.
+        mLauncher.getWorkspace();
+        checkDetectedLeaks(mLauncher, true);
+    }
+
+    protected void clearPackageData(String pkg) throws IOException, InterruptedException {
+        assertTrue("pm clear command failed",
+                mDevice.executeShellCommand("pm clear " + pkg)
+                        .contains("Success"));
+        assertTrue("pm wait-for-handler command failed",
+                mDevice.executeShellCommand("pm wait-for-handler")
+                        .contains("Success"));
+    }
+
+    protected TestRule getRulesInsideActivityMonitor() {
+        final RuleChain inner = RuleChain
+                .outerRule(new FailureWatcher(mLauncher, null))
+                .around(new TestIsolationRule(mLauncher, true));
+        return TestHelpers.isInLauncherProcess()
+                ? RuleChain.outerRule(ShellCommandRule.setDefaultLauncher()).around(inner)
+                : inner;
+    }
+
+    @Rule
+    public TestRule mOrderSensitiveRules = RuleChain
+            .outerRule(new SamplerRule())
+            .around(new TestStabilityRule())
+            .around(getRulesInsideActivityMonitor());
+
+    public UiDevice getDevice() {
+        return mDevice;
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        mLauncher.onTestStart();
+
+        final String launcherPackageName = mDevice.getLauncherPackageName();
+        try {
+            final Context context = InstrumentationRegistry.getContext();
+            final PackageManager pm = context.getPackageManager();
+            final PackageInfo launcherPackage = pm.getPackageInfo(launcherPackageName, 0);
+
+            if (!launcherPackage.versionName.equals("BuildFromAndroidStudio")) {
+                Assert.assertEquals("Launcher version doesn't match tests version",
+                        pm.getPackageInfo(context.getPackageName(), 0).getLongVersionCode(),
+                        launcherPackage.getLongVersionCode());
+            }
+        } catch (PackageManager.NameNotFoundException e) {
+            throw new RuntimeException(e);
+        }
+
+        mLauncherPid = 0;
+
+        mTargetContext = InstrumentationRegistry.getTargetContext();
+        mTargetPackage = mTargetContext.getPackageName();
+        mLauncherPid = mLauncher.getPid();
+
+        UserManager userManager = mTargetContext.getSystemService(UserManager.class);
+        if (userManager != null) {
+            for (UserHandle userHandle : userManager.getUserProfiles()) {
+                if (!userHandle.isSystem()) {
+                    mDevice.executeShellCommand(
+                            "pm remove-user --wait " + userHandle.getIdentifier());
+                }
+            }
+        }
+
+        onTestStart();
+        performInitialization();
+    }
+
+    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();
+
+        if (TestStabilityRule.isPresubmit()) {
+            aggressivelyUnlockSysUi();
+        } else {
+            verifyKeyguardInvisible();
+        }
+    }
+
+    private static boolean hasSystemUiObject(String resId) {
+        return getUiDevice().hasObject(
+                By.res(SYSTEMUI_PACKAGE, resId));
+    }
+
+    @NonNull
+    private static UiDevice getUiDevice() {
+        return UiDevice.getInstance(getInstrumentation());
+    }
+
+    private static void aggressivelyUnlockSysUi() {
+        final UiDevice device = getUiDevice();
+        for (int i = 0; i < 10 && hasSystemUiObject("keyguard_status_view"); ++i) {
+            Log.d(TAG, "Before attempting to unlock the phone");
+            try {
+                device.executeShellCommand("input keyevent 82");
+            } catch (IOException e) {
+                throw new RuntimeException(e);
+            }
+            device.waitForIdle();
+        }
+        Assert.assertTrue("Keyguard still visible",
+                TestHelpers.wait(
+                        Until.gone(By.res(SYSTEMUI_PACKAGE, "keyguard_status_view")), 60000));
+        Log.d(TAG, "Keyguard is not visible");
+    }
+
+    /** Waits for setup wizard to go away. */
+    private static void waitForSetupWizardDismissal() {
+        if (sFirstTimeWaitingForWizard) {
+            try {
+                getUiDevice().executeShellCommand(
+                        "am force-stop com.google.android.setupwizard");
+            } catch (IOException e) {
+                throw new RuntimeException(e);
+            }
+        }
+
+        final boolean wizardDismissed = TestHelpers.wait(
+                Until.gone(By.pkg("com.google.android.setupwizard").depth(0)),
+                sFirstTimeWaitingForWizard ? 120000 : 0);
+        sFirstTimeWaitingForWizard = false;
+        Assert.assertTrue("Setup wizard is still visible", wizardDismissed);
+    }
+
+    /** Asserts that keyguard is not visible */
+    public static void verifyKeyguardInvisible() {
+        final boolean keyguardAlreadyVisible = sSeenKeyguard;
+
+        sSeenKeyguard = sSeenKeyguard
+                || !TestHelpers.wait(
+                Until.gone(By.res(SYSTEMUI_PACKAGE, "keyguard_status_view")), 60000);
+
+        Assert.assertFalse(
+                "Keyguard is visible, which is likely caused by a crash in SysUI, seeing keyguard"
+                        + " for the first time = "
+                        + !keyguardAlreadyVisible,
+                sSeenKeyguard);
+    }
+
+    @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:",
+                        mLauncherPid, mLauncher.getPid().intValue());
+            }
+        } finally {
+            mLauncher.onTestFinish();
+        }
+    }
+
+    protected void reinitializeLauncherData() {
+        reinitializeLauncherData(false);
+    }
+
+    protected void reinitializeLauncherData(boolean clearWorkspace) {
+        if (clearWorkspace) {
+            mLauncher.clearLauncherData();
+        } else {
+            mLauncher.reinitializeLauncherData();
+        }
+        mLauncher.waitForLauncherInitialized();
+    }
+
+    public static void startAppFast(String packageName) {
+        startIntent(
+                getInstrumentation().getContext().getPackageManager().getLaunchIntentForPackage(
+                        packageName),
+                By.pkg(packageName).depth(0),
+                true /* newTask */);
+    }
+
+    public static void startTestActivity(String activityName, String activityLabel) {
+        final String packageName = getAppPackageName();
+        final Intent intent = getInstrumentation().getContext().getPackageManager()
+                        .getLaunchIntentForPackage(packageName);
+        intent.setComponent(new ComponentName(packageName,
+                "com.android.launcher3.tests." + activityName));
+        startIntent(intent, By.pkg(packageName).text(activityLabel),
+                false /* newTask */);
+    }
+
+    public static void startTestActivity(int activityNumber) {
+        startTestActivity("Activity" + activityNumber, "TestActivity" + activityNumber);
+    }
+
+    public static void startImeTestActivity() {
+        final String packageName = getAppPackageName();
+        final Intent intent = getInstrumentation().getContext().getPackageManager()
+                        .getLaunchIntentForPackage(packageName);
+        intent.setComponent(new ComponentName(packageName,
+                "com.android.launcher3.testcomponent.ImeTestActivity"));
+        startIntent(intent, By.pkg(packageName).text("ImeTestActivity"),
+                false /* newTask */);
+    }
+
+    /** Starts ExcludeFromRecentsTestActivity, which has excludeFromRecents="true". */
+    public static void startExcludeFromRecentsTestActivity() {
+        final String packageName = getAppPackageName();
+        final Intent intent = getInstrumentation().getContext().getPackageManager()
+                .getLaunchIntentForPackage(packageName);
+        intent.setComponent(new ComponentName(packageName,
+                "com.android.launcher3.testcomponent.ExcludeFromRecentsTestActivity"));
+        startIntent(intent, By.pkg(packageName).text("ExcludeFromRecentsTestActivity"),
+                false /* newTask */);
+    }
+
+    private static void startIntent(Intent intent, BySelector selector, boolean newTask) {
+        intent.addCategory(Intent.CATEGORY_LAUNCHER);
+        if (newTask) {
+            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+        } else {
+            intent.addFlags(
+                    Intent.FLAG_ACTIVITY_MULTIPLE_TASK | Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
+        }
+        getInstrumentation().getTargetContext().startActivity(intent);
+        assertTrue("App didn't start: " + selector,
+                TestHelpers.wait(Until.hasObject(selector), DEFAULT_UI_TIMEOUT));
+
+        // Wait for the Launcher to stop.
+        final LauncherInstrumentation launcherInstrumentation = new LauncherInstrumentation();
+        Wait.atMost("Launcher activity didn't stop",
+                () -> !launcherInstrumentation.isLauncherActivityStarted(),
+                launcherInstrumentation, DEFAULT_ACTIVITY_TIMEOUT);
+    }
+
+    public static ActivityInfo resolveSystemAppInfo(String category) {
+        return getInstrumentation().getContext().getPackageManager().resolveActivity(
+                new Intent(Intent.ACTION_MAIN).addCategory(category),
+                PackageManager.MATCH_SYSTEM_ONLY)
+                .activityInfo;
+    }
+
+
+    public static String resolveSystemApp(String category) {
+        return resolveSystemAppInfo(category).packageName;
+    }
+
+    protected HomeAppIcon createShortcutInCenterIfNotExist(String name) {
+        Point dimension = mLauncher.getWorkspace().getIconGridDimensions();
+        return createShortcutIfNotExist(name, dimension.x / 2, dimension.y / 2);
+    }
+
+    protected HomeAppIcon createShortcutIfNotExist(String name, Point cellPosition) {
+        return createShortcutIfNotExist(name, cellPosition.x, cellPosition.y);
+    }
+
+    protected HomeAppIcon createShortcutIfNotExist(String name, int cellX, int cellY) {
+        HomeAppIcon homeAppIcon = mLauncher.getWorkspace().tryGetWorkspaceAppIcon(name);
+        Log.d(ICON_MISSING, "homeAppIcon: " + homeAppIcon + " name: " + name
+                + " cell: " + cellX + ", " + cellY);
+        if (homeAppIcon == null) {
+            HomeAllApps allApps = mLauncher.getWorkspace().switchToAllApps();
+            allApps.freeze();
+            try {
+                allApps.getAppIcon(name).dragToWorkspace(cellX, cellY);
+            } finally {
+                allApps.unfreeze();
+            }
+            homeAppIcon = mLauncher.getWorkspace().getWorkspaceAppIcon(name);
+        }
+        return homeAppIcon;
+    }
+
+    protected void commitTransactionAndLoadHome(FavoriteItemsTransaction transaction) {
+        transaction.commit();
+
+        // Launch the home activity
+        UiDevice.getInstance(getInstrumentation()).pressHome();
+        mLauncher.waitForLauncherInitialized();
+    }
+
+    /** Clears all recent tasks */
+    protected void clearAllRecentTasks() {
+        if (!mLauncher.getRecentTasks().isEmpty()) {
+            mLauncher.goHome().switchToOverview().dismissAllTasks();
+        }
+    }
+}
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/AddConfigWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
new file mode 100644
index 0000000..bb645d7
--- /dev/null
+++ b/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.ui.widget;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static com.android.launcher3.util.LauncherBindableItemsContainer.ItemOperator;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+
+import android.appwidget.AppWidgetManager;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Process;
+import android.view.View;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.LargeTest;
+
+import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.celllayout.FavoriteItemsTransaction;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.LauncherAppWidgetInfo;
+import com.android.launcher3.testcomponent.WidgetConfigActivity;
+import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
+import com.android.launcher3.ui.TestViewHelpers;
+import com.android.launcher3.util.BaseLauncherActivityTest;
+import com.android.launcher3.util.BlockingBroadcastReceiver;
+import com.android.launcher3.util.PackageUserKey;
+import com.android.launcher3.util.Wait;
+import com.android.launcher3.util.rule.ShellCommandRule;
+import com.android.launcher3.views.OptionsPopupView;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
+import com.android.launcher3.widget.PendingAddWidgetInfo;
+import com.android.launcher3.widget.WidgetCell;
+import com.android.launcher3.widget.picker.WidgetsFullSheet;
+import com.android.launcher3.widget.picker.WidgetsListAdapter;
+import com.android.launcher3.widget.picker.WidgetsRecyclerView;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Test to verify widget configuration is properly shown.
+ */
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+public class AddConfigWidgetTest extends BaseLauncherActivityTest<Launcher> {
+
+    @Rule
+    public ShellCommandRule mGrantWidgetRule = ShellCommandRule.grantWidgetBind();
+
+    private LauncherAppWidgetProviderInfo mWidgetInfo;
+    private AppWidgetManager mAppWidgetManager;
+
+    private int mWidgetId;
+
+    @Before
+    public void setUp() throws Exception {
+        mWidgetInfo = TestViewHelpers.findWidgetProvider(true /* hasConfigureScreen */);
+        mAppWidgetManager = AppWidgetManager.getInstance(targetContext());
+    }
+
+    @Test
+    @PortraitLandscape
+    public void testWidgetConfig() throws Throwable {
+        runTest(true);
+    }
+
+    @Test
+    @PortraitLandscape
+    public void testConfigCancelled() throws Throwable {
+        runTest(false);
+    }
+
+    /**
+     * @param acceptConfig accept the config activity
+     */
+    private void runTest(boolean acceptConfig) throws Throwable {
+        new FavoriteItemsTransaction(targetContext()).commit();
+        loadLauncherSync();
+
+        // Add widget to homescreen
+        WidgetConfigStartupMonitor monitor = new WidgetConfigStartupMonitor();
+        executeOnLauncher(OptionsPopupView::openWidgets);
+        uiDevice.waitForIdle();
+
+        // Select the widget header
+        Context testContext = getInstrumentation().getContext();
+        String packageName = testContext.getPackageName();
+        executeOnLauncher(l -> {
+            WidgetsRecyclerView wrv = WidgetsFullSheet.getWidgetsView(l);
+            WidgetsListAdapter adapter = (WidgetsListAdapter) wrv.getAdapter();
+            int pos = adapter.getItems().indexOf(
+                    adapter.getItems().stream()
+                            .filter(entry -> packageName.equals(entry.mPkgItem.packageName))
+                            .findFirst()
+                            .get());
+            wrv.getLayoutManager().scrollToPosition(pos);
+            adapter.onHeaderClicked(true, new PackageUserKey(packageName, Process.myUserHandle()));
+        });
+        uiDevice.waitForIdle();
+
+        View widgetView = getOnceNotNull("Widget not found", l -> searchView(l.getDragLayer(), v ->
+                v instanceof WidgetCell
+                        && v.getTag() instanceof PendingAddWidgetInfo pawi
+                        && mWidgetInfo.provider.equals(pawi.componentName)));
+        addToWorkspace(widgetView);
+
+        // Widget id for which the config activity was opened
+        mWidgetId = monitor.getWidgetId();
+
+        // Verify that the widget id is valid and bound
+        assertNotNull(mAppWidgetManager.getAppWidgetInfo(mWidgetId));
+        setResult(acceptConfig);
+
+        if (acceptConfig) {
+            getOnceNotNull("Widget was not added", l -> {
+                // Close the resize frame before searching for widget
+                AbstractFloatingView.closeAllOpenViews(l);
+                return l.getWorkspace().getFirstMatch(new WidgetSearchCondition());
+            });
+            assertNotNull(mAppWidgetManager.getAppWidgetInfo(mWidgetId));
+        } else {
+            // Verify that the widget id is deleted.
+            Wait.atMost("", () -> mAppWidgetManager.getAppWidgetInfo(mWidgetId) == null);
+        }
+    }
+
+    private void setResult(boolean success) {
+        getInstrumentation().getTargetContext().sendBroadcast(
+                WidgetConfigActivity.getCommandIntent(WidgetConfigActivity.class,
+                        success ? "clickOK" : "clickCancel"));
+        uiDevice.waitForIdle();
+    }
+
+    /**
+     * Condition for searching widget id
+     */
+    private class WidgetSearchCondition implements ItemOperator {
+
+        @Override
+        public boolean evaluate(ItemInfo info, View view) {
+            return info instanceof LauncherAppWidgetInfo lawi
+                    && lawi.providerName.equals(mWidgetInfo.provider)
+                    && lawi.appWidgetId == mWidgetId;
+        }
+    }
+
+    /**
+     * Broadcast receiver for receiving widget config activity status.
+     */
+    private static class WidgetConfigStartupMonitor extends BlockingBroadcastReceiver {
+
+        WidgetConfigStartupMonitor() {
+            super(WidgetConfigActivity.class.getName());
+        }
+
+        public int getWidgetId() throws InterruptedException {
+            Intent intent = blockingGetExtraIntent();
+            assertNotNull("Null EXTRA_INTENT", intent);
+            assertEquals("Intent action is not ACTION_APPWIDGET_CONFIGURE",
+                    AppWidgetManager.ACTION_APPWIDGET_CONFIGURE, intent.getAction());
+            int widgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
+                    LauncherAppWidgetInfo.NO_ID);
+            assertNotSame("Widget id is NO_ID", widgetId, LauncherAppWidgetInfo.NO_ID);
+            return widgetId;
+        }
+    }
+}
diff --git a/tests/src/com/android/launcher3/ui/widget/TaplBindWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java
similarity index 77%
rename from tests/src/com/android/launcher3/ui/widget/TaplBindWidgetTest.java
rename to tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java
index d40d3bc..8846d65 100644
--- a/tests/src/com/android/launcher3/ui/widget/TaplBindWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java
@@ -1,17 +1,17 @@
 /*
  * Copyright (C) 2017 The Android Open Source Project
  *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
+ * 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.ui.widget;
 
@@ -22,12 +22,12 @@
 import static com.android.launcher3.model.data.LauncherAppWidgetInfo.FLAG_RESTORE_STARTED;
 import static com.android.launcher3.provider.LauncherDbUtils.itemIdMatch;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+import static com.android.launcher3.util.TestUtil.getOnUiThread;
+import static com.android.launcher3.util.Wait.atMost;
 import static com.android.launcher3.util.WidgetUtils.createWidgetInfo;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
 
 import android.appwidget.AppWidgetManager;
 import android.content.ComponentName;
@@ -36,6 +36,7 @@
 import android.content.pm.PackageManager;
 import android.database.Cursor;
 import android.os.Bundle;
+import android.text.TextUtils;
 import android.widget.RemoteViews;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -49,12 +50,12 @@
 import com.android.launcher3.celllayout.FavoriteItemsTransaction;
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
 import com.android.launcher3.pm.InstallSessionHelper;
-import com.android.launcher3.tapl.Widget;
-import com.android.launcher3.tapl.Workspace;
-import com.android.launcher3.ui.AbstractLauncherUiTest;
 import com.android.launcher3.ui.TestViewHelpers;
+import com.android.launcher3.util.BaseLauncherActivityTest;
 import com.android.launcher3.util.rule.ShellCommandRule;
+import com.android.launcher3.widget.LauncherAppWidgetHostView;
 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
+import com.android.launcher3.widget.PendingAppWidgetHostView;
 import com.android.launcher3.widget.WidgetManagerHelper;
 
 import org.junit.After;
@@ -66,6 +67,7 @@
 import java.util.HashSet;
 import java.util.Set;
 import java.util.function.Consumer;
+import java.util.function.Function;
 
 /**
  * Tests for bind widget flow.
@@ -74,7 +76,7 @@
  */
 @LargeTest
 @RunWith(AndroidJUnit4.class)
-public class TaplBindWidgetTest extends AbstractLauncherUiTest<Launcher> {
+public class BindWidgetTest extends BaseLauncherActivityTest<Launcher> {
 
     @Rule
     public ShellCommandRule mGrantWidgetRule = ShellCommandRule.grantWidgetBind();
@@ -86,11 +88,9 @@
 
     private LauncherModel mModel;
 
-    @Override
     @Before
     public void setUp() throws Exception {
-        super.setUp();
-        mModel = LauncherAppState.getInstance(mTargetContext).getModel();
+        mModel = LauncherAppState.getInstance(targetContext()).getModel();
     }
 
     @After
@@ -100,7 +100,7 @@
         }
 
         if (mSessionId > -1) {
-            mTargetContext.getPackageManager().getPackageInstaller().abandonSession(mSessionId);
+            targetContext().getPackageManager().getPackageInstaller().abandonSession(mSessionId);
         }
     }
 
@@ -121,13 +121,12 @@
         LauncherAppWidgetProviderInfo info = addWidgetToScreen(false, false,
                 item -> item.appWidgetId = -33);
 
-        final Workspace workspace = mLauncher.getWorkspace();
         // Item deleted from db
         mCursor = queryItem();
         assertEquals(0, mCursor.getCount());
 
         // The view does not exist
-        assertTrue("Widget exists", workspace.tryGetWidget(info.label, 0) == null);
+        verifyItemEventuallyNull("Widget exists", widgetProvider(info));
     }
 
     @Test
@@ -153,18 +152,19 @@
         // Widget has a valid Id now.
         assertEquals(0, mCursor.getInt(mCursor.getColumnIndex(LauncherSettings.Favorites.RESTORED))
                 & FLAG_ID_NOT_VALID);
-        assertNotNull(AppWidgetManager.getInstance(mTargetContext)
+        assertNotNull(AppWidgetManager.getInstance(targetContext())
                 .getAppWidgetInfo(mCursor.getInt(mCursor.getColumnIndex(
                         LauncherSettings.Favorites.APPWIDGET_ID))));
 
         // send OPTION_APPWIDGET_RESTORE_COMPLETED
         int appWidgetId = mCursor.getInt(
                 mCursor.getColumnIndex(LauncherSettings.Favorites.APPWIDGET_ID));
-        AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(mTargetContext);
+        AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(targetContext());
 
         Bundle b = new Bundle();
         b.putBoolean(WidgetManagerHelper.WIDGET_OPTION_RESTORE_COMPLETED, true);
-        RemoteViews remoteViews = new RemoteViews(mTargetPackage, R.layout.appwidget_not_ready);
+        RemoteViews remoteViews = new RemoteViews(
+                targetContext().getPackageName(), R.layout.appwidget_not_ready);
         appWidgetManager.updateAppWidgetOptions(appWidgetId, b);
         appWidgetManager.updateAppWidget(appWidgetId, remoteViews);
 
@@ -174,15 +174,14 @@
                         WidgetManagerHelper.WIDGET_OPTION_RESTORE_COMPLETED));
         executeOnLauncher(l -> l.getAppWidgetHolder().startListening());
         verifyWidgetPresent(info);
-        assertNull(mLauncher.getWorkspace().tryGetPendingWidget(100));
+        verifyItemEventuallyNull("Pending widget exists", pendingWidgetProvider());
     }
 
     @Test
     public void testPendingWidget_notRestored_removed() {
         addPendingItemToScreen(getInvalidWidgetInfo(), FLAG_ID_NOT_VALID | FLAG_PROVIDER_NOT_READY);
 
-        assertTrue("Pending widget exists",
-                mLauncher.getWorkspace().tryGetPendingWidget(0) == null);
+        verifyItemEventuallyNull("Pending widget exists", pendingWidgetProvider());
         // Item deleted from db
         mCursor = queryItem();
         assertEquals(0, mCursor.getCount());
@@ -215,7 +214,7 @@
         // Create an active installer session
         SessionParams params = new SessionParams(SessionParams.MODE_FULL_INSTALL);
         params.setAppPackageName(item.providerName.getPackageName());
-        PackageInstaller installer = mTargetContext.getPackageManager().getPackageInstaller();
+        PackageInstaller installer = targetContext().getPackageManager().getPackageInstaller();
         mSessionId = installer.createSession(params);
 
         addPendingItemToScreen(item, FLAG_ID_NOT_VALID | FLAG_PROVIDER_NOT_READY);
@@ -233,34 +232,47 @@
     }
 
     private void verifyWidgetPresent(LauncherAppWidgetProviderInfo info) {
-        final Widget widget = mLauncher.getWorkspace().tryGetWidget(info.label, DEFAULT_UI_TIMEOUT);
-        assertTrue("Widget is not present",
-                widget != null);
+        getOnceNotNull("Widget is not present", widgetProvider(info));
     }
 
     private void verifyPendingWidgetPresent() {
-        final Widget widget = mLauncher.getWorkspace().tryGetPendingWidget(DEFAULT_UI_TIMEOUT);
-        assertTrue("Pending widget is not present",
-                widget != null);
+        getOnceNotNull("Widget is not present", pendingWidgetProvider());
+    }
+
+    private Function<Launcher, Object> pendingWidgetProvider() {
+        return l -> l.getWorkspace().getFirstMatch(
+                (item, view) -> view instanceof PendingAppWidgetHostView);
+    }
+
+    private Function<Launcher, Object> widgetProvider(LauncherAppWidgetProviderInfo info) {
+        return l -> l.getWorkspace().getFirstMatch((item, view) ->
+                view instanceof LauncherAppWidgetHostView
+                        && TextUtils.equals(info.label, view.getContentDescription()));
+    }
+
+    private void verifyItemEventuallyNull(String message, Function<Launcher, Object> provider) {
+        atMost(message, () -> getFromLauncher(provider) == null);
     }
 
     private void addPendingItemToScreen(LauncherAppWidgetInfo item, int restoreStatus) {
         item.restoreStatus = restoreStatus;
         item.screenId = FIRST_SCREEN_ID;
-        commitTransactionAndLoadHome(
-                new FavoriteItemsTransaction(mTargetContext).addItem(() -> item));
+        new FavoriteItemsTransaction(targetContext()).addItem(() -> item).commit();
+        loadLauncherSync();
     }
 
     private LauncherAppWidgetProviderInfo addWidgetToScreen(boolean hasConfigureScreen,
             boolean bindWidget, Consumer<LauncherAppWidgetInfo> itemOverride) {
         LauncherAppWidgetProviderInfo info = TestViewHelpers.findWidgetProvider(hasConfigureScreen);
-        commitTransactionAndLoadHome(new FavoriteItemsTransaction(mTargetContext)
+        new FavoriteItemsTransaction(targetContext())
                 .addItem(() -> {
-                    LauncherAppWidgetInfo item = createWidgetInfo(info, mTargetContext, bindWidget);
+                    LauncherAppWidgetInfo item =
+                            createWidgetInfo(info, targetContext(), bindWidget);
                     item.screenId = FIRST_SCREEN_ID;
                     itemOverride.accept(item);
                     return item;
-                }));
+                }).commit();
+        loadLauncherSync();
         return info;
     }
 
@@ -274,13 +286,13 @@
 
         Set<String> activePackage = getOnUiThread(() -> {
             Set<String> packages = new HashSet<>();
-            InstallSessionHelper.INSTANCE.get(mTargetContext).getActiveSessions()
+            InstallSessionHelper.INSTANCE.get(targetContext()).getActiveSessions()
                     .keySet().forEach(packageUserKey -> packages.add(packageUserKey.mPackageName));
             return packages;
         });
         while (true) {
             try {
-                mTargetContext.getPackageManager().getPackageInfo(
+                targetContext().getPackageManager().getPackageInfo(
                         pkg, PackageManager.GET_UNINSTALLED_PACKAGES);
             } catch (Exception e) {
                 if (!activePackage.contains(pkg)) {
diff --git a/tests/src/com/android/launcher3/ui/widget/TaplRequestPinItemTest.java b/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
similarity index 61%
rename from tests/src/com/android/launcher3/ui/widget/TaplRequestPinItemTest.java
rename to tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
index 74047f0..2fb7987 100644
--- a/tests/src/com/android/launcher3/ui/widget/TaplRequestPinItemTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
@@ -1,34 +1,41 @@
 /*
  * Copyright (C) 2017 The Android Open Source Project
  *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
+ * 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.ui.widget;
 
 import static android.app.PendingIntent.FLAG_MUTABLE;
 import static android.app.PendingIntent.FLAG_ONE_SHOT;
 
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNotSame;
 
+import static java.util.regex.Pattern.CASE_INSENSITIVE;
+
 import android.app.PendingIntent;
 import android.appwidget.AppWidgetManager;
+import android.content.Context;
 import android.content.Intent;
 import android.graphics.Color;
 import android.view.View;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.LargeTest;
-import androidx.test.runner.AndroidJUnit4;
+import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.BySelector;
 
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherSettings.Favorites;
@@ -37,14 +44,13 @@
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.shortcuts.ShortcutKey;
-import com.android.launcher3.tapl.AddToHomeScreenPrompt;
 import com.android.launcher3.testcomponent.AppWidgetNoConfig;
 import com.android.launcher3.testcomponent.AppWidgetWithConfig;
 import com.android.launcher3.testcomponent.RequestPinItemActivity;
-import com.android.launcher3.ui.AbstractLauncherUiTest;
+import com.android.launcher3.util.BaseLauncherActivityTest;
+import com.android.launcher3.util.BlockingBroadcastReceiver;
 import com.android.launcher3.util.LauncherBindableItemsContainer.ItemOperator;
-import com.android.launcher3.util.Wait;
-import com.android.launcher3.util.Wait.Condition;
+import com.android.launcher3.util.TestUtil;
 import com.android.launcher3.util.rule.ShellCommandRule;
 
 import org.junit.Before;
@@ -53,25 +59,27 @@
 import org.junit.runner.RunWith;
 
 import java.util.UUID;
+import java.util.regex.Pattern;
 
 /**
  * Test to verify pin item request flow.
  */
 @LargeTest
 @RunWith(AndroidJUnit4.class)
-public class TaplRequestPinItemTest extends AbstractLauncherUiTest<Launcher> {
+public class RequestPinItemTest extends BaseLauncherActivityTest<Launcher> {
 
     @Rule
     public ShellCommandRule mGrantWidgetRule = ShellCommandRule.grantWidgetBind();
 
+    @Rule
+    public ShellCommandRule mDefaultLauncherRule = ShellCommandRule.setDefaultLauncher();
+
     private String mCallbackAction;
     private String mShortcutId;
     private int mAppWidgetId;
 
-    @Override
     @Before
     public void setUp() throws Exception {
-        super.setUp();
         mCallbackAction = UUID.randomUUID().toString();
         mShortcutId = UUID.randomUUID().toString();
     }
@@ -81,9 +89,9 @@
 
     @Test
     public void testPinWidgetNoConfig() throws Throwable {
-        runTest("pinWidgetNoConfig", true, (info, view) -> info instanceof LauncherAppWidgetInfo &&
-                ((LauncherAppWidgetInfo) info).appWidgetId == mAppWidgetId &&
-                ((LauncherAppWidgetInfo) info).providerName.getClassName()
+        runTest("pinWidgetNoConfig", true, (info, view) -> info instanceof LauncherAppWidgetInfo
+                && ((LauncherAppWidgetInfo) info).appWidgetId == mAppWidgetId
+                && ((LauncherAppWidgetInfo) info).providerName.getClassName()
                         .equals(AppWidgetNoConfig.class.getName()));
     }
 
@@ -94,18 +102,18 @@
                 RequestPinItemActivity.class, "setRemoteViewColor").putExtra(
                 RequestPinItemActivity.EXTRA_PARAM + "0", Color.RED);
 
-        runTest("pinWidgetNoConfig", true, (info, view) -> info instanceof LauncherAppWidgetInfo &&
-                ((LauncherAppWidgetInfo) info).appWidgetId == mAppWidgetId &&
-                ((LauncherAppWidgetInfo) info).providerName.getClassName()
+        runTest("pinWidgetNoConfig", true, (info, view) -> info instanceof LauncherAppWidgetInfo
+                && ((LauncherAppWidgetInfo) info).appWidgetId == mAppWidgetId
+                && ((LauncherAppWidgetInfo) info).providerName.getClassName()
                         .equals(AppWidgetNoConfig.class.getName()), command);
     }
 
     @Test
     public void testPinWidgetWithConfig() throws Throwable {
         runTest("pinWidgetWithConfig", true,
-                (info, view) -> info instanceof LauncherAppWidgetInfo &&
-                        ((LauncherAppWidgetInfo) info).appWidgetId == mAppWidgetId &&
-                        ((LauncherAppWidgetInfo) info).providerName.getClassName()
+                (info, view) -> info instanceof LauncherAppWidgetInfo
+                        && ((LauncherAppWidgetInfo) info).appWidgetId == mAppWidgetId
+                        && ((LauncherAppWidgetInfo) info).providerName.getClassName()
                                 .equals(AppWidgetWithConfig.class.getName()));
     }
 
@@ -119,47 +127,48 @@
         runTest("pinShortcut", false, new ItemOperator() {
             @Override
             public boolean evaluate(ItemInfo info, View view) {
-                return info instanceof WorkspaceItemInfo &&
-                        info.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT &&
-                        ShortcutKey.fromItemInfo(info).getId().equals(mShortcutId);
+                return info instanceof WorkspaceItemInfo
+                        && info.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT
+                        && ShortcutKey.fromItemInfo(info).getId().equals(mShortcutId);
             }
         }, command);
     }
 
     private void runTest(String activityMethod, boolean isWidget, ItemOperator itemMatcher,
             Intent... commandIntents) throws Throwable {
-        commitTransactionAndLoadHome(new FavoriteItemsTransaction(mTargetContext));
+        new FavoriteItemsTransaction(targetContext()).commit();
+        loadLauncherSync();
 
         // Open Pin item activity
         BlockingBroadcastReceiver openMonitor = new BlockingBroadcastReceiver(
                 RequestPinItemActivity.class.getName());
-        mLauncher.
-                getWorkspace().
-                switchToAllApps().
-                getAppIcon("Test Pin Item").
-                launch(getAppPackageName());
+        Context testContext = getInstrumentation().getContext();
+        startAppFast(
+                testContext.getPackageName(),
+                new Intent(testContext, RequestPinItemActivity.class));
         assertNotNull(openMonitor.blockingGetExtraIntent());
 
         // Set callback
-        PendingIntent callback = PendingIntent.getBroadcast(mTargetContext, 0,
-                new Intent(mCallbackAction).setPackage(mTargetContext.getPackageName()),
+        PendingIntent callback = PendingIntent.getBroadcast(targetContext(), 0,
+                new Intent(mCallbackAction).setPackage(targetContext().getPackageName()),
                 FLAG_ONE_SHOT | FLAG_MUTABLE);
-        mTargetContext.sendBroadcast(RequestPinItemActivity.getCommandIntent(
+        targetContext().sendBroadcast(RequestPinItemActivity.getCommandIntent(
                 RequestPinItemActivity.class, "setCallback").putExtra(
                 RequestPinItemActivity.EXTRA_PARAM + "0", callback));
 
         for (Intent command : commandIntents) {
-            mTargetContext.sendBroadcast(command);
+            targetContext().sendBroadcast(command);
         }
 
         // call the requested method to start the flow
-        mTargetContext.sendBroadcast(RequestPinItemActivity.getCommandIntent(
+        targetContext().sendBroadcast(RequestPinItemActivity.getCommandIntent(
                 RequestPinItemActivity.class, activityMethod));
-        final AddToHomeScreenPrompt addToHomeScreenPrompt = mLauncher.getAddToHomeScreenPrompt();
 
         // Accept confirmation:
         BlockingBroadcastReceiver resultReceiver = new BlockingBroadcastReceiver(mCallbackAction);
-        addToHomeScreenPrompt.addAutomatically();
+        BySelector selector = By.text(Pattern.compile("^Add to home screen$", CASE_INSENSITIVE))
+                .pkg(targetContext().getPackageName());
+        uiDevice.wait(device -> device.findObject(selector), TestUtil.DEFAULT_UI_TIMEOUT).click();
         Intent result = resultReceiver.blockingGetIntent();
         assertNotNull(result);
         mAppWidgetId = result.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1);
@@ -167,29 +176,9 @@
             assertNotSame(-1, mAppWidgetId);
         }
 
-        // Go back to home
-        mLauncher.goHome();
-        Wait.atMost("", new ItemSearchCondition(itemMatcher), DEFAULT_ACTIVITY_TIMEOUT,
-                mLauncher);
-    }
-
-    /**
-     * Condition for for an item
-     */
-    private class ItemSearchCondition implements Condition {
-
-        private final ItemOperator mOp;
-
-        ItemSearchCondition(ItemOperator op) {
-            mOp = op;
-        }
-
-        @Override
-        public boolean isTrue() throws Throwable {
-            return mMainThreadExecutor.submit(() -> {
-                Launcher l = Launcher.ACTIVITY_TRACKER.getCreatedActivity();
-                return l != null && l.getWorkspace().getFirstMatch(mOp) != null;
-            }).get();
-        }
+        // Reload activity, so that the activity is focused
+        closeCurrentActivity();
+        loadLauncherSync();
+        getOnceNotNull("", l -> l.getWorkspace().getFirstMatch(itemMatcher));
     }
 }
diff --git a/tests/src/com/android/launcher3/ui/widget/TaplAddConfigWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/TaplAddConfigWidgetTest.java
deleted file mode 100644
index e6e02b4..0000000
--- a/tests/src/com/android/launcher3/ui/widget/TaplAddConfigWidgetTest.java
+++ /dev/null
@@ -1,173 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
-package com.android.launcher3.ui.widget;
-
-import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
-
-import static com.android.launcher3.util.LauncherBindableItemsContainer.ItemOperator;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNotSame;
-
-import android.appwidget.AppWidgetManager;
-import android.content.Intent;
-import android.view.View;
-
-import androidx.test.filters.LargeTest;
-import androidx.test.runner.AndroidJUnit4;
-
-import com.android.launcher3.Launcher;
-import com.android.launcher3.celllayout.FavoriteItemsTransaction;
-import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.model.data.LauncherAppWidgetInfo;
-import com.android.launcher3.testcomponent.WidgetConfigActivity;
-import com.android.launcher3.ui.AbstractLauncherUiTest;
-import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
-import com.android.launcher3.ui.TestViewHelpers;
-import com.android.launcher3.util.Wait;
-import com.android.launcher3.util.rule.ShellCommandRule;
-import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/**
- * Test to verify widget configuration is properly shown.
- */
-@LargeTest
-@RunWith(AndroidJUnit4.class)
-public class TaplAddConfigWidgetTest extends AbstractLauncherUiTest<Launcher> {
-
-    @Rule
-    public ShellCommandRule mGrantWidgetRule = ShellCommandRule.grantWidgetBind();
-
-    private LauncherAppWidgetProviderInfo mWidgetInfo;
-    private AppWidgetManager mAppWidgetManager;
-
-    private int mWidgetId;
-
-    @Override
-    @Before
-    public void setUp() throws Exception {
-        super.setUp();
-        mWidgetInfo = TestViewHelpers.findWidgetProvider(true /* hasConfigureScreen */);
-        mAppWidgetManager = AppWidgetManager.getInstance(mTargetContext);
-    }
-
-    @Test
-    @PortraitLandscape
-    public void testWidgetConfig() throws Throwable {
-        runTest(true);
-    }
-
-    @Test
-    @PortraitLandscape
-    public void testConfigCancelled() throws Throwable {
-        runTest(false);
-    }
-
-
-    /**
-     * @param acceptConfig accept the config activity
-     */
-    private void runTest(boolean acceptConfig) throws Throwable {
-        commitTransactionAndLoadHome(new FavoriteItemsTransaction(mTargetContext));
-
-        // Drag widget to homescreen
-        WidgetConfigStartupMonitor monitor = new WidgetConfigStartupMonitor();
-        mLauncher.getWorkspace()
-                .openAllWidgets()
-                .getWidget(mWidgetInfo.getLabel(mTargetContext.getPackageManager()))
-                .dragToWorkspace(true, false);
-        // Widget id for which the config activity was opened
-        mWidgetId = monitor.getWidgetId();
-
-        // Verify that the widget id is valid and bound
-        assertNotNull(mAppWidgetManager.getAppWidgetInfo(mWidgetId));
-
-        setResultAndWaitForAnimation(acceptConfig);
-        if (acceptConfig) {
-            Wait.atMost("", new WidgetSearchCondition(), DEFAULT_ACTIVITY_TIMEOUT, mLauncher);
-            assertNotNull(mAppWidgetManager.getAppWidgetInfo(mWidgetId));
-        } else {
-            // Verify that the widget id is deleted.
-            Wait.atMost("", () -> mAppWidgetManager.getAppWidgetInfo(mWidgetId) == null,
-                    DEFAULT_ACTIVITY_TIMEOUT, mLauncher);
-        }
-    }
-
-    private static void setResult(boolean success) {
-        getInstrumentation().getTargetContext().sendBroadcast(
-                WidgetConfigActivity.getCommandIntent(WidgetConfigActivity.class,
-                        success ? "clickOK" : "clickCancel"));
-    }
-
-    private void setResultAndWaitForAnimation(boolean success) {
-        if (mLauncher.isLauncher3()) {
-            setResult(success);
-        } else {
-            mLauncher.executeAndWaitForWallpaperAnimation(
-                    () -> setResult(success),
-                    "setting widget coinfig result");
-        }
-    }
-
-    /**
-     * Condition for searching widget id
-     */
-    private class WidgetSearchCondition implements Wait.Condition, ItemOperator {
-
-        @Override
-        public boolean isTrue() throws Throwable {
-            return mMainThreadExecutor.submit(() -> {
-                Launcher l = Launcher.ACTIVITY_TRACKER.getCreatedActivity();
-                return l != null && l.getWorkspace().getFirstMatch(this) != null;
-            }).get();
-        }
-
-        @Override
-        public boolean evaluate(ItemInfo info, View view) {
-            return info instanceof LauncherAppWidgetInfo
-                    && ((LauncherAppWidgetInfo) info).providerName.getClassName().equals(
-                            mWidgetInfo.provider.getClassName())
-                    && ((LauncherAppWidgetInfo) info).appWidgetId == mWidgetId;
-        }
-    }
-
-    /**
-     * Broadcast receiver for receiving widget config activity status.
-     */
-    private class WidgetConfigStartupMonitor extends BlockingBroadcastReceiver {
-
-        public WidgetConfigStartupMonitor() {
-            super(WidgetConfigActivity.class.getName());
-        }
-
-        public int getWidgetId() throws InterruptedException {
-            Intent intent = blockingGetExtraIntent();
-            assertNotNull("Null EXTRA_INTENT", intent);
-            assertEquals("Intent action is not ACTION_APPWIDGET_CONFIGURE",
-                    AppWidgetManager.ACTION_APPWIDGET_CONFIGURE, intent.getAction());
-            int widgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
-                    LauncherAppWidgetInfo.NO_ID);
-            assertNotSame("Widget id is NO_ID", widgetId, LauncherAppWidgetInfo.NO_ID);
-            return widgetId;
-        }
-    }
-}
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/TaplWidgetPickerTest.java b/tests/src/com/android/launcher3/ui/widget/WidgetPickerTest.java
similarity index 68%
rename from tests/src/com/android/launcher3/ui/widget/TaplWidgetPickerTest.java
rename to tests/src/com/android/launcher3/ui/widget/WidgetPickerTest.java
index 19c5850..caad1d9 100644
--- a/tests/src/com/android/launcher3/ui/widget/TaplWidgetPickerTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/WidgetPickerTest.java
@@ -22,24 +22,30 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.MediumTest;
 
+import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.Launcher;
-import com.android.launcher3.tapl.Widgets;
-import com.android.launcher3.ui.AbstractLauncherUiTest;
 import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
+import com.android.launcher3.util.BaseLauncherActivityTest;
+import com.android.launcher3.util.rule.ScreenRecordRule;
 import com.android.launcher3.util.rule.ScreenRecordRule.ScreenRecord;
+import com.android.launcher3.views.OptionsPopupView;
 import com.android.launcher3.widget.picker.WidgetsFullSheet;
 import com.android.launcher3.widget.picker.WidgetsRecyclerView;
 
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TestRule;
 import org.junit.runner.RunWith;
 
 /**
- * This test run in both Out of process (Oop) and in-process (Ipc).
  * Make sure the basic interactions with the WidgetPicker works.
  */
 @MediumTest
 @RunWith(AndroidJUnit4.class)
-public class TaplWidgetPickerTest extends AbstractLauncherUiTest<Launcher> {
+public class WidgetPickerTest extends BaseLauncherActivityTest<Launcher> {
+
+    @Rule
+    public TestRule screenRecordRule = new ScreenRecordRule();
 
     private WidgetsRecyclerView getWidgetsView(Launcher launcher) {
         return WidgetsFullSheet.getWidgetsView(launcher);
@@ -56,30 +62,21 @@
     @ScreenRecord
     @PortraitLandscape
     public void testWidgets() {
-        mLauncher.goHome();
+        loadLauncherSync();
         // Test opening widgets.
         executeOnLauncher(launcher ->
                 assertTrue("Widgets is initially opened", getWidgetsView(launcher) == null));
-        Widgets widgets = mLauncher.getWorkspace().openAllWidgets();
-        assertNotNull("openAllWidgets() returned null", widgets);
-        widgets = mLauncher.getAllWidgets();
+        assertNotNull("openAllWidgets() returned null",
+                getFromLauncher(OptionsPopupView::openWidgets));
+        WidgetsRecyclerView widgets = getFromLauncher(this::getWidgetsView);
         assertNotNull("getAllWidgets() returned null", widgets);
-        executeOnLauncher(launcher ->
-                assertTrue("Widgets is not shown", getWidgetsView(launcher).isShown()));
+        executeOnLauncher(launcher -> assertTrue("Widgets is not shown", widgets.isShown()));
         executeOnLauncher(launcher -> assertEquals("Widgets is scrolled upon opening",
                 0, getWidgetsScroll(launcher)));
 
-        // Test flinging widgets.
-        widgets.flingForward();
-        Integer flingForwardY = getFromLauncher(launcher -> getWidgetsScroll(launcher));
-        executeOnLauncher(launcher -> assertTrue("Flinging forward didn't scroll widgets",
-                flingForwardY > 0));
+        executeOnLauncher(AbstractFloatingView::closeAllOpenViews);
+        uiDevice.waitForIdle();
 
-        widgets.flingBackward();
-        executeOnLauncher(launcher -> assertTrue("Flinging backward didn't scroll widgets",
-                getWidgetsScroll(launcher) < flingForwardY));
-
-        mLauncher.goHome();
         waitForLauncherCondition("Widgets were not closed",
                 launcher -> getWidgetsView(launcher) == null);
     }
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..c623513
--- /dev/null
+++ b/tests/src/com/android/launcher3/ui/workspace/ThemeIconsTest.java
@@ -0,0 +1,172 @@
+/*
+ * 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.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.ViewGroup;
+
+import androidx.test.filters.LargeTest;
+
+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;
+
+/**
+ * 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);
+        }
+        return (BubbleTextView) searchView(parent, v ->
+                v instanceof BubbleTextView btv
+                    && btv.getContentDescription() != null
+                        && title.equals(btv.getContentDescription().toString()));
+    }
+
+    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);
+        });
+    }
+}
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..61fa7d5
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/BaseLauncherActivityTest.kt
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.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 android.view.View
+import android.view.ViewGroup
+import androidx.core.view.children
+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.R
+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.Predicate
+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 }
+
+    @JvmField val uiDevice = UiDevice.getInstance(getInstrumentation())
+
+    @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) {
+        uiDevice.waitForIdle()
+        val eventTime = SystemClock.uptimeMillis()
+        val event =
+            KeyEvent(
+                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,
+            )
+        executeOnLauncher { it.dispatchKeyEvent(event) }
+    }
+
+    @JvmOverloads
+    fun startAppFast(
+        packageName: String,
+        intent: 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.waitForIdle()
+    }
+
+    fun freezeAllApps() = executeOnLauncher {
+        it.appsView.appsStore.enableDeferUpdates(DEFER_UPDATES_TEST)
+    }
+
+    fun executeShellCommand(cmd: String) = uiDevice.executeShellCommand(cmd)
+
+    fun addToWorkspace(view: View) {
+        TestUtil.runOnExecutorSync(Executors.MAIN_EXECUTOR) {
+            view.accessibilityDelegate.performAccessibilityAction(
+                view,
+                R.id.action_add_to_workspace,
+                null,
+            )
+        }
+        UiDevice.getInstance(getInstrumentation()).waitForIdle()
+    }
+
+    fun ViewGroup.searchView(filter: Predicate<View>): View? {
+        if (filter.test(this)) return this
+        for (child in children) {
+            if (filter.test(child)) return child
+            if (child is ViewGroup)
+                child.searchView(filter)?.let {
+                    return it
+                }
+        }
+        return null
+    }
+}
diff --git a/tests/src/com/android/launcher3/util/BlockingBroadcastReceiver.kt b/tests/src/com/android/launcher3/util/BlockingBroadcastReceiver.kt
new file mode 100644
index 0000000..20881d1
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/BlockingBroadcastReceiver.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.util
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.os.Parcelable
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.TimeUnit.SECONDS
+
+private const val DEFAULT_BROADCAST_TIMEOUT_SECS: Long = 10
+
+/** Broadcast receiver which blocks until the result is received. */
+open class BlockingBroadcastReceiver(action: String) : BroadcastReceiver() {
+
+    val value = CompletableFuture<Intent>()
+
+    init {
+        getInstrumentation()
+            .targetContext
+            .registerReceiver(this, IntentFilter(action), Context.RECEIVER_EXPORTED)
+    }
+
+    override fun onReceive(context: Context, intent: Intent) {
+        value.complete(intent)
+    }
+
+    @Throws(InterruptedException::class)
+    fun blockingGetIntent(): Intent =
+        value.get(DEFAULT_BROADCAST_TIMEOUT_SECS, SECONDS).also {
+            getInstrumentation().targetContext.unregisterReceiver(this)
+        }
+
+    @Throws(InterruptedException::class)
+    fun blockingGetExtraIntent(): Intent? =
+        blockingGetIntent().getParcelableExtra<Parcelable>(Intent.EXTRA_INTENT) as Intent?
+}
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/com/android/launcher3/util/rule/FailureWatcher.java b/tests/src/com/android/launcher3/util/rule/FailureWatcher.java
index 7bdc040..3b85309 100644
--- a/tests/src/com/android/launcher3/util/rule/FailureWatcher.java
+++ b/tests/src/com/android/launcher3/util/rule/FailureWatcher.java
@@ -12,7 +12,7 @@
 
 import com.android.app.viewcapture.data.ExportedData;
 import com.android.launcher3.tapl.LauncherInstrumentation;
-import com.android.launcher3.ui.AbstractLauncherUiTest;
+import com.android.launcher3.ui.BaseLauncherTaplTest;
 
 import org.junit.rules.TestWatcher;
 import org.junit.runner.Description;
@@ -57,7 +57,7 @@
     @Override
     protected void succeeded(Description description) {
         super.succeeded(description);
-        AbstractLauncherUiTest.checkDetectedLeaks(mLauncher);
+        BaseLauncherTaplTest.checkDetectedLeaks(mLauncher);
     }
 
     @Override
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,